mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-24 11:35:54 +08:00
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:
co-authored by
Evgenia
mr-anton-t
parent
457909dd82
commit
801139e3ff
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user