dbeaver/pro#8068 [CB] Fix: form reload on save and AI settings language (#4140)

* dbeaver/pro#8068 fix: FormPart reload after save: race condition between loaded=false and savingPromise

Problem:
FormPart.save() sets loaded=false after successful save, expecting
useAutoLoad to call load() and reload data from server. However,
load() is blocked by formState.savingPromise guard (added recently in dbeaver/pro#8151, 9c406a4e1e),
which is only cleared later in FormState.save() finally block.
By the time savingPromise is cleared, no observable changes trigger
a React re-render, so useAutoLoad never retries — loaded stays false
forever and isChanged always returns false, permanently disabling
the Save button.

Solution:
Make FormPart.save() self-contained by adding an inline post-save
reload. After saveChanges(), set loaded=false (so setInitialState
resets both initialState and state), call loader() directly, then
set loaded=true. This eliminates the dependency on the external
useAutoLoad mechanism for post-save reload, avoiding the race
condition with savingPromise entirely.

* dbeaver/pro#8068 fix(Combobox): preserve custom value on Enter key press

When allowCustomValue is enabled, pressing Enter was selecting
an auto-highlighted item from the dropdown instead of keeping
the user's typed value. Disable autoSelect and close the popover
via Ariakit store on Enter to match the blur behavior.

* dbeaver/pro#8068 fix: after saveChanges(), reset state synchronously via setInitialState

* dbeaver/pro#8068 fix: remove redundant state loading in FormPart class

* dbeaver/pro#8068 feat: improve loadable state handling in useAutoLoad and FormPart

---------

Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
Sychev Andrey
2026-02-23 17:18:50 +01:00
committed by GitHub
co-authored by Evgenia mr-anton-t
parent 457909dd82
commit 801139e3ff
5 changed files with 42 additions and 16 deletions
@@ -30,8 +30,8 @@ export interface ComboboxProps extends Omit<AriaComboboxProps, 'size'> {
size?: 'small' | 'medium' | 'large' | 'xlarge';
}
export function ComboboxInput({ size, ...props }: ComboboxProps): React.JSX.Element {
return <Combobox {...props} className={clsx('dbv-kit-combobox', `dbv-kit-combobox--${size ?? 'medium'}`, props.className)} autoSelect />;
export function ComboboxInput({ size, autoSelect = true, ...props }: ComboboxProps): React.JSX.Element {
return <Combobox {...props} className={clsx('dbv-kit-combobox', `dbv-kit-combobox--${size ?? 'medium'}`, props.className)} autoSelect={autoSelect} />;
}
export interface ComboboxPopoverProps extends AriaComboboxPopoverProps {
@@ -7,7 +7,17 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext, useId, useState } from 'react';
import { ComboboxInput, ComboboxItem, clsx, Spinner, ComboboxPopover, ComboboxDisclosure, ComboboxProvider, ComboboxCancel } from '@dbeaver/ui-kit';
import {
ComboboxInput,
ComboboxItem,
clsx,
Spinner,
ComboboxPopover,
ComboboxDisclosure,
ComboboxProvider,
ComboboxCancel,
useComboboxStore,
} from '@dbeaver/ui-kit';
import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps.js';
import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps.js';
@@ -165,12 +175,28 @@ export const Combobox: ComboboxType = observer(function Combobox({
onChange?.(value);
}
const comboboxStore = useComboboxStore({
value: displayValue,
setValue: setInputValue,
selectedValue: selectedValue,
defaultValue: comboboxDefaultValue,
defaultSelectedValue: comboboxDefaultSelectedValue as string,
setSelectedValue: handleSelect,
});
function handleBlur() {
if (!allowCustomValue) {
setInputValue(null);
}
}
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (allowCustomValue && event.key === 'Enter') {
event.preventDefault();
comboboxStore.setOpen(false);
}
}
function handleSetValueOnClick() {
if (allowCustomValue) {
return true;
@@ -194,14 +220,7 @@ export const Combobox: ComboboxType = observer(function Combobox({
{children}
</FieldLabel>
)}
<ComboboxProvider
value={displayValue}
setValue={setInputValue}
selectedValue={selectedValue}
defaultValue={comboboxDefaultValue}
defaultSelectedValue={comboboxDefaultSelectedValue as string}
setSelectedValue={handleSelect}
>
<ComboboxProvider store={comboboxStore}>
<div className="tw:relative tw:flex tw:flex-1 tw:items-center tw:gap-2">
<ComboboxInput
defaultValue={comboboxDefaultValue}
@@ -216,7 +235,9 @@ export const Combobox: ComboboxType = observer(function Combobox({
title={title}
id={inputId}
size={size}
autoSelect={!allowCustomValue}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
{...rest}
/>
{loading ? (
@@ -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.
@@ -31,7 +31,7 @@ export function useAutoLoad(
for (const loader of state as ReadonlyArray<ILoadableState>) {
getComputed(
// activate mobx subscriptions
() => (!loader.isLoaded() || loader.isOutdated?.() === true) && !loader.isError(),
() => (!loader.isLoaded() || loader.isOutdated?.() === true) && !loader.isError() && loader.isLoadable?.() !== false,
);
}
@@ -42,7 +42,7 @@ export function useAutoLoad(
}
for (const loader of state as ReadonlyArray<ILoadableState>) {
if (loader.isError() || (loader.lazy === true && !lazy)) {
if (loader.isError() || (loader.lazy === true && !lazy) || loader.isLoadable?.() === false) {
continue;
}
+5 -1
View File
@@ -75,6 +75,10 @@ export abstract class FormPart<TPartState extends object, TFormState = any> impl
return false;
}
isLoadable(): boolean {
return !this.formState.savingPromise;
}
isLoaded(): boolean {
return this.loaded;
}
@@ -125,7 +129,7 @@ export abstract class FormPart<TPartState extends object, TFormState = any> impl
}
async load(): Promise<void> {
if (this.formState.savingPromise) {
if (!this.isLoadable()) {
return;
}
@@ -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.
@@ -18,6 +18,7 @@ export interface ILoadableState {
isOutdated?: () => boolean;
isCancelled?: () => boolean;
isLoadable?: () => boolean;
cancel?: () => void;
}