[CB-4362] Add trim to all inputs in forms (#3687)

* dbeaver/pro#4696 adds zod schema parsing for form parts

* adds validation error to context

* adds schema parsing for UserFormInfoPart & ConnectionFormOptionsPart

* spreads RecordKeySchema across zod validations

* fixes form editing for connection info part

* Revert "spreads RecordKeySchema across zod validations"

This reverts commit 0e6719b32f.

* removes connection form errors during saving when empty fields

* reverts connection form options part parsing

* removes trimming from teams & authRole

* build fix

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
This commit is contained in:
sergeyteleshev
2025-09-15 21:47:55 +08:00
committed by GitHub
co-authored by mr-anton-t
parent 1c57e670c0
commit e466462269
7 changed files with 61 additions and 70 deletions
+21 -3
View File
@@ -8,13 +8,14 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { executorHandlerFilter, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { isObjectsEqual } from '@cloudbeaver/core-utils';
import { isObjectsEqual, schema } from '@cloudbeaver/core-utils';
import type { IFormPart } from './IFormPart.js';
import type { IFormState } from './IFormState.js';
import { formSubmitContext } from './formSubmitContext.js';
import { formValidationContext } from './formValidationContext.js';
export abstract class FormPart<TPartState, TFormState = any> implements IFormPart<TPartState> {
export abstract class FormPart<TPartState extends object, TFormState = any> implements IFormPart<TPartState> {
state: TPartState;
initialState: TPartState;
isSaving: boolean;
@@ -24,10 +25,12 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
protected loaded: boolean;
protected loading: boolean;
readonly schema: schema.ZodType<TPartState> | null;
constructor(
protected readonly formState: IFormState<TFormState>,
initialState: TPartState,
schema: schema.ZodType<TPartState> | null = null,
) {
this.initialState = initialState;
this.state = toJS(this.initialState);
@@ -38,14 +41,16 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
this.loaded = false;
this.loading = false;
this.schema = schema;
this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this)));
this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded() && this.isChanged, this.format.bind(this)));
this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.handleValidation.bind(this)));
makeObservable<this, 'loaded' | 'loading' | 'setInitialState' | 'setState'>(this, {
makeObservable<this, 'loaded' | 'loading' | 'setInitialState' | 'setState' | 'schema'>(this, {
initialState: observable,
state: observable,
schema: observable.ref,
exception: observable.ref,
promise: observable.ref,
isSaving: observable.ref,
@@ -150,8 +155,21 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
}
private async handleValidation(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): Promise<void> {
const validation = contexts.getContext(formValidationContext);
try {
if (this.schema) {
const parsedState = this.schema.parse(toJS(this.state));
this.setState(observable(parsedState));
}
} catch (e: any) {
validation.error(schema.prettifyError(e));
return;
}
try {
this.exception = null;
await this.validate(data, contexts);
} catch (exception: any) {
this.exception = exception;
@@ -9,9 +9,9 @@ import { schema } from '@cloudbeaver/core-utils';
const ServerConfigurationFormPartStateConfigSchema = schema.object({
adminCredentialsSaveEnabled: schema.boolean().optional(),
adminName: schema.string().optional(),
adminPassword: schema.string().optional(),
adminPasswordRepeat: schema.string().optional(),
adminName: schema.string().trim().optional(),
adminPassword: schema.string().trim().optional(),
adminPasswordRepeat: schema.string().trim().optional(),
anonymousAccessEnabled: schema.boolean().optional(),
authenticationEnabled: schema.boolean().optional(),
customConnectionsEnabled: schema.boolean().optional(),
@@ -21,8 +21,8 @@ const ServerConfigurationFormPartStateConfigSchema = schema.object({
publicCredentialsSaveEnabled: schema.boolean().optional(),
resourceManagerEnabled: schema.boolean().optional(),
secretManagerEnabled: schema.boolean().optional(),
serverName: schema.string().optional(),
serverURL: schema.string().optional(),
serverName: schema.string().trim().optional(),
serverURL: schema.string().trim().optional(),
sessionExpireTime: schema.number().optional(),
forceHttps: schema.boolean().optional(),
supportedHosts: schema.string(),
@@ -39,10 +39,11 @@ const ServerConfigurationFormPartStateNavigatorSchema = schema.object({
showUtilityObjects: schema.boolean(),
});
export const ServerConfigStateSchema = schema.object({
serverConfig: ServerConfigurationFormPartStateConfigSchema,
navigatorConfig: ServerConfigurationFormPartStateNavigatorSchema,
});
export type IServerConfig = schema.infer<typeof ServerConfigurationFormPartStateConfigSchema>;
export type INavigatorConfig = schema.infer<typeof ServerConfigurationFormPartStateNavigatorSchema>;
export type IServerConfigurationFormPartState = {
serverConfig: IServerConfig;
navigatorConfig: INavigatorConfig;
};
export type IServerConfigurationFormPartState = schema.infer<typeof ServerConfigStateSchema>;
@@ -16,7 +16,7 @@ import { isIp, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { MIN_SESSION_EXPIRE_TIME } from './Form/MIN_SESSION_EXPIRE_TIME.js';
import type { IServerConfigurationFormPartState } from './IServerConfigurationFormPartState.js';
import { ServerConfigStateSchema, type IServerConfigurationFormPartState } from './IServerConfigurationFormPartState.js';
function DEFAULT_STATE_GETTER(): IServerConfigurationFormPartState {
return {
@@ -54,7 +54,7 @@ export class ServerConfigurationFormPart extends FormPart<IServerConfigurationFo
private readonly passwordPolicyService: PasswordPolicyService,
private readonly localizationService: LocalizationService,
) {
super(formState, DEFAULT_STATE_GETTER());
super(formState, DEFAULT_STATE_GETTER(), ServerConfigStateSchema);
}
override isOutdated(): boolean {
@@ -99,24 +99,6 @@ export class ServerConfigurationFormPart extends FormPart<IServerConfigurationFo
}
}
protected override format() {
if (this.state.serverConfig.adminName) {
this.state.serverConfig.adminName = this.state.serverConfig.adminName.trim();
}
if (this.state.serverConfig.adminPassword) {
this.state.serverConfig.adminPassword = this.state.serverConfig.adminPassword.trim();
}
if (this.state.serverConfig.serverName) {
this.state.serverConfig.serverName = this.state.serverConfig.serverName.trim();
}
if (this.state.serverConfig.serverURL) {
this.state.serverConfig.serverURL = this.state.serverConfig.serverURL.trim();
}
}
override get isChanged(): boolean {
if (this.loaded && this.administrationScreenService.isConfigurationMode) {
return true;
@@ -1,17 +1,20 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IUserFormInfoState {
userId: string;
enabled: boolean;
password: string;
metaParameters: Record<string, any>;
teams: string[];
import { schema } from '@cloudbeaver/core-utils';
authRole: string; // used in TE product
}
export const USER_FORM_INFO_PART_SCHEMA = schema.object({
userId: schema.string().trim(),
enabled: schema.boolean(),
password: schema.string().trim(),
metaParameters: schema.record(schema.string(), schema.string().trim().or(schema.any())),
teams: schema.array(schema.string()),
authRole: schema.string(),
});
export type IUserFormInfoState = schema.infer<typeof USER_FORM_INFO_PART_SCHEMA>;
@@ -15,7 +15,7 @@ import { isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-
import { isDefined } from '@dbeaver/js-helpers';
import type { IUserFormState } from '../AdministrationUserFormService.js';
import type { IUserFormInfoState } from './IUserFormInfoState.js';
import { USER_FORM_INFO_PART_SCHEMA, type IUserFormInfoState } from './IUserFormInfoState.js';
const DEFAULT_ENABLED = true;
@@ -27,35 +27,22 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
private readonly usersResource: UsersResource,
private readonly usersMetaParametersResource: UsersMetaParametersResource,
) {
super(formState, {
userId: formState.state.userId || '',
enabled: DEFAULT_ENABLED,
password: '',
metaParameters: {},
teams: [],
authRole: '',
});
super(
formState,
{
userId: formState.state.userId || '',
enabled: DEFAULT_ENABLED,
password: '',
metaParameters: {},
teams: [],
authRole: '',
},
USER_FORM_INFO_PART_SCHEMA,
);
this.disableUser = this.disableUser.bind(this);
}
protected override format(data: IFormState<IUserFormState>, contexts: IExecutionContextProvider<IFormState<IUserFormState>>): void | Promise<void> {
this.state.password = this.state.password.trim();
const metaParameters = this.state.metaParameters;
if (this.formState.mode === FormMode.Create) {
this.state.userId = this.state.userId.trim();
}
for (const key in metaParameters) {
const value = metaParameters[key];
if (typeof value === 'string') {
metaParameters[key] = value.trim();
}
}
}
override isOutdated(): boolean {
if (this.formState.mode === FormMode.Edit && this.initialState.userId) {
return this.usersResource.isOutdated(this.initialState.userId) || this.usersMetaParametersResource.isOutdated(this.initialState.userId);
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.