diff --git a/webapp/packages/core-blocks/src/CommonDialog/ConfirmationDialog.tsx b/webapp/packages/core-blocks/src/CommonDialog/ConfirmationDialog.tsx index ef9655973d4..9bc65482819 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/ConfirmationDialog.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/ConfirmationDialog.tsx @@ -37,6 +37,12 @@ export interface ConfirmationDialogPayload { confirmActionText?: TLocalizationToken; cancelActionText?: TLocalizationToken; extraActionText?: TLocalizationToken; + /** + * Async action run when the confirm button is pressed. + * While it runs the confirm button shows a loader and is disabled. + * The dialog resolves only if it returns `true`. + */ + onConfirm?: () => Promise; } export interface ConfirmationDialogResult { @@ -67,10 +73,25 @@ export const ConfirmationDialog: DialogComponent )} - diff --git a/webapp/packages/core-blocks/src/CommonDialog/confirmUnsavedChanges.tsx b/webapp/packages/core-blocks/src/CommonDialog/confirmUnsavedChanges.tsx new file mode 100644 index 00000000000..c16524bdb43 --- /dev/null +++ b/webapp/packages/core-blocks/src/CommonDialog/confirmUnsavedChanges.tsx @@ -0,0 +1,59 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2026 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { type CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import type { TLocalizationToken } from '@cloudbeaver/core-localization'; + +import { ConfirmationDialog } from './ConfirmationDialog.js'; + +export interface IUnsavedChangesProvider { + readonly isChanged: boolean; + readonly isSaving?: boolean; + /** Returns whether the save succeeded. `undefined` means there was nothing to save and navigation may proceed. */ + save(): Promise | undefined; + reset(): void; + title?: string; + subTitle?: string; + message?: TLocalizationToken; +} + +/** + * Shows a unified "Save / Don't save / Cancel" dialog when the provider has unsaved changes. + * + * @returns `true` when navigation may proceed (saved successfully or discarded), `false` to block it. + */ +export async function confirmUnsavedChanges(commonDialogService: CommonDialogService, provider: IUnsavedChangesProvider): Promise { + if (!provider.isChanged) { + return true; + } + + if (provider.isSaving) { + return false; + } + + const { status, result } = await commonDialogService.open(ConfirmationDialog, { + title: provider.title ?? 'ui_save_reminder', + subTitle: provider.subTitle, + message: provider.message ?? 'ui_changes_might_be_lost', + confirmActionText: 'ui_processing_save', + extraActionText: 'ui_processing_dont_save', + cancelActionText: 'ui_processing_cancel', + showExtraAction: true, + onConfirm: async () => (await provider.save()) ?? true, + }); + + if (status === DialogueStateResult.Resolved) { + return true; + } + + if (result?.isExtraAction) { + provider.reset(); + return true; + } + + return false; +} diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 7e3e932b9bb..4e6b2a87523 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -17,6 +17,7 @@ export * from './CommonDialog/CommonDialog/CommonDialogBody.js'; export * from './CommonDialog/CommonDialog/CommonDialogFooter.js'; export * from './CommonDialog/CommonDialog/CommonDialogHeader.js'; export * from './CommonDialog/CommonDialog/CommonDialogWrapper.js'; +export * from './CommonDialog/confirmUnsavedChanges.js'; export * from './CommonDialog/ConfirmationDialog.js'; export { default as ConfirmationDialogStyles } from './CommonDialog/ConfirmationDialog.module.css'; export * from './CommonDialog/ConfirmationDialogDelete.js'; diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 693fb99d523..ba9bf8f274b 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -20,6 +20,7 @@ export default [ ['ui_processing_ok', 'Ok'], ['ui_processing_create', 'Create'], ['ui_processing_save', 'Save'], + ['ui_processing_dont_save', "Don't save"], ['ui_processing_saving', 'Saving...'], ['ui_processing_do_you_want_to_proceed', 'Do you want to proceed?'], ['ui_processing_saved', 'Saved'], diff --git a/webapp/packages/core-localization/src/locales/fr.ts b/webapp/packages/core-localization/src/locales/fr.ts index 833d52ed603..e77f01a44b2 100644 --- a/webapp/packages/core-localization/src/locales/fr.ts +++ b/webapp/packages/core-localization/src/locales/fr.ts @@ -20,6 +20,7 @@ export default [ ['ui_processing_ok', 'Ok'], ['ui_processing_create', 'Créer'], ['ui_processing_save', 'Sauvegarder'], + ['ui_processing_dont_save', 'Ne pas sauvegarder'], ['ui_processing_saved', 'Sauvegardé'], ['ui_processing_stop', 'Arrêter'], ['ui_processing_skip', 'Passer'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index a0cc800593d..45d40ad3419 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -19,6 +19,7 @@ export default [ ['ui_processing_ok', 'Ok'], ['ui_processing_create', 'Crea'], ['ui_processing_save', 'Salva'], + ['ui_processing_dont_save', 'Non salvare'], ['ui_processing_saving', 'Saving...'], ['ui_processing_do_you_want_to_proceed', 'Do you want to proceed?'], ['ui_processing_saved', 'Saved'], @@ -95,6 +96,7 @@ export default [ ['ui_no_items_placeholder', 'Non ci sono ancora elementi.'], ['ui_search_no_result_placeholder', 'Nessun risultato trovato.'], ['ui_save_reminder', 'Ci sono modifiche non salvate.'], + ['ui_changes_might_be_lost', 'Le tue modifiche potrebbero andare perse'], ['ui_yes', 'Sì'], ['ui_no', 'No'], ['ui_select_all', 'Select all'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 4d7eb39041a..2f1d5d1b2ea 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -18,6 +18,7 @@ export default [ ['ui_processing_ok', 'Принять'], ['ui_processing_create', 'Создать'], ['ui_processing_save', 'Сохранить'], + ['ui_processing_dont_save', 'Не сохранять'], ['ui_processing_saving', 'Сохранение...'], ['ui_processing_do_you_want_to_proceed', 'Хотите продолжить?'], ['ui_processing_saved', 'Сохранено'], diff --git a/webapp/packages/core-localization/src/locales/vi.ts b/webapp/packages/core-localization/src/locales/vi.ts index 438b20f77ad..9a2e0c710cd 100644 --- a/webapp/packages/core-localization/src/locales/vi.ts +++ b/webapp/packages/core-localization/src/locales/vi.ts @@ -20,6 +20,7 @@ export default [ ['ui_processing_ok', 'Đồng ý'], ['ui_processing_create', 'Tạo'], ['ui_processing_save', 'Lưu'], + ['ui_processing_dont_save', 'Không lưu'], ['ui_processing_saving', 'Đang lưu...'], ['ui_processing_do_you_want_to_proceed', 'Bạn có muốn tiếp tục không?'], ['ui_processing_saved', 'Đã lưu'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index cb70c11eb54..a2d0a8765a7 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -19,6 +19,7 @@ export default [ ['ui_processing_ok', '好'], ['ui_processing_create', '创建'], ['ui_processing_save', '保存'], + ['ui_processing_dont_save', '不保存'], ['ui_processing_saving', '保存中...'], ['ui_processing_do_you_want_to_proceed', '是否继续?'], ['ui_processing_saved', '已保存'], diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 2090a0c24b4..205f1afbbdd 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -45,6 +45,7 @@ export interface IFormState { isError: boolean; isCancelled: boolean; isChanged: boolean; + isSaving: boolean; isReadOnly: boolean; save(providedContext?: IExecutionContext>): Promise; diff --git a/webapp/packages/core-ui/src/Tabs/TabsState.tsx b/webapp/packages/core-ui/src/Tabs/TabsState.tsx index 985217619db..00dc7523736 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsState.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabsState.tsx @@ -9,7 +9,7 @@ import { TabProvider, useStoreState, useTabStore } from '@dbeaver/ui-kit'; import { action, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { useAutoLoad, useExecutor, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks'; import { useDataContext } from '@cloudbeaver/core-data-context'; @@ -28,7 +28,7 @@ export type TabsStateProps> = ExtractContainerProps & /** Default selected tab id */ selectedId?: string; orientation?: 'horizontal' | 'vertical'; - /** Provide a tab Id to control tabs state */ + /** Provide a tab Id to fully control the selected tab: the selection always mirrors `currentTabId`. */ currentTabId?: string | null; container?: ITabsContainer; localState?: MetadataMap; @@ -109,15 +109,16 @@ export const TabsState = observer(function TabsState>({ selected, store, tabList, + currentTabId, }, ); - useEffect(() => { - if (isNotNullDefined(currentTabId)) { + useLayoutEffect(() => { + if (isNotNullDefined(currentTabId) && selected !== currentTabId) { dynamic.store.setSelectedId(currentTabId); dynamic.selectedId = currentTabId; } - }, [currentTabId]); + }, [currentTabId, selected]); useEffect(() => { if (displayed.length > 0 && autoSelect) { @@ -139,6 +140,9 @@ export const TabsState = observer(function TabsState>({ ExecutorInterrupter.interrupt(contexts); return; } + if (isNotNullDefined(dynamic.currentTabId)) { + return; + } dynamic.selectedId = data.tabId; if (dynamic.store.getState().selectedId !== data.tabId) { dynamic.store.setSelectedId(data.tabId); @@ -159,7 +163,7 @@ export const TabsState = observer(function TabsState>({ const currentSelectedId = selected; useEffect(() => { - if (!isNotNullDefined(currentSelectedId) || dynamic.selectedId === currentSelectedId) { + if (isNotNullDefined(currentTabId) || !isNotNullDefined(currentSelectedId) || dynamic.selectedId === currentSelectedId) { return; } @@ -167,7 +171,7 @@ export const TabsState = observer(function TabsState>({ tabId: currentSelectedId, props, }); - }, [currentSelectedId]); + }, [currentSelectedId, currentTabId]); const value = useObservableRef>( () => ({ diff --git a/webapp/packages/core-ui/src/module.ts b/webapp/packages/core-ui/src/module.ts index b100870282f..40bfb2c6d85 100644 --- a/webapp/packages/core-ui/src/module.ts +++ b/webapp/packages/core-ui/src/module.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts index 5744382563b..2df0f246edd 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts @@ -6,9 +6,9 @@ * you may not use this file except in compliance with the License. */ import { AdministrationItemService, AdministrationItemType, ConfigurationWizardService } from '@cloudbeaver/core-administration'; -import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; +import { confirmUnsavedChanges, importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { SessionDataResource } from '@cloudbeaver/core-root'; import { formValidationContext } from '@cloudbeaver/core-ui'; import { getFirstException } from '@cloudbeaver/core-utils'; @@ -112,25 +112,15 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap { this.serverConfigurationFormStateManager.destroy(); }, - canDeActivate: async configurationWizard => { + canDeActivate: configurationWizard => { const state = this.serverConfigurationFormStateManager.formState; - if (state?.isSaving) { - return false; - } - - if (!configurationWizard && state?.isChanged) { - const { status } = await this.commonDialogService.open(ConfirmationDialog, { - title: 'ui_save_reminder', - message: 'ui_are_you_sure', - }); - - if (status === DialogueStateResult.Rejected) { - return false; - } - } - - return true; + return confirmUnsavedChanges(this.commonDialogService, { + isChanged: !configurationWizard && !!state?.isChanged, + isSaving: state?.isSaving, + save: () => this.serverConfigurationFormStateManager.save(), + reset: () => state?.reset(), + }); }, getContentComponent: () => ServerConfigurationPage, getDrawerComponent: () => ServerConfigurationDrawerItem, diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx index f1c65299b61..650a3f2ee14 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/CreateTeam.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx index 7919b4473bf..7a0b89416e2 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsTable/TeamEdit.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -8,9 +8,18 @@ import { observer } from 'mobx-react-lite'; import { TeamsResource } from '@cloudbeaver/core-authentication'; -import { ColoredContainer, ConfirmationDialog, GroupBack, GroupTitle, Text, useExecutor, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { + ColoredContainer, + confirmUnsavedChanges, + GroupBack, + GroupTitle, + Text, + useExecutor, + useResource, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { FormMode } from '@cloudbeaver/core-ui'; @@ -35,16 +44,14 @@ export const TeamEdit = observer(function TeamEdit({ item }) { executor: teamsTableOptionsPanelService.onClose, handlers: [ async function closeHandler(event, contexts) { - if (formState.isChanged && event === 'before') { - const { status } = await commonDialogService.open(ConfirmationDialog, { - title: 'ui_save_reminder', - message: 'ui_are_you_sure', - confirmActionText: 'ui_yes', - }); + if (event !== 'before') { + return; + } + + const confirmed = await confirmUnsavedChanges(commonDialogService, formState); - if (status === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } + if (!confirmed) { + ExecutorInterrupter.interrupt(contexts); } }, ], diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts index dce23314acd..07339ce4aee 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersAdministrationService.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,8 +9,9 @@ import React from 'react'; import { AdministrationItemService, type IAdministrationItem } from '@cloudbeaver/core-administration'; import { type AdminUser, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication'; -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; +import { confirmUnsavedChanges, PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { TabsContainer } from '@cloudbeaver/core-ui'; import { CreateTeamService } from './Teams/TeamsTable/CreateTeamService.js'; @@ -36,7 +37,7 @@ export interface IUserDetailsInfoProps { user: AdminUser; } -@injectable(() => [AdministrationItemService, CreateUserService, TeamsResource, CreateTeamService, UsersResource]) +@injectable(() => [AdministrationItemService, CreateUserService, TeamsResource, CreateTeamService, UsersResource, CommonDialogService]) export class UsersAdministrationService extends Bootstrap { readonly tabsContainer: TabsContainer; readonly userDetailsInfoPlaceholder: PlaceholderContainer; @@ -49,6 +50,7 @@ export class UsersAdministrationService extends Bootstrap { private readonly teamsResource: TeamsResource, private readonly createTeamService: CreateTeamService, private readonly usersResource: UsersResource, + private readonly commonDialogService: CommonDialogService, ) { super(); this.userDetailsInfoPlaceholder = new PlaceholderContainer(); @@ -66,11 +68,13 @@ export class UsersAdministrationService extends Bootstrap { }, { name: EUsersAdministrationSub.Users, + canDeActivate: this.confirmUserCreate.bind(this), onDeActivate: this.cancelUserCreate.bind(this), }, { name: EUsersAdministrationSub.Teams, onActivate: this.loadTeams.bind(this), + canDeActivate: this.confirmTeamCreate.bind(this), onDeActivate: this.cancelTeamCreate.bind(this), }, ], @@ -81,6 +85,26 @@ export class UsersAdministrationService extends Bootstrap { this.userDetailsInfoPlaceholder.add(UserCredentialsList, 0); } + private confirmUserCreate() { + const state = this.createUserService.state; + + if (!state) { + return true; + } + + return confirmUnsavedChanges(this.commonDialogService, state); + } + + private confirmTeamCreate() { + const data = this.createTeamService.data; + + if (!data) { + return true; + } + + return confirmUnsavedChanges(this.commonDialogService, data); + } + private cancelUserCreate(param: string | null, configurationWizard: boolean, outside: boolean) { if (param === 'create') { this.createUserService.close(); diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx index e42f815543f..dec753a3d85 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UserEdit.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import { ColoredContainer, - ConfirmationDialog, + confirmUnsavedChanges, GroupBack, GroupTitle, Loader, @@ -19,7 +19,7 @@ import { useTranslate, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { FormMode } from '@cloudbeaver/core-ui'; @@ -37,16 +37,14 @@ export const UserEdit = observer>(function UserEdit executor: usersTableOptionsPanelService.onClose, handlers: [ async function closeHandler(event, contexts) { - if (state.isChanged && event === 'before') { - const { status } = await commonDialogService.open(ConfirmationDialog, { - title: 'ui_save_reminder', - message: 'ui_are_you_sure', - confirmActionText: 'ui_yes', - }); + if (event !== 'before') { + return; + } + + const confirmed = await confirmUnsavedChanges(commonDialogService, state); - if (status === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } + if (!confirmed) { + ExecutorInterrupter.interrupt(contexts); } }, ], diff --git a/webapp/packages/plugin-settings-administration/package.json b/webapp/packages/plugin-settings-administration/package.json index 3c15980130f..c651b6fda0c 100644 --- a/webapp/packages/plugin-settings-administration/package.json +++ b/webapp/packages/plugin-settings-administration/package.json @@ -25,6 +25,7 @@ "@cloudbeaver/core-administration": "workspace:*", "@cloudbeaver/core-blocks": "workspace:*", "@cloudbeaver/core-di": "workspace:*", + "@cloudbeaver/core-dialogs": "workspace:*", "@cloudbeaver/core-events": "workspace:*", "@cloudbeaver/core-localization": "workspace:*", "@cloudbeaver/core-settings": "workspace:*", diff --git a/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx b/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx index dfd808f9876..da5e247ed87 100644 --- a/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx +++ b/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2025 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. diff --git a/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts b/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts index 949879a6666..2042c51cffc 100644 --- a/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts +++ b/webapp/packages/plugin-settings-administration/src/SettingsAdministrationPluginBootstrap.ts @@ -1,20 +1,30 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import { AdministrationItemService, AdministrationItemType } from '@cloudbeaver/core-administration'; -import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { confirmUnsavedChanges, importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import type { IEditableSettingsSource } from '@cloudbeaver/core-settings'; + +import { SettingsAdministrationService } from './SettingsAdministrationService.js'; const SettingsAdministration = importLazyComponent(() => import('./SettingsAdministration.js').then(module => module.SettingsAdministration)); const SettingsDrawerItem = importLazyComponent(() => import('./SettingsDrawerItem.js').then(module => module.SettingsDrawerItem)); -@injectable(() => [AdministrationItemService]) +@injectable(() => [AdministrationItemService, SettingsAdministrationService, CommonDialogService, NotificationService]) export class SettingsAdministrationPluginBootstrap extends Bootstrap { - constructor(private readonly administrationItemService: AdministrationItemService) { + constructor( + private readonly administrationItemService: AdministrationItemService, + private readonly settingsAdministrationService: SettingsAdministrationService, + private readonly commonDialogService: CommonDialogService, + private readonly notificationService: NotificationService, + ) { super(); } @@ -23,8 +33,42 @@ export class SettingsAdministrationPluginBootstrap extends Bootstrap { name: 'settings', type: AdministrationItemType.Administration, order: 3, + canDeActivate: this.confirmUnsavedSettings.bind(this), getContentComponent: () => SettingsAdministration, getDrawerComponent: () => SettingsDrawerItem, }); } + + private confirmUnsavedSettings() { + const edited = this.settingsAdministrationService.tabsContainer.tabInfoList + .map(info => info.options?.source) + .filter((source): source is IEditableSettingsSource => source?.isEdited() === true); + + if (edited.length === 0) { + return true; + } + + return confirmUnsavedChanges(this.commonDialogService, { + get isChanged() { + return edited.some(source => source.isEdited()); + }, + save: async () => { + try { + for (const source of edited) { + await source.save(); + } + this.notificationService.logSuccess({ title: 'plugin_settings_administration_settings_save_success' }); + return true; + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_settings_administration_settings_save_fail'); + return false; + } + }, + reset: () => { + for (const source of edited) { + source.clear(); + } + }, + }); + } } diff --git a/webapp/packages/plugin-settings-administration/tsconfig.json b/webapp/packages/plugin-settings-administration/tsconfig.json index 302f30fbbd5..14193ffd1a4 100644 --- a/webapp/packages/plugin-settings-administration/tsconfig.json +++ b/webapp/packages/plugin-settings-administration/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../core-di" }, + { + "path": "../core-dialogs" + }, { "path": "../core-events" }, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 0aeb995cff4..f4decb88be0 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -4192,6 +4192,7 @@ __metadata: "@cloudbeaver/core-blocks": "workspace:*" "@cloudbeaver/core-cli": "workspace:*" "@cloudbeaver/core-di": "workspace:*" + "@cloudbeaver/core-dialogs": "workspace:*" "@cloudbeaver/core-events": "workspace:*" "@cloudbeaver/core-localization": "workspace:*" "@cloudbeaver/core-settings": "workspace:*"