diff --git a/webapp/packages/core-administration/package.json b/webapp/packages/core-administration/package.json index b91285872c..adf258a365 100644 --- a/webapp/packages/core-administration/package.json +++ b/webapp/packages/core-administration/package.json @@ -20,6 +20,7 @@ "@cloudbeaver/core-root": "^0.1.0", "@cloudbeaver/core-routing": "^0.1.0", "@cloudbeaver/core-theming": "^0.1.0", + "@cloudbeaver/core-executor": "^0.1.0", "react": "^16.x.x", "mobx": "^5.x.x", "mobx-react": "^6.x.x", diff --git a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts index 2e7b9b2ea2..735c10e856 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -54,7 +54,7 @@ export interface IAdministrationItemSubItem { export interface IConfigurationWizardItemOptions { description: string; - onValidate?: () => boolean; + onValidate?: () => Promise | boolean; isDisabled?: () => boolean; isHidden?: () => boolean; isDone?: () => boolean; diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts index a0292993c1..38daf0e41b 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts @@ -55,15 +55,6 @@ export class ConfigurationWizardService { }); } - @computed get canNext() { - if (this.currentStep?.configurationWizardOptions?.isDone - && !this.currentStep?.configurationWizardOptions?.isDone()) { - return false; - } - - return true; - } - @computed get nextStep() { return this.steps.find((item, index) => { if (index <= this.currentStepIndex) { @@ -112,27 +103,37 @@ export class ConfigurationWizardService { const step = this.getStep(name); if (!step) { - return; + return false; + } + + if (step.configurationWizardOptions?.onValidate) { + const isValid = await step.configurationWizardOptions.onValidate(); + if (!isValid) { + return false; + } } if (step.configurationWizardOptions?.onFinish) { await step.configurationWizardOptions.onFinish(); } + return true; } - next() { - if (!this.currentStep || !this.canNext) { + async next() { + if (!this.currentStep) { return; } - this.finishStep(this.currentStep.name); + if (!await this.finishStep(this.currentStep.name)) { + return; + } if (this.currentStepIndex + 1 < this.steps.length) { if (this.nextStep) { this.administrationScreenService.navigateToItem(this.nextStep.name); } } else { - this.finish(); + await this.finish(); } } diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationForm.tsx b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationForm.tsx index 0eadab70a8..d39f7d5086 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationForm.tsx +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationForm.tsx @@ -7,15 +7,19 @@ */ import { observer } from 'mobx-react'; +import { useEffect } from 'react'; import styled, { css } from 'reshadow'; import { SubmittingForm, InputGroup, InputField, useFocus, Switch } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ServerConfigInput } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; +import { ServerConfigurationService } from './ServerConfigurationService'; + export const formStyles = css` SubmittingForm { flex: 1; @@ -51,11 +55,22 @@ export const ServerConfigurationForm = observer(function ServerConfigurationForm onChange, onSave, }: Props) { + const service = useService(ServerConfigurationService); const translate = useTranslate(); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const [focusedRef] = useFocus({ focusFirstChild: true }); + + useEffect(() => { + const validate = () => { + focusedRef.current?.checkValidity(); + focusedRef.current?.reportValidity(); + }; + service.validationTask.addHandler(validate); + + return () => service.validationTask.removeHandler(validate); + }, [service]); return styled(useStyles(formStyles, boxStyles))( - }> + diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationService.ts index 7b3870a25f..ed5bf7bd55 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/Pages/ServerConfiguration/ServerConfigurationService.ts @@ -8,6 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { IExecutor, Executor, IContextProvider } from '@cloudbeaver/core-executor'; import { GraphQLService } from '@cloudbeaver/core-sdk'; import { AdministrationScreenService } from '../../../AdministrationScreenService'; @@ -17,6 +18,7 @@ import { IServerConfigurationPageState } from './IServerConfigurationPageState'; export class ServerConfigurationService { readonly state: IServerConfigurationPageState; + readonly validationTask: IExecutor; constructor( private administrationScreenService: AdministrationScreenService, @@ -42,18 +44,35 @@ export class ServerConfigurationService { showUtilityObjects: false, }, })); + this.validationTask = new Executor(); } isDone() { return this.isFormFilled(); } - validate() { - return this.isFormFilled(); + async validate() { + const context = await this.validationTask.execute(true); + const state = await context.getContext(this.validationStatusContext); + + return state.getState(); + } + + validationStatusContext = (context: IContextProvider) => { + let state = this.isFormFilled(); + + const invalidate = () => { + state = false; + }; + const getState = () => state; + + return { + getState, + invalidate, + }; } private isFormFilled() { - return !!( this.state?.serverConfig.serverName && this.state.serverConfig.adminName diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/WizardStepper.tsx b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/WizardStepper.tsx index c7675faeea..fc42b83bbb 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/WizardStepper.tsx +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/WizardStepper.tsx @@ -79,7 +79,6 @@ export const WizardStepper = observer(function WizardStepper() { type="button" mod={['unelevated']} onClick={() => service.next()} - disabled={!service.canNext} > {translate(service.currentStepIndex === service.steps.length - 1 ? 'ui_stepper_finish' : 'ui_stepper_next')} diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx index f545a710ba..5b6f44f5e9 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx @@ -47,7 +47,7 @@ const navigationTreeStyles = css` export const NavigationTree = observer(function NavigationTree() { const navTreeService = useService(NavigationTreeService); const [onFocus, onBlur] = useActiveView(navTreeService.getView); - const [ref] = useFocus({ onFocus, onBlur }); + const [ref] = useFocus({ onFocus, onBlur }); const nodeChildren = useChildren(); const isEnabled = usePermission(EPermission.public); @@ -69,7 +69,7 @@ export const NavigationTree = observer(function NavigationTree() { } return styled(navigationTreeStyles)( - }> + {nodeChildren.children.map(id => ( ))} diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx index 47eb312aa6..57da00ce7b 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx +++ b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx @@ -41,7 +41,7 @@ const stylesArray = [styles]; export const NavigationTabsBar = observer(function NavigationTabsBar() { const navigation = useService(NavigationTabsService); const [onFocus, onBlur] = useActiveView(navigation.getView); - const [ref] = useFocus({ onFocus, onBlur }); + const [ref] = useFocus({ onFocus, onBlur }); const handleSelect = useCallback((tabId: string) => navigation.selectTab(tabId), [navigation]); const handleClose = useCallback((tabId: string) => navigation.closeTab(tabId), [navigation]); @@ -62,7 +62,7 @@ export const NavigationTabsBar = observer(function NavigationTabsBar() { ))} style={stylesArray} tabIndex={0} - ref={ref as React.RefObject} + ref={ref} > {navigation.tabIdList.map(tabId => ( diff --git a/webapp/packages/core-blocks/src/useFocus.ts b/webapp/packages/core-blocks/src/useFocus.ts index fa078a81e0..47b0beaba1 100644 --- a/webapp/packages/core-blocks/src/useFocus.ts +++ b/webapp/packages/core-blocks/src/useFocus.ts @@ -16,18 +16,18 @@ type FocusOptions = { onBlur?: () => void; } -export function useFocus({ +export function useFocus({ focusFirstChild, onFocus, onBlur, -}: FocusOptions): [React.RefObject, boolean] { +}: FocusOptions): [React.RefObject, boolean] { const [focus, setFocus] = useState(false); - const reference = useRef(null); + const reference = useRef(null); useLayoutEffect(() => { if (reference.current !== null && focusFirstChild) { const firstFocusable = reference.current - .querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + .querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (firstFocusable) { firstFocusable.focus(); diff --git a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx index c0e6634d28..b85a2b3195 100644 --- a/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx +++ b/webapp/packages/core-connections/src/Administration/Connections/ConnectionsTable/ConnectionForm/ConnectionForm.tsx @@ -39,10 +39,10 @@ export const ConnectionForm = observer(function ConnectionForm({ controller, }: ConnectionFormProps) { const translate = useTranslate(); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const [focusedRef] = useFocus({ focusFirstChild: true }); return styled(useStyles(styles, formStyles))( - }> + diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index a8effa4255..81ecb71678 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -61,7 +61,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ rejectDialog, }: DialogComponentProps) { const connection = useConnectionInfo(payload); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const [focusedRef] = useFocus({ focusFirstChild: true }); const { driver } = useDBDriver(connection.connectionInfo?.driverId || ''); const controller = useController(DBAuthDialogController, payload, rejectDialog); @@ -81,7 +81,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ {(!connection.isLoaded() || connection.isLoading()) ? : ( - }> + implements IExecutor { private defaultData?: T | null ) { } - async execute(data: T) { + async execute(data: T): Promise> { if ((data === undefined || data === null) && this.defaultData !== undefined && this.defaultData !== null) { data = this.defaultData; } @@ -29,13 +29,14 @@ export class Executor implements IExecutor { const result = await handler(context, data); if (result === false) { - return; + return context; } } for (const handler of this.postHandlers) { await handler(context, data); } + return context; } addHandler(handler: IExecutorHandler) { @@ -43,8 +44,16 @@ export class Executor implements IExecutor { return this; } + removeHandler(handler: IExecutorHandler) { + this.handlers = this.handlers.filter(h => h === handler); + } + addPostHandler(handler: IExecutorHandler) { this.postHandlers.push(handler); return this; } + + removePostHandler(handler: IExecutorHandler) { + this.postHandlers = this.postHandlers.filter(h => h === handler); + } } diff --git a/webapp/packages/core-executor/src/IExecutor.ts b/webapp/packages/core-executor/src/IExecutor.ts index 07b1a065ec..88197768d7 100644 --- a/webapp/packages/core-executor/src/IExecutor.ts +++ b/webapp/packages/core-executor/src/IExecutor.ts @@ -6,9 +6,13 @@ * you may not use this file except in compliance with the License. */ +import { IContextProvider } from './ExecutionContext'; import { IExecutorHandler } from './IExecutorHandler'; export interface IExecutor { - execute(data: T): Promise; + execute(data: T): Promise>; addHandler(handler: IExecutorHandler): void; + removeHandler(handler: IExecutorHandler): void; + addPostHandler(handler: IExecutorHandler): void; + removePostHandler(handler: IExecutorHandler): void; } diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx index 3db5775e9d..367c671b9e 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserForm.tsx @@ -138,7 +138,7 @@ export const UserForm = observer(function UserForm({ }: Props) { const translate = useTranslate(); const controller = useController(UserFormController, userId, onCancel); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const [focusedRef] = useFocus({ focusFirstChild: true }); const handleLoginChange = useCallback( (value: string) => controller.credentials.login = value, @@ -186,7 +186,7 @@ export const UserForm = observer(function UserForm({ - }> + diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx index 06974d6617..ac1ce255d2 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx @@ -27,13 +27,13 @@ export const AuthProviderForm = observer(function AuthProviderForm({ credentials, authenticate, }: Props) { - const [elementRef] = useFocus({ focusFirstChild: true }); + const [elementRef] = useFocus({ focusFirstChild: true }); const handleChange = useCallback((key: string, value: string) => { credentials[key] = value; }, [credentials]); return styled(useStyles(formStyles))( - }> + {provider.credentialParameters.map(parameter => parameter.user && ( ({ focusFirstChild: true }); return styled(useStyles(formStyles))( - }> + ) { - const [focusedRef] = useFocus({ focusFirstChild: true }); + const [focusedRef] = useFocus({ focusFirstChild: true }); const controller = useController(ConnectionController, rejectDialog); const translate = useTranslate(); let title = translate('basicConnection_connectionDialog_newConnection'); @@ -81,7 +81,7 @@ export const ConnectionDialog = observer(function ConnectionDialog({ {controller.isConnecting && translate('basicConnection_connectionDialog_connecting_message')} ) : ( - }> + this.gridModel.access === AccessMode.Readonly, isCellEdited: this.isCellEdited.bind(this), + editCellValue: this.editCellValue.bind(this), revertCellValue: this.revertCellValue.bind(this), onEditSave: this.onEditSave.bind(this), onEditCancel: this.onEditCancel.bind(this), @@ -158,8 +159,12 @@ export class AgGridTableController implements IInitializableController, IDestruc this.gridModel.revertCellValue(rowIndex, colId); } + private editCellValue(rowIndex: number, colId: string, value: any, editing: boolean) { + this.gridModel.onCellEditingStopped(rowIndex, colId, value, editing); + } + private handleCellEditingStopped(event: CellEditingStoppedEvent) { - this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getColId(), event.value); + this.gridModel.onCellEditingStopped(event.rowIndex, event.column.getColId(), event.value, false); } private isCellEdited(rowIndex: number, column: string) { @@ -247,12 +252,7 @@ export const INDEX_COLUMN_DEF: ColDef = { suppressMenu: true, editable: false, sortable: false, - cellRenderer: (params) => { - if (!params.data) { - return 'Loading...'; - } - return params.rowIndex + 1; - }, + cellRenderer: 'indexCellRenderer', }; function mapDataToColumns(columns?: IAgGridCol[]): ColDef[] { diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/IndexCellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/IndexCellRenderer.tsx new file mode 100644 index 0000000000..092a27fd61 --- /dev/null +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/IndexCellRenderer.tsx @@ -0,0 +1,19 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { CellClassParams } from '@ag-grid-community/core'; +import { Loader } from '@cloudbeaver/core-blocks'; + +export function IndexCellRenderer(props: CellClassParams) { + return ( +
+ {!props.data && } + {props.data && props.rowIndex + 1} +
+ ); +} diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/LoadingCell.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/LoadingCell.tsx deleted file mode 100644 index 9bd9b2a7d0..0000000000 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/LoadingCell.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 function LoadingCell() { - return ( -
- Loading... -
- ); -} diff --git a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx index 47cd1b1295..d039987838 100644 --- a/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx +++ b/webapp/packages/plugin-data-spreadsheet/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx @@ -71,6 +71,7 @@ export const PlainTextEditor = forwardRef, ICellEditorP const handleChange = useCallback((newValue: string) => { value.current = newValue; + context.editCellValue(props.rowIndex, props.column.getColId(), newValue, true); forceUpdate(value.current); }, []); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableDataModel/TableEditor.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableDataModel/TableEditor.ts index 9f20c1ca14..8d40659787 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableDataModel/TableEditor.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableDataModel/TableEditor.ts @@ -30,7 +30,7 @@ export class TableEditor { /** * this method doesn't modify TableRow but create a diff that can be applied later */ - editCellValue(rowId: number, columnKey: string, value: any) { + editCellValue(rowId: number, columnKey: string, value: any, editing: boolean) { const column = this.dataModel .getColumns() .find(column => column.name === columnKey); @@ -42,7 +42,9 @@ export class TableEditor { const newValue = this.getOrCreateEditedRow(rowId) .setValue(column.position, value); - this.rowsUpdateSubject.next([rowId]); + if (!editing) { + this.rowsUpdateSubject.next([rowId]); + } return newValue; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts index 1d48a52130..f262fab860 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewerModel.ts @@ -322,12 +322,12 @@ export class TableViewerModel { } } - onCellEditingStopped(rowNumber: number, column: string, value: any) { + onCellEditingStopped(rowNumber: number, column: string, value: any, editing: boolean) { if (this.access === AccessMode.Readonly) { return; } - this.tableEditor.editCellValue(rowNumber, column, value); + this.tableEditor.editCellValue(rowNumber, column, value, editing); } onSortChanged(sorting: SortModel) {