diff --git a/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts b/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts index c647e372f1..799222836b 100644 --- a/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts +++ b/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts @@ -1,22 +1,22 @@ /* * 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. */ -import { useContext, useEffect, useRef } from 'react'; - -import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; - +import { useEffect, useRef } from 'react'; import { useTranslate } from '../localization/useTranslate.js'; -import { useExecutor } from '../useExecutor.js'; -import { FormContext } from './FormContext.js'; + +export interface ICustomInputValidation { + ref: React.RefObject; + revalidate: () => void; + revalidateAndReport: (element: TType) => boolean; +} export function useCustomInputValidation( validation: (value: T) => string | null, -): React.RefObject { - const context = useContext(FormContext); +): ICustomInputValidation { const inputRef = useRef(null); const translate = useTranslate(); @@ -27,37 +27,32 @@ export function useCustomInputValidation { const element = inputRef.current; @@ -68,14 +63,14 @@ export function useCustomInputValidation( + validation: (value: T) => string | null, + formContext?: IFormContext, +): ICustomInputValidation { + const customValidation = useCustomInputValidation(validation); + const reactContext = useContext(FormContext); + const context = formContext ?? reactContext; + + if (!context) { + throw new Error('useCustomInputValidation must be used within a FormContext provider'); + } + + useExecutor({ + executor: context?.onValidate, + handlers: [ + function validationHandler(_, context) { + if (!customValidation.ref.current) { + return; + } + + if (!customValidation.revalidateAndReport(customValidation.ref.current)) { + ExecutorInterrupter.interrupt(context); + } + }, + ], + }); + + return customValidation; +} diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 7fcad9c718..16bd02165e 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -179,6 +179,7 @@ export * from './FormControls/Form.js'; export * from './FormControls/Textarea.js'; export * from './FormControls/useCapsLockTracker.js'; export * from './FormControls/useCustomInputValidation.js'; +export * from './FormControls/useFormCustomInputValidation.js'; export * from './FormControls/useForm.js'; export * from './FormControls/Textarea.js'; export * from './Link.js'; diff --git a/webapp/packages/core-blocks/src/usePasswordValidation.ts b/webapp/packages/core-blocks/src/usePasswordValidation.ts index 459f48ba38..684d5ddf07 100644 --- a/webapp/packages/core-blocks/src/usePasswordValidation.ts +++ b/webapp/packages/core-blocks/src/usePasswordValidation.ts @@ -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. @@ -9,21 +9,22 @@ import { PasswordPolicyService } from '@cloudbeaver/core-authentication'; import { useService } from '@cloudbeaver/core-di'; import { PasswordPolicyResource } from '@cloudbeaver/core-root'; -import { useCustomInputValidation } from './FormControls/useCustomInputValidation.js'; +import { useFormCustomInputValidation } from './FormControls/useFormCustomInputValidation.js'; import { useResource } from './ResourcesHooks/useResource.js'; +import type { IFormContext } from './FormControls/FormContext.js'; -export function usePasswordValidation() { +export function usePasswordValidation(formContext?: IFormContext) { useResource(usePasswordValidation, PasswordPolicyResource, undefined); const passwordPolicyService = useService(PasswordPolicyService); - const ref = useCustomInputValidation(value => { + const { ref } = useFormCustomInputValidation(value => { if (!value) { return null; } const validation = passwordPolicyService.validatePassword(value); return validation.isValid ? null : validation.errorMessage; - }); + }, formContext); return ref; } diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx index 00946edce5..f1ed1ea7a2 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.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. @@ -17,7 +17,7 @@ import { Textarea, useResource, useTranslate, - useCustomInputValidation, + useFormCustomInputValidation, } from '@cloudbeaver/core-blocks'; import { ServerConfigResource } from '@cloudbeaver/core-root'; @@ -33,7 +33,7 @@ interface Props { export const ServerConfigurationInfoForm = observer(function ServerConfigurationInfoForm({ state }) { const serverConfigLoader = useResource(ServerConfigurationInfoForm, ServerConfigResource, undefined); const translate = useTranslate(); - const validation = useCustomInputValidation(value => { + const { ref: validation } = useFormCustomInputValidation(value => { const currentHost = window.location.host; if (!isIp(window.location.hostname) && value.trim() && !value.includes(currentHost)) { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.tsx index ac7c1702fd..c375411dee 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.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,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { ADMIN_USERNAME_MIN_LENGTH } from '@cloudbeaver/core-authentication'; -import { Group, GroupTitle, InputField, useCustomInputValidation, usePasswordValidation, useTranslate } from '@cloudbeaver/core-blocks'; +import { Group, GroupTitle, InputField, useFormCustomInputValidation, usePasswordValidation, useTranslate } from '@cloudbeaver/core-blocks'; import { isValuesEqual } from '@cloudbeaver/core-utils'; import type { IServerConfig } from '@cloudbeaver/plugin-administration'; @@ -20,7 +20,7 @@ export const ServerConfigurationAdminForm = observer(function ServerConfi const translate = useTranslate(); const passwordValidationRef = usePasswordValidation(); - const passwordRepeatRef = useCustomInputValidation(value => { + const { ref: passwordRepeatRef } = useFormCustomInputValidation(value => { if (!isValuesEqual(value, serverConfig.adminPassword, null)) { return translate('authentication_user_passwords_not_match'); } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx index 58569b862c..c2490a589e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsForm/Options/TeamOptions.tsx @@ -1,13 +1,22 @@ /* * 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. */ import { observer } from 'mobx-react-lite'; -import { Container, Group, InputField, Textarea, useAutoLoad, useCustomInputValidation, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { + Container, + Group, + InputField, + Textarea, + useAutoLoad, + useFormCustomInputValidation, + useResource, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { ServerConfigResource } from '@cloudbeaver/core-root'; import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; @@ -27,7 +36,7 @@ export const TeamOptions: TabContainerPanelComponent = observer(f useAutoLoad(TeamOptions, tabState, tab.selected && !loaded); - const idValidationRef = useCustomInputValidation(value => { + const { ref: idValidationRef } = useFormCustomInputValidation(value => { const v = value.trim(); if (!v) { diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx index a2eedaf028..a8bfb12a41 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.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. @@ -12,7 +12,7 @@ import { Container, GroupTitle, InputField, - useCustomInputValidation, + useFormCustomInputValidation, usePasswordValidation, useResource, useTranslate, @@ -45,7 +45,7 @@ export const UserFormInfoCredentials = observer(function UserFormInfoCred local = !editing || (!!userInfo.data && isLocalUser(userInfo.data)); } - const usernameValidationRef = useCustomInputValidation(value => { + const { ref: usernameValidationRef } = useFormCustomInputValidation(value => { const v = value.trim(); if (!v) { @@ -59,7 +59,7 @@ export const UserFormInfoCredentials = observer(function UserFormInfoCred return null; }); - const passwordRepeatRef = useCustomInputValidation(value => { + const { ref: passwordRepeatRef } = useFormCustomInputValidation(value => { if (!isValuesEqual(value, tabState.state.password, null)) { return translate('authentication_user_passwords_not_match'); } diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingField.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingField.tsx index d81ba6aed6..e5a7a7ac10 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingField.tsx +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingField.tsx @@ -1,12 +1,12 @@ /* * 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. */ -import { Select, FieldCheckbox, InputField, Textarea, useCustomInputValidation, useTranslate } from '@cloudbeaver/core-blocks'; +import { Select, FieldCheckbox, InputField, Textarea, useTranslate, useFormCustomInputValidation } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { ESettingsValueType, @@ -55,7 +55,7 @@ export const SettingField = observer(function SettingField({ resolver, se value = value ?? ''; - const customValidation = useCustomInputValidation(value => { + const { ref: customValidation } = useFormCustomInputValidation(value => { if (!(setting.key in settingsProviderService.schema.shape)) { return null; } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx index da26217d13..de2fa57b66 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.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. @@ -19,9 +19,9 @@ import { InputField, ToolsAction, ToolsPanel, - useCustomInputValidation, useExecutor, useForm, + useFormCustomInputValidation, useObservableRef, usePasswordValidation, useTranslate, @@ -54,13 +54,6 @@ export const ChangePassword = observer(function ChangePassword() { const userInfoResource = useService(UserInfoResource); const commonDialogService = useService(CommonDialogService); const disabled = userInfoResource.isLoading(); - const passwordValidationRef = usePasswordValidation(); - const passwordRepeatRef = useCustomInputValidation(value => { - if (!isValuesEqual(value, state.password, null)) { - return translate('authentication_user_passwords_not_match'); - } - return null; - }); const form = useForm({ async onSubmit() { @@ -75,6 +68,13 @@ export const ChangePassword = observer(function ChangePassword() { } }, }); + const passwordValidationRef = usePasswordValidation(form); + const passwordRepeatValidation = useFormCustomInputValidation(value => { + if (!isValuesEqual(value, state.password, null)) { + return translate('authentication_user_passwords_not_match'); + } + return null; + }, form); function resetForm() { state.oldPassword = ''; @@ -143,11 +143,12 @@ export const ChangePassword = observer(function ChangePassword() { mapValue={(value?: string) => value?.trim() ?? ''} small required + onChange={passwordRepeatValidation.revalidate} > {translate('plugin_user_profile_authentication_change_password_new_password')}