Cb 5562 Fix bad this.setState for the new form API (#2903)

* CB-5478 adds useAdministrationUserFormState with flexible id field

* CB-5478 simplifies logic fork form state hooks

* CB-5562 refactors form state and form parts

* CB-5562 fixes disabled param for formState

* CB-5562 comments cleanup

* CB-5562 fixes getting of exceptions

* CB-5562 baseform bad set state fix

* CB-5562 removes load and configure method for forms

* CB-5562 reverts some state.exception changes

* CB-5562 removes bad set state from user form

* CB-5562 cleanup

* CB-5562 cleanup

* CB-5562 removes bad set state from user form

* CB-5562 fixes bad set state for user profile

* CB-5562 pr fixes

* CB-5562 pr fixes

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
sergeyteleshev
2024-09-17 20:14:34 +03:00
committed by GitHub
co-authored by Daria Marutkina
parent 4f0db5b854
commit 2d1bde22a2
11 changed files with 68 additions and 130 deletions
@@ -7,7 +7,7 @@
*/
import { observer } from 'mobx-react-lite';
import { Button, Container, Form, s, StatusMessage, useAutoLoad, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { Button, Container, Form, getComputed, s, StatusMessage, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { getFirstException } from '@cloudbeaver/core-utils';
import { TabList } from '../../Tabs/TabList';
@@ -22,7 +22,8 @@ export const BaseForm = observer<IBaseFormProps<any>>(function BaseForm({ servic
const translate = useTranslate();
const editing = state.mode === FormMode.Edit;
const changed = state.isChanged();
const changed = state.isChanged;
const error = getComputed(() => getFirstException(state.exception));
const form = useForm({
async onSubmit() {
@@ -36,15 +37,13 @@ export const BaseForm = observer<IBaseFormProps<any>>(function BaseForm({ servic
},
});
useAutoLoad(BaseForm, state);
return (
<Form context={form} disabled={state.isDisabled} contents focusFirstChild>
<TabsState container={service.parts} localState={state.parts} formState={state}>
<Container compact parent noWrap vertical>
<Container className={s(styles, { bar: true })} gap keepSize noWrap>
<Container fill>
<StatusMessage exception={getFirstException(state.exception)} type={state.statusType} message={state.statusMessage} />
<StatusMessage exception={error} type={state.statusType} message={state.statusMessage} />
<TabList className={s(styles, { tabList: true })} underline big />
</Container>
<Container keepSize noWrap center gap compact>
+16 -6
View File
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action, makeObservable, observable, toJS } from 'mobx';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { executorHandlerFilter, ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { isObjectsEqual } from '@cloudbeaver/core-utils';
@@ -16,6 +16,7 @@ import type { IFormState } from './IFormState';
export abstract class FormPart<TPartState, TFormState = any> implements IFormPart<TPartState> {
state: TPartState;
initialState: TPartState;
isSaving: boolean;
exception: Error | null;
promise: Promise<any> | null;
@@ -29,6 +30,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
) {
this.initialState = initialState;
this.state = toJS(this.initialState);
this.isSaving = false;
this.exception = null;
this.promise = null;
@@ -37,7 +39,6 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
this.loading = false;
this.formState.submitTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.save.bind(this)));
this.formState.configureTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.configure.bind(this)));
this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.format.bind(this)));
this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.validate.bind(this)));
@@ -46,12 +47,19 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
state: observable,
exception: observable.ref,
promise: observable.ref,
isSaving: observable.ref,
loaded: observable,
loading: observable,
setInitialState: action,
isDisabled: computed,
isChanged: computed,
});
}
get isDisabled(): boolean {
return this.isSaving || this.isLoading();
}
isLoading(): boolean {
return this.loading;
}
@@ -68,7 +76,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
return this.exception !== null;
}
isChanged(): boolean {
get isChanged(): boolean {
if (!this.loaded || this.initialState === this.state) {
return false;
}
@@ -85,10 +93,12 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
try {
await this.loader();
if (!this.isChanged()) {
if (!this.isChanged) {
return;
}
this.isSaving = true;
await this.saveChanges(data, contexts);
if (ExecutorInterrupter.isInterrupted(contexts)) {
return;
@@ -100,6 +110,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
this.exception = exception;
throw exception;
} finally {
this.isSaving = false;
this.loading = false;
}
}
@@ -136,7 +147,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
protected setInitialState(initialState: TPartState) {
this.initialState = initialState;
if (this.isChanged()) {
if (this.isChanged) {
return;
}
@@ -147,7 +158,6 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
this.state = state;
}
protected configure(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
protected format(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
protected validate(data: IFormState<TFormState>, contexts: IExecutionContextProvider<IFormState<TFormState>>): void | Promise<void> {}
+29 -86
View File
@@ -11,7 +11,7 @@ import { DataContext, dataContextAddDIProvider, DataContextGetter, type IDataCon
import type { IServiceProvider } from '@cloudbeaver/core-di';
import type { ENotificationType } from '@cloudbeaver/core-events';
import { Executor, ExecutorInterrupter, IExecutionContextProvider, type IExecutor } from '@cloudbeaver/core-executor';
import { isLoadableStateHasException, MetadataMap, uuid } from '@cloudbeaver/core-utils';
import { isArraysEqual, isNotNullDefined, MetadataMap, uuid } from '@cloudbeaver/core-utils';
import { DATA_CONTEXT_LOADABLE_STATE, loadableStateContext } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_FORM_STATE } from './DATA_CONTEXT_FORM_STATE';
@@ -25,23 +25,24 @@ export class FormState<TState> implements IFormState<TState> {
mode: FormMode;
parts: MetadataMap<string, IFormPart<any>>;
state: TState;
isSaving: boolean;
statusMessage: string | string[] | null;
statusType: ENotificationType | null;
exception: Error | (Error | null)[] | null;
promise: Promise<any> | null;
get isDisabled(): boolean {
return this.isSaving || this.isLoading();
return this.partsValues.some(part => part.isSaving || part?.isLoading?.());
}
get isSaving(): boolean {
return this.partsValues.some(part => part.isSaving);
}
readonly id: string;
readonly service: FormBaseService<TState, any>;
readonly dataContext: IDataContext;
readonly configureTask: IExecutor<IFormState<TState>>;
readonly formStateTask: IExecutor<TState>;
readonly fillDefaultConfigTask: IExecutor<IFormState<TState>>;
readonly submitTask: IExecutor<IFormState<TState>>;
@@ -56,17 +57,12 @@ export class FormState<TState> implements IFormState<TState> {
this.mode = FormMode.Create;
this.parts = new MetadataMap<string, any>();
this.state = state;
this.isSaving = false;
this.statusMessage = null;
this.statusType = null;
this.exception = null;
this.promise = null;
this.configureTask = new Executor(this as IFormState<TState>, () => true);
this.configureTask.addCollection(service.onConfigure);
this.formStateTask = new Executor<TState>(state, () => true);
this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this));
@@ -90,42 +86,43 @@ export class FormState<TState> implements IFormState<TState> {
mode: observable,
parts: observable.ref,
promise: observable.ref,
exception: observable.ref,
isSaving: observable.ref,
state: observable,
isSaving: computed,
exception: computed,
isDisabled: computed,
setMode: action,
setPartsState: action,
setException: action,
setState: action,
isChanged: computed,
partsValues: computed<IFormPart<any>[]>({
equals: isArraysEqual,
}),
isError: computed,
isCancelled: computed,
});
}
isLoading(): boolean {
return this.promise !== null || this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isLoading());
get partsValues() {
return Array.from(this.parts.values());
}
isLoaded(): boolean {
if (this.promise) {
return false;
}
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.every(loader => loader.isLoaded());
get exception(): Error | (Error | null)[] | null {
return this.partsValues
.map(part => part?.exception)
.flat()
.filter(isNotNullDefined);
}
isError(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isError());
get isError(): boolean {
return this.partsValues.some(part => part.isError());
}
isOutdated(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isOutdated?.() === true);
get isCancelled(): boolean {
return this.partsValues.some(part => part?.isCancelled?.());
}
isCancelled(): boolean {
return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isCancelled?.() === true);
}
isChanged(): boolean {
return Array.from(this.parts.values()).some(part => part.isChanged());
get isChanged(): boolean {
return this.partsValues.some(part => part.isChanged);
}
getPart<T extends IFormPart<any>>(getter: DataContextGetter<T>, init: (context: IDataContext, id: string) => T): T {
@@ -140,50 +137,6 @@ export class FormState<TState> implements IFormState<TState> {
}) as T;
}
async load(refresh?: boolean): Promise<void> {
if (this.promise !== null) {
return this.promise;
}
if (this.isLoaded() && !this.isOutdated() && !refresh) {
return;
}
this.promise = (async () => {
try {
await this.configureTask.execute(this);
const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders;
for (const loader of loaders) {
if (isLoadableStateHasException(loader)) {
continue;
}
if (!loader.isLoaded() || loader.isOutdated?.() === true) {
try {
await loader.load();
} catch {
return;
}
}
}
await this.fillDefaultConfigTask.execute(this);
this.exception = null;
} catch (exception: any) {
this.exception = exception;
throw exception;
} finally {
this.promise = null;
}
})();
}
async reload(): Promise<void> {
await this.load(true);
}
cancel(): void {
const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders;
@@ -210,11 +163,6 @@ export class FormState<TState> implements IFormState<TState> {
return this;
}
setException(exception: Error | (Error | null)[] | null): this {
this.exception = exception;
return this;
}
setState(state: TState): this {
this.state = state;
return this;
@@ -222,20 +170,15 @@ export class FormState<TState> implements IFormState<TState> {
async save(): Promise<boolean> {
try {
this.isSaving = true;
const context = await this.submitTask.execute(this);
if (ExecutorInterrupter.isInterrupted(context)) {
return false;
}
this.exception = null;
return true;
} catch (exception: any) {
this.exception = exception;
} finally {
this.isSaving = false;
}
} catch (exception: any) {}
return false;
}
@@ -10,8 +10,10 @@ import type { ILoadableState } from '@cloudbeaver/core-utils';
export interface IFormPart<TState> extends ILoadableState {
readonly state: TState;
readonly initialState: TState;
isSaving: boolean;
readonly isDisabled: boolean;
isChanged(): boolean;
readonly isChanged: boolean;
load(): Promise<void>;
reset(): void;
+6 -13
View File
@@ -8,13 +8,13 @@
import type { DataContextGetter, IDataContext } from '@cloudbeaver/core-data-context';
import type { ENotificationType } from '@cloudbeaver/core-events';
import type { IExecutor } from '@cloudbeaver/core-executor';
import type { ILoadableState, MetadataMap } from '@cloudbeaver/core-utils';
import type { MetadataMap } from '@cloudbeaver/core-utils';
import type { FormBaseService } from './FormBaseService';
import type { FormMode } from './FormMode';
import type { IFormPart } from './IFormPart';
export interface IFormState<TState> extends ILoadableState {
export interface IFormState<TState> {
readonly id: string;
readonly service: FormBaseService<TState>;
readonly dataContext: IDataContext;
@@ -23,14 +23,13 @@ export interface IFormState<TState> extends ILoadableState {
readonly parts: MetadataMap<string, any>;
readonly state: TState;
readonly isDisabled: boolean;
readonly exception: Error | (Error | null)[] | null;
readonly promise: Promise<any> | null;
readonly exception: Error | (Error | null)[] | null;
readonly statusMessage: string | string[] | null;
readonly statusType: ENotificationType | null;
readonly configureTask: IExecutor<IFormState<TState>>;
readonly formStateTask: IExecutor<TState>;
readonly fillDefaultConfigTask: IExecutor<IFormState<TState>>;
readonly submitTask: IExecutor<IFormState<TState>>;
@@ -39,20 +38,14 @@ export interface IFormState<TState> extends ILoadableState {
setMode(mode: FormMode): this;
setPartsState(state: MetadataMap<string, any>): this;
setException(exception: Error | (Error | null)[] | null): this;
setState(state: TState): this;
getPart<T extends IFormPart<any>>(getter: DataContextGetter<T>, init: (context: IDataContext, id: string) => T): T;
isLoading(): boolean;
isLoaded(): boolean;
isError(): boolean;
isOutdated(): boolean;
isCancelled(): boolean;
isChanged(): boolean;
isError: boolean;
isCancelled: boolean;
isChanged: boolean;
load(): Promise<void>;
reload(): Promise<void>;
save(): Promise<boolean>;
reset(): void;
cancel(): void;
@@ -59,7 +59,7 @@ export const AdministrationUserForm = observer<Props>(function AdministrationUse
},
});
useAutoLoad(AdministrationUserForm, state);
useAutoLoad(AdministrationUserForm, [userFormInfoPart]);
return (
<Form context={form} disabled={state.isDisabled} contents focusFirstChild>
@@ -69,7 +69,7 @@ export const AdministrationUserForm = observer<Props>(function AdministrationUse
<Container fill>
<StatusMessage
className={s(styles, { statusMessage: true })}
exception={getFirstException(state.exception)}
exception={getFirstException(userFormInfoPart.exception)}
type={state.statusType}
message={state.statusMessage}
/>
@@ -24,7 +24,7 @@ export class UserFormConnectionAccessPart extends FormPart<AdminConnectionGrantI
super(formState, []);
}
isChanged(): boolean {
get isChanged(): boolean {
if (!this.loaded) {
return false;
}
@@ -9,12 +9,11 @@ import { observable, toJS } from 'mobx';
import type { AdminUser, AuthRolesResource, UserResourceIncludes, UsersResource } from '@cloudbeaver/core-authentication';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { CachedResourceIncludeArgs, getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource';
import { CachedResourceIncludeArgs } from '@cloudbeaver/core-resource';
import type { ServerConfigResource } from '@cloudbeaver/core-root';
import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk';
import { FormMode, FormPart, formValidationContext, IFormState } from '@cloudbeaver/core-ui';
import { isArraysEqual, isDefined, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
import { DATA_CONTEXT_LOADABLE_STATE } from '@cloudbeaver/core-view';
import type { IUserFormState } from '../AdministrationUserFormService';
import type { IUserFormInfoState } from './IUserFormInfoState';
@@ -62,7 +61,7 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
return this.usersResource.isOutdated(this.initialState.userId, this.baseIncludes);
}
return false;
return this.serverConfigResource.isOutdated() || this.authRolesResource.isOutdated();
}
isLoaded(): boolean {
@@ -77,7 +76,7 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
return this.loaded;
}
isChanged(): boolean {
get isChanged(): boolean {
if (!this.loaded) {
return false;
}
@@ -138,14 +137,6 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
}
}
}
protected override configure() {
const loadableStateContext = this.formState.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!;
loadableStateContext.getState('user-info', () => [
getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined),
getCachedDataResourceLoaderState(this.authRolesResource, () => undefined),
]);
}
private async updateCredentials() {
const password = this.state.password;
@@ -220,7 +211,7 @@ export class UserFormInfoPart extends FormPart<IUserFormInfoState, IUserFormStat
protected override async loader() {
let user: AdminUser | null = null;
const serverConfig = await this.serverConfigResource.load();
const [serverConfig] = await Promise.all([this.serverConfigResource.load(), this.authRolesResource.load()]);
if (this.formState.mode === FormMode.Edit && this.initialState.userId) {
user = await this.usersResource.load(this.initialState.userId, this.baseIncludes);
@@ -44,7 +44,7 @@ export class UserProfileFormAuthenticationPart extends FormPart<IUserProfileForm
return this.loaded && this.userInfoResource.isLoaded(undefined, this.baseIncludes);
}
isChanged(): boolean {
get isChanged(): boolean {
if (!this.loaded) {
return false;
}
@@ -44,7 +44,7 @@ export class UserProfileFormInfoPart extends FormPart<IUserProfileFormInfoState,
return this.loaded && this.userInfoResource.isLoaded(undefined, this.baseIncludes);
}
isChanged(): boolean {
get isChanged(): boolean {
if (!this.loaded) {
return false;
}
@@ -39,7 +39,7 @@ export const UserProfileFormPanel: TabContainerPanelComponent = observer(functio
async function closeHandler(_, contexts) {
const context = contexts.getContext(userProfileContext);
if (state.isChanged() && !context.force) {
if (state.isChanged && !context.force) {
const result = await commonDialogService.open(ConfirmationDialog, {
title: 'connections_public_connection_edit_cancel_title',
message: 'connections_public_connection_edit_cancel_message',