diff --git a/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx b/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx index e9de4a144b..96f18cc7c6 100644 --- a/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx +++ b/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx @@ -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>(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>(function BaseForm({ servic }, }); - useAutoLoad(BaseForm, state); - return (
- + diff --git a/webapp/packages/core-ui/src/Form/FormPart.ts b/webapp/packages/core-ui/src/Form/FormPart.ts index 2dcf2063d7..c789e0df64 100644 --- a/webapp/packages/core-ui/src/Form/FormPart.ts +++ b/webapp/packages/core-ui/src/Form/FormPart.ts @@ -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 implements IFormPart { state: TPartState; initialState: TPartState; + isSaving: boolean; exception: Error | null; promise: Promise | null; @@ -29,6 +30,7 @@ export abstract class FormPart 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 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 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 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 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 implements IFormPar this.exception = exception; throw exception; } finally { + this.isSaving = false; this.loading = false; } } @@ -136,7 +147,7 @@ export abstract class FormPart implements IFormPar protected setInitialState(initialState: TPartState) { this.initialState = initialState; - if (this.isChanged()) { + if (this.isChanged) { return; } @@ -147,7 +158,6 @@ export abstract class FormPart implements IFormPar this.state = state; } - protected configure(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} protected format(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} protected validate(data: IFormState, contexts: IExecutionContextProvider>): void | Promise {} diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index 2b651eda81..93bd21daa2 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -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 implements IFormState { mode: FormMode; parts: MetadataMap>; state: TState; - isSaving: boolean; statusMessage: string | string[] | null; statusType: ENotificationType | null; - exception: Error | (Error | null)[] | null; promise: Promise | 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; readonly dataContext: IDataContext; - readonly configureTask: IExecutor>; readonly formStateTask: IExecutor; readonly fillDefaultConfigTask: IExecutor>; readonly submitTask: IExecutor>; @@ -56,17 +57,12 @@ export class FormState implements IFormState { this.mode = FormMode.Create; this.parts = new MetadataMap(); 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, () => true); - this.configureTask.addCollection(service.onConfigure); - this.formStateTask = new Executor(state, () => true); this.formStateTask.addCollection(service.onState).addPostHandler(this.updateFormState.bind(this)); @@ -90,42 +86,43 @@ export class FormState implements IFormState { 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[]>({ + equals: isArraysEqual, + }), + isError: computed, + isCancelled: computed, }); } - isLoading(): boolean { - return this.promise !== null || this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isLoading()); - } - - isLoaded(): boolean { - if (this.promise) { - return false; - } - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.every(loader => loader.isLoaded()); + get partsValues() { + return Array.from(this.parts.values()); } - isError(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isError()); + get exception(): Error | (Error | null)[] | null { + return this.partsValues + .map(part => part?.exception) + .flat() + .filter(isNotNullDefined); } - isOutdated(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isOutdated?.() === true); + get isError(): boolean { + return this.partsValues.some(part => part.isError()); } - isCancelled(): boolean { - return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders.some(loader => loader.isCancelled?.() === true); + get isCancelled(): boolean { + return this.partsValues.some(part => part?.isCancelled?.()); } - isChanged(): boolean { - return Array.from(this.parts.values()).some(part => part.isChanged()); + get isChanged(): boolean { + return this.partsValues.some(part => part.isChanged); } getPart>(getter: DataContextGetter, init: (context: IDataContext, id: string) => T): T { @@ -140,50 +137,6 @@ export class FormState implements IFormState { }) as T; } - async load(refresh?: boolean): Promise { - 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 { - await this.load(true); - } - cancel(): void { const loaders = this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE)!.loaders; @@ -210,11 +163,6 @@ export class FormState implements IFormState { 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 implements IFormState { async save(): Promise { 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; } diff --git a/webapp/packages/core-ui/src/Form/IFormPart.ts b/webapp/packages/core-ui/src/Form/IFormPart.ts index faa9e63117..446c33fd97 100644 --- a/webapp/packages/core-ui/src/Form/IFormPart.ts +++ b/webapp/packages/core-ui/src/Form/IFormPart.ts @@ -10,8 +10,10 @@ import type { ILoadableState } from '@cloudbeaver/core-utils'; export interface IFormPart extends ILoadableState { readonly state: TState; readonly initialState: TState; + isSaving: boolean; + readonly isDisabled: boolean; - isChanged(): boolean; + readonly isChanged: boolean; load(): Promise; reset(): void; diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 67b2029e1f..050332649b 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -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 extends ILoadableState { +export interface IFormState { readonly id: string; readonly service: FormBaseService; readonly dataContext: IDataContext; @@ -23,14 +23,13 @@ export interface IFormState extends ILoadableState { readonly parts: MetadataMap; readonly state: TState; readonly isDisabled: boolean; + readonly exception: Error | (Error | null)[] | null; readonly promise: Promise | null; - readonly exception: Error | (Error | null)[] | null; readonly statusMessage: string | string[] | null; readonly statusType: ENotificationType | null; - readonly configureTask: IExecutor>; readonly formStateTask: IExecutor; readonly fillDefaultConfigTask: IExecutor>; readonly submitTask: IExecutor>; @@ -39,20 +38,14 @@ export interface IFormState extends ILoadableState { setMode(mode: FormMode): this; setPartsState(state: MetadataMap): this; - setException(exception: Error | (Error | null)[] | null): this; setState(state: TState): this; getPart>(getter: DataContextGetter, 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; - reload(): Promise; save(): Promise; reset(): void; cancel(): void; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx index 5c006048a7..99bd4c784e 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/AdministrationUserForm.tsx @@ -59,7 +59,7 @@ export const AdministrationUserForm = observer(function AdministrationUse }, }); - useAutoLoad(AdministrationUserForm, state); + useAutoLoad(AdministrationUserForm, [userFormInfoPart]); return ( @@ -69,7 +69,7 @@ export const AdministrationUserForm = observer(function AdministrationUse diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts index 5e7d6ea83b..6cc4e3cf2a 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/ConnectionAccess/UserFormConnectionAccessPart.ts @@ -24,7 +24,7 @@ export class UserFormConnectionAccessPart extends FormPart [ - 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