From 997fba1cff2ca4fabf66ba73ed110448f25a283b Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Thu, 29 Aug 2024 23:56:55 +0530 Subject: [PATCH 01/26] Improved the extendability fo the SchemaView and DataGridView. Restructured these modules for ease of mainteance and apply the single responsibilty principle (wherever is applicable). * SchemaView - Split the code based on the functionality, and responsibility. - Introduced a new View 'InlineView' instead of using the 'nextInline' configuration of the fields to have a better, and manageable view. - Using the separate class 'SchemaState' for managing the data and states of the SchemaView (separated from the 'useSchemaState' custom hook). - Introduced three new custom hooks 'useFieldValue', 'useFieldOptions', 'useFieldError' for the individual control to use for each Schema Field. - Don't pass value as the parameter props, and let the 'useFieldValue' and other custom hooks to decide, wheather to rerender the control itself or the whole dialog/view. (single responsibilty principle) - Introduced a new data store with subscription facility. - Moving the field metadata (option) evaluation to separate place for better management, and each option can be defined for a particular kind of field (for example - colleciton, row, cell, general, etc). - Allow to provide custom control for all kind of Schema field. * DataGridView - Same as SchemaView, split the DataGridView call in smaller and manageable chunks. (For example - grid, row, mappedCell, etc). - Use context based approach for providing the row and table data instead of passing them as parameter to each and every component separately. - Have a facility to extend this feature separately in future. (for example - selecatable cell, column grouping, etc.) - Separated the features like deletable, editable, reorder, expandable etc. cells using the above feature support. - Added ability to provide the CustomHeader, and CustomRow through the Schema field, which will extend the ability to customisation better. - Removed the 'DataGridViewWithHeaderForm' as it has been achieved through providing 'CustomHeader', and also introduced 'DataGridFormHeader' (a custom header) to achieve the same feature as 'DataGridViewWithHeaderForm'. Also - Modified the testcases to use separate 'Schema' objects for testing the rendering of create, edit, and properties mode as BaseUISchema instance share the same SchemaState object. --- web/.eslintrc.js | 2 +- web/package.json | 4 +- .../publications/static/js/publication.ui.js | 2 +- .../static/js/domain_constraints.ui.js | 2 +- .../schemas/domains/static/js/domain.ui.js | 2 +- .../static/js/fts_configuration.ui.js | 6 +- .../schemas/packages/static/js/package.ui.js | 4 +- .../tables/columns/static/js/column.ui.js | 2 +- .../static/js/check_constraint.ui.js | 2 +- .../static/js/exclusion_constraint.ui.js | 12 +- .../foreign_key/static/js/foreign_key.ui.js | 39 +- .../tables/indexes/static/js/index.ui.js | 29 +- .../schemas/tables/static/js/table.ui.js | 71 +- .../schemas/types/static/js/type.ui.js | 4 +- .../schemas/views/static/js/mview.ui.js | 2 +- .../schemas/views/static/js/view.ui.js | 6 +- .../databases/static/js/database.ui.js | 2 +- .../static/js/subscription.ui.js | 4 +- .../servers/static/js/server.ui.js | 12 +- .../servers/static/js/variable.ui.js | 2 +- .../static/js/server_group.ui.js | 3 +- .../misc/properties/ObjectNodeProperties.jsx | 3 +- .../js/components/PreferencesComponent.jsx | 122 ++-- .../static/js/SchemaView/DataGridView.jsx | 625 ------------------ .../js/SchemaView/DataGridView/SearchBox.jsx | 47 ++ .../js/SchemaView/DataGridView/context.js | 14 + .../DataGridView/features/common.jsx | 21 + .../DataGridView/features/deletable.js | 95 +++ .../features/expandabledFormView.jsx | 90 +++ .../DataGridView/features/feature.js | 134 ++++ .../DataGridView/features/fixedrows.jsx | 46 ++ .../DataGridView/features/index.jsx | 29 + .../DataGridView/features/reorder.jsx | 160 +++++ .../DataGridView/features/search.js | 54 ++ .../js/SchemaView/DataGridView/formHeader.jsx | 166 +++++ .../js/SchemaView/DataGridView/grid.jsx | 198 ++++++ .../js/SchemaView/DataGridView/header.jsx | 103 +++ .../js/SchemaView/DataGridView/index.js | 24 + .../js/SchemaView/DataGridView/mappedCell.jsx | 111 ++++ .../static/js/SchemaView/DataGridView/row.jsx | 98 +++ .../DataGridView/utils/createGridColumns.jsx | 68 ++ .../js/SchemaView/DataGridView/utils/index.js | 16 + .../static/js/SchemaView/DepListener.js | 10 +- .../static/js/SchemaView/FieldControl.jsx | 27 + .../static/js/SchemaView/FieldSetView.jsx | 158 +---- .../static/js/SchemaView/FormLoader.jsx | 30 + web/pgadmin/static/js/SchemaView/FormView.jsx | 560 ++++++---------- .../static/js/SchemaView/InlineView.jsx | 56 ++ .../static/js/SchemaView/MappedControl.jsx | 299 +++++++-- .../static/js/SchemaView/ResetButton.jsx | 42 ++ web/pgadmin/static/js/SchemaView/SQLTab.jsx | 45 ++ .../static/js/SchemaView/SaveButton.jsx | 50 ++ .../static/js/SchemaView/SchemaDialogView.jsx | 113 ++-- .../js/SchemaView/SchemaPropertiesView.jsx | 198 ++---- .../js/SchemaView/SchemaState/SchemaState.js | 328 +++++++++ .../{schemaUtils.js => SchemaState/common.js} | 55 +- .../js/SchemaView/SchemaState/context.js | 12 + .../static/js/SchemaView/SchemaState/index.js | 21 + .../js/SchemaView/SchemaState/reducer.js | 123 ++++ .../static/js/SchemaView/SchemaState/store.js | 78 +++ .../static/js/SchemaView/SchemaView.jsx | 3 + .../static/js/SchemaView/StyledComponents.jsx | 2 +- .../static/js/SchemaView/base_schema.ui.js | 43 +- web/pgadmin/static/js/SchemaView/common.js | 64 +- .../static/js/SchemaView/hooks/index.js | 21 + .../js/SchemaView/hooks/useFieldError.js | 34 + .../js/SchemaView/hooks/useFieldOptions.js | 25 + .../js/SchemaView/hooks/useFieldValue.js | 25 + .../js/SchemaView/hooks/useSchemaState.js | 136 ++++ web/pgadmin/static/js/SchemaView/index.jsx | 23 +- .../static/js/SchemaView/options/common.js | 40 ++ .../static/js/SchemaView/options/index.js | 176 +++++ .../static/js/SchemaView/options/registry.js | 156 +++++ web/pgadmin/static/js/SchemaView/registry.js | 42 ++ .../static/js/SchemaView/useSchemaState.js | 489 -------------- .../SchemaView/utils/createFieldControls.jsx | 205 ++++++ .../static/js/SchemaView/utils/index.js | 17 + .../js/SchemaView/utils/listenDepChanges.js | 57 ++ .../static/js/components/FormComponents.jsx | 8 +- .../js/components/PgReactTableStyled.jsx | 13 - .../ReactCodeMirror/components/Editor.jsx | 29 +- .../static/js/components/SearchInputText.jsx | 49 ++ .../js/helpers/DataGridViewWithHeaderForm.jsx | 103 --- .../static/js/helpers/withStandardTabInfo.jsx | 16 +- web/pgadmin/static/js/utils.js | 59 ++ .../tools/backup/static/js/backup.ui.js | 71 +- .../components/DebuggerArgumentComponent.jsx | 4 +- .../maintenance/static/js/maintenance.ui.js | 23 +- .../tools/restore/static/js/restore.ui.js | 27 +- .../js/components/dialogs/MacrosDialog.jsx | 7 +- .../sqleditor/static/js/show_view_data.js | 2 +- .../static/js/UserManagementDialog.jsx | 9 +- .../javascript/SchemaView/store.spec.js | 157 +++++ .../schema_ui_files/aggregate.ui.spec.js | 8 +- .../schema_ui_files/cast.ui.spec.js | 9 +- .../schema_ui_files/catalog.ui.spec.js | 8 +- .../catalog_object_column.ui.spec.js | 10 +- .../check_constraint.ui.spec.js | 9 +- .../schema_ui_files/collation.ui.spec.js | 12 +- .../schema_ui_files/column.ui.spec.js | 10 +- .../compound_trigger.ui.spec.js | 13 +- .../schema_ui_files/database.ui.spec.js | 18 +- .../schema_ui_files/domain.ui.spec.js | 11 +- .../domain_constraint.ui.spec.js | 11 +- .../schema_ui_files/edbfunc.ui.spec.js | 12 +- .../schema_ui_files/edbvar.ui.spec.js | 12 +- .../schema_ui_files/event_trigger.ui.spec.js | 12 +- .../exclusion_constraint.ui.spec.js | 20 +- .../schema_ui_files/extension.ui.spec.js | 11 +- .../foreign_data_wrapper.ui.spec.js | 11 +- .../schema_ui_files/foreign_key.ui.spec.js | 2 - .../schema_ui_files/foreign_server.ui.spec.js | 12 +- .../schema_ui_files/foreign_table.ui.spec.js | 10 +- .../fts_configuration.ui.spec.js | 15 +- .../schema_ui_files/fts_dictionary.ui.spec.js | 12 +- .../schema_ui_files/fts_parser.ui.spec.js | 32 +- .../schema_ui_files/functions.ui.spec.js | 15 +- .../import_export_servers.ui.spec.js | 7 +- .../schema_ui_files/index.ui.spec.js | 82 +-- .../schema_ui_files/language.ui.spec.js | 13 +- .../schema_ui_files/membership.ui.spec.js | 15 +- .../schema_ui_files/mview.ui.spec.js | 13 +- .../schema_ui_files/operator.ui.spec.js | 12 +- .../schema_ui_files/packages.ui.spec.js | 17 +- .../schema_ui_files/partition.ui.spec.js | 44 +- .../partition.utils.ui.spec.js | 32 +- .../schema_ui_files/pga_job.ui.spec.js | 14 +- .../schema_ui_files/pga_jobstep.ui.spec.js | 17 +- .../schema_ui_files/pga_schedule.ui.spec.js | 49 +- .../schema_ui_files/primary_key.ui.spec.js | 2 - .../schema_ui_files/privilege.ui.spec.js | 19 +- .../schema_ui_files/publication.ui.spec.js | 40 +- .../schema_ui_files/resource_group.ui.spec.js | 8 +- .../schema_ui_files/restore.ui.spec.js | 8 +- .../schema_ui_files/role.ui.spec.js | 12 +- .../row_security_policy.ui.spec.js | 12 +- .../schema_ui_files/rule.ui.spec.js | 12 +- .../schema_ui_files/schema.ui.spec.js | 14 +- .../schema_ui_files/sequence.ui.spec.js | 15 +- .../schema_ui_files/server.ui.spec.js | 9 +- .../schema_ui_files/server_group.ui.spec.js | 12 +- .../schema_ui_files/subscription.ui.spec.js | 62 +- .../schema_ui_files/synonym.ui.spec.js | 45 +- .../schema_ui_files/table.ui.spec.js | 76 ++- .../schema_ui_files/tablespace.ui.spec.js | 25 +- .../schema_ui_files/trigger.ui.spec.js | 22 +- .../trigger_function.ui.spec.js | 15 +- .../schema_ui_files/type.ui.spec.js | 141 ++-- .../unique_constraint.ui.spec.js | 2 - .../schema_ui_files/user_mapping.ui.spec.js | 35 +- .../javascript/schema_ui_files/utils.js | 4 +- .../schema_ui_files/variable.ui.spec.js | 13 +- .../schema_ui_files/view.ui.spec.js | 32 +- web/yarn.lock | 136 ++-- 154 files changed, 5172 insertions(+), 2985 deletions(-) delete mode 100644 web/pgadmin/static/js/SchemaView/DataGridView.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/SearchBox.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/context.js create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/common.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/deletable.js create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/feature.js create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/fixedrows.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/reorder.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/features/search.js create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/formHeader.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/header.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/index.js create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/row.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/utils/createGridColumns.jsx create mode 100644 web/pgadmin/static/js/SchemaView/DataGridView/utils/index.js create mode 100644 web/pgadmin/static/js/SchemaView/FieldControl.jsx create mode 100644 web/pgadmin/static/js/SchemaView/FormLoader.jsx create mode 100644 web/pgadmin/static/js/SchemaView/InlineView.jsx create mode 100644 web/pgadmin/static/js/SchemaView/ResetButton.jsx create mode 100644 web/pgadmin/static/js/SchemaView/SQLTab.jsx create mode 100644 web/pgadmin/static/js/SchemaView/SaveButton.jsx create mode 100644 web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js rename web/pgadmin/static/js/SchemaView/{schemaUtils.js => SchemaState/common.js} (87%) create mode 100644 web/pgadmin/static/js/SchemaView/SchemaState/context.js create mode 100644 web/pgadmin/static/js/SchemaView/SchemaState/index.js create mode 100644 web/pgadmin/static/js/SchemaView/SchemaState/reducer.js create mode 100644 web/pgadmin/static/js/SchemaView/SchemaState/store.js create mode 100644 web/pgadmin/static/js/SchemaView/hooks/index.js create mode 100644 web/pgadmin/static/js/SchemaView/hooks/useFieldError.js create mode 100644 web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js create mode 100644 web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js create mode 100644 web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js create mode 100644 web/pgadmin/static/js/SchemaView/options/common.js create mode 100644 web/pgadmin/static/js/SchemaView/options/index.js create mode 100644 web/pgadmin/static/js/SchemaView/options/registry.js create mode 100644 web/pgadmin/static/js/SchemaView/registry.js delete mode 100644 web/pgadmin/static/js/SchemaView/useSchemaState.js create mode 100644 web/pgadmin/static/js/SchemaView/utils/createFieldControls.jsx create mode 100644 web/pgadmin/static/js/SchemaView/utils/index.js create mode 100644 web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js create mode 100644 web/pgadmin/static/js/components/SearchInputText.jsx delete mode 100644 web/pgadmin/static/js/helpers/DataGridViewWithHeaderForm.jsx create mode 100644 web/regression/javascript/SchemaView/store.spec.js diff --git a/web/.eslintrc.js b/web/.eslintrc.js index 8fbbd4c717e..9376bb9c5e8 100644 --- a/web/.eslintrc.js +++ b/web/.eslintrc.js @@ -80,7 +80,7 @@ module.exports = [ 'error', 'only-multiline', ], - 'no-console': ['error', { allow: ['warn', 'error'] }], + 'no-console': ['error', { allow: ['warn', 'error', 'trace'] }], // We need to exclude below for RegEx case 'no-useless-escape': 'off', 'no-prototype-builtins': 'off', diff --git a/web/package.json b/web/package.json index a7e3963c9e7..05eb5921fa1 100644 --- a/web/package.json +++ b/web/package.json @@ -140,7 +140,7 @@ "react-leaflet": "^4.2.1", "react-new-window": "^1.0.1", "react-resize-detector": "^11.0.1", - "react-rnd": "^10.3.5", + "react-rnd": "^10.4.12", "react-select": "^5.7.2", "react-timer-hook": "^3.0.5", "react-virtualized-auto-sizer": "^1.0.6", @@ -152,7 +152,7 @@ "uplot-react": "^1.1.4", "valid-filename": "^2.0.1", "wkx": "^0.5.0", - "zustand": "^4.4.1" + "zustand": "^4.5.4" }, "scripts": { "linter": "yarn run eslint -c .eslintrc.js .", diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js b/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js index e174d7acbb7..2e22f476559 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js @@ -209,7 +209,7 @@ export default class PublicationSchema extends BaseUISchema { } if ( !_.isUndefined(table) && table.length > 0 && - !_.isEqual(this.origData.pubtable, state.pubtable) + !_.isEqual(this.sessData.pubtable, state.pubtable) ){ return false; } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js index cae3a8c7b56..d3a793e00f3 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js @@ -51,7 +51,7 @@ export default class DomainConstraintSchema extends BaseUISchema { cell:'boolean', group: gettext('Definition'), min_version: 90200, mode: ['properties', 'create', 'edit'], readonly: function(state) { - return !obj.isNew(state) && obj.origData.convalidated; + return !obj.isNew(state) && obj.sessData.convalidated; } } ]; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js index c5db4e78b41..bc745ec5a95 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js @@ -39,7 +39,7 @@ export class DomainConstSchema extends BaseUISchema { id: 'convalidated', label: gettext('Validate?'), cell: 'checkbox', type: 'checkbox', readonly: function(state) { - let currCon = _.find(obj.top.origData.constraints, (con)=>con.conoid == state.conoid); + let currCon = _.find(obj.top.sessData.constraints, (con)=>con.conoid == state.conoid); return !obj.isNew(state) && currCon.convalidated; }, } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.ui.js index 14fc2ec096b..7ca1a7fc772 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/js/fts_configuration.ui.js @@ -9,7 +9,7 @@ import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; -import DataGridViewWithHeaderForm from 'sources/helpers/DataGridViewWithHeaderForm'; +import { DataGridFormHeader } from 'sources/SchemaView/DataGridView'; import { isEmptyString } from '../../../../../../../../static/js/validators'; class TokenHeaderSchema extends BaseUISchema { @@ -155,8 +155,8 @@ export default class FTSConfigurationSchema extends BaseUISchema { group: gettext('Tokens'), mode: ['create','edit'], editable: false, schema: this.tokColumnSchema, headerSchema: this.tokHeaderSchema, - headerVisible: function() { return true;}, - CustomControl: DataGridViewWithHeaderForm, + headerFormVisible: true, + GridHeader: DataGridFormHeader, uniqueCol : ['token'], canAdd: true, canEdit: false, canDelete: true, } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js index f48b752d5f8..1149cb0350a 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js @@ -101,7 +101,7 @@ export default class PackageSchema extends BaseUISchema { depChange: (state, source, topState, actionObj) => { if( - packageSchemaObj.origData.oid && + packageSchemaObj.sessData.oid && state.pkgheadsrc != actionObj.oldState.pkgheadsrc ) { packageSchemaObj.warningText = gettext( @@ -120,7 +120,7 @@ export default class PackageSchema extends BaseUISchema { depChange: (state, source, topState, actionObj) => { if( - packageSchemaObj.origData.oid && + packageSchemaObj.sessData.oid && state.pkgbodysrc != actionObj.oldState.pkgbodysrc ) { packageSchemaObj.warningText = gettext( diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js index afa4b8d5d1d..8c74097aec2 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js @@ -622,7 +622,7 @@ export default class ColumnSchema extends BaseUISchema { } if (!this.isNew(state) && state.colconstype == 'i' - && (this.origData.attidentity == 'a' || this.origData.attidentity == 'd') + && (this.sessData.attidentity == 'a' || this.sessData.attidentity == 'd') && (state.attidentity == 'a' || state.attidentity == 'd')) { if(isEmptyString(state.seqincrement)) { msg = gettext('Increment value cannot be empty.'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js index 57046b0d4c1..4ea95a7984d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js @@ -90,7 +90,7 @@ export default class CheckConstraintSchema extends BaseUISchema { if(obj.inTable && obj.top && !obj.top.isNew()) { return !(_.isUndefined(state.oid) || state.convalidated); } - return !obj.isNew(state) && !obj.origData.convalidated; + return !obj.isNew(state) && !obj.sessData.convalidated; }, mode: ['properties', 'create', 'edit'], }]; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui.js index b63b08bca36..be518b3be40 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui.js @@ -10,8 +10,8 @@ import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import _ from 'lodash'; import { isEmptyString } from 'sources/validators'; -import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../../../static/js/SchemaView'; -import DataGridViewWithHeaderForm from '../../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm'; +import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView'; +import { DataGridFormHeader } from 'sources/SchemaView/DataGridView'; import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax'; import TableSchema from '../../../../static/js/table.ui'; import pgAdmin from 'sources/pgadmin'; @@ -342,10 +342,12 @@ export default class ExclusionConstraintSchema extends BaseUISchema { group: gettext('Columns'), type: 'collection', mode: ['create', 'edit', 'properties'], editable: false, schema: this.exColumnSchema, - headerSchema: this.exHeaderSchema, headerVisible: (state)=>obj.isNew(state), - CustomControl: DataGridViewWithHeaderForm, + headerSchema: this.exHeaderSchema, + headerFormVisible: (state)=>obj.isNew(state), + GridHeader: DataGridFormHeader, uniqueCol: ['column'], - canAdd: false, canDelete: function(state) { + canAdd: (state)=>obj.isNew(state), + canDelete: function(state) { // We can't update columns of existing return obj.isNew(state); }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js index 261d89cbe16..acd577c110f 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js @@ -11,11 +11,12 @@ import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import _ from 'lodash'; import { isEmptyString } from 'sources/validators'; -import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../../../static/js/SchemaView'; -import DataGridViewWithHeaderForm from '../../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm'; +import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView'; +import { DataGridFormHeader } from 'sources/SchemaView/DataGridView'; import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax'; import TableSchema from '../../../../static/js/table.ui'; + export function getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, noColumns=false, initData={}) { return new ForeignKeySchema({ local_column: noColumns ? [] : ()=>getNodeListByName('column', treeNodeInfo, itemNodeData), @@ -58,12 +59,12 @@ class ForeignKeyHeaderSchema extends BaseUISchema { } addDisabled(state) { - return !(state.local_column && (state.references || this.origData.references) && state.referenced); + return !(state.local_column && (state.references || this.sessData.references) && state.referenced); } /* Data to ForeignKeyColumnSchema will added using the header form */ getNewData(data) { - let references_table_name = _.find(this.refTables, (t)=>t.value==data.references || t.value == this.origData.references)?.label; + let references_table_name = _.find(this.refTables, (t)=>t.value==data.references || t.value == this.sessData.references)?.label; return { local_column: data.local_column, referenced: data.referenced, @@ -227,13 +228,16 @@ export default class ForeignKeySchema extends BaseUISchema { type: 'switch', group: gettext('Definition'), readonly: (state)=>{ if(!obj.isNew(state)) { - let origData = {}; + let sessData = {}; if(obj.inTable && obj.top) { - origData = _.find(obj.top.origData['foreign_key'], (r)=>r.cid == state.cid); + sessData = _.find( + obj.top.sessData['foreign_key'], + (r) => r.cid == state.cid + ); } else { - origData = obj.origData; + sessData = obj.sessData; } - return origData.convalidated; + return sessData.convalidated; } return false; }, @@ -304,14 +308,14 @@ export default class ForeignKeySchema extends BaseUISchema { group: gettext('Columns'), type: 'collection', mode: ['create', 'edit', 'properties'], editable: false, schema: this.fkColumnSchema, - headerSchema: this.fkHeaderSchema, headerVisible: (state)=>obj.isNew(state), - CustomControl: DataGridViewWithHeaderForm, + headerSchema: this.fkHeaderSchema, + headerFormVisible: (state)=>obj.isNew(state), + GridHeader: DataGridFormHeader, uniqueCol: ['local_column', 'references', 'referenced'], - canAdd: false, canDelete: function(state) { - // We can't update columns of existing foreign key. - return obj.isNew(state); - }, - readonly: obj.isReadonly, cell: ()=>({ + canAdd: (state)=>obj.isNew(state), + canDelete: (state)=>obj.isNew(state), + readonly: obj.isReadonly, + cell: () => ({ cell: '', controlProps: { formatter: { @@ -358,10 +362,9 @@ export default class ForeignKeySchema extends BaseUISchema { } } if(actionObj.type == SCHEMA_STATE_ACTIONS.ADD_ROW) { - obj.fkHeaderSchema.origData.references = null; // Set references value. - obj.fkHeaderSchema.origData.references = obj.fkHeaderSchema.sessData.references; - obj.fkHeaderSchema.origData._disable_references = true; + obj.fkHeaderSchema.sessData.references = null; + obj.fkHeaderSchema.sessData._disable_references = true; } return {columns: currColumns}; }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js index 650dca4a571..b6cedb88a86 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js @@ -7,12 +7,13 @@ // ////////////////////////////////////////////////////////////// -import gettext from 'sources/gettext'; -import BaseUISchema from 'sources/SchemaView/base_schema.ui'; -import DataGridViewWithHeaderForm from '../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm'; import _ from 'lodash'; -import { isEmptyString } from 'sources/validators'; + +import { DataGridFormHeader } from 'sources/SchemaView/DataGridView'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import gettext from 'sources/gettext'; import pgAdmin from 'sources/pgadmin'; +import { isEmptyString } from 'sources/validators'; function inSchema(node_info) { @@ -23,8 +24,8 @@ class IndexColHeaderSchema extends BaseUISchema { constructor(columns) { super({ is_exp: true, - colname: undefined, - expression: undefined, + colname: '', + expression: '', }); this.columns = columns; @@ -90,10 +91,10 @@ class IndexColumnSchema extends BaseUISchema { } isEditable(state) { - let topObj = this._top; + let topObj = this.top; if(this.inSchemaWithModelCheck(state)) { return false; - } else if (topObj._sessData && topObj._sessData.amname === 'btree') { + } else if (topObj.sessData && topObj.sessData.amname === 'btree') { state.is_sort_nulls_applicable = true; return true; } else { @@ -155,9 +156,7 @@ class IndexColumnSchema extends BaseUISchema { * to access method selected by user if not selected * send btree related op_class options */ - let amname = obj._top?._sessData ? - obj._top?._sessData.amname : - obj._top?.origData.amname; + let amname = obj.top?.sessData.amname; if(_.isUndefined(amname)) return options; @@ -573,10 +572,12 @@ export default class IndexSchema extends BaseUISchema { group: gettext('Columns'), type: 'collection', mode: ['create', 'edit', 'properties'], editable: false, schema: this.indexColumnSchema, - headerSchema: this.indexHeaderSchema, headerVisible: (state)=>indexSchemaObj.isNew(state), - CustomControl: DataGridViewWithHeaderForm, + headerSchema: this.indexHeaderSchema, + headerFormVisible: (state)=>indexSchemaObj.isNew(state), + GridHeader: DataGridFormHeader, uniqueCol: ['colname'], - canAdd: false, canDelete: function(state) { + canAdd: (state)=>indexSchemaObj.isNew(state), + canDelete: function(state) { // We can't update columns of existing return indexSchemaObj.isNew(state); }, cell: ()=>({ diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js index 92ee4a662f2..67d4dab63a4 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js @@ -25,9 +25,11 @@ import { getNodePrivilegeRoleSchema } from '../../../../../static/js/privilege.u import pgAdmin from 'sources/pgadmin'; export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) { - const spcname = ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{ - return (m.label != 'pg_global'); - }); + const spcname = () => getNodeListByName( + 'tablespace', treeNodeInfo, itemNodeData, {}, (m) => { + return (m.label != 'pg_global'); + } + ); let tableNode = pgBrowser.Nodes['table']; @@ -48,9 +50,9 @@ export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) { }, treeNodeInfo, { - columns: ()=>getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser), - vacuum_settings: ()=>getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData), - constraints: ()=>new ConstraintsSchema( + columns: () => getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser), + vacuum_settings: () => getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData), + constraints: () => new ConstraintsSchema( treeNodeInfo, ()=>getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, true, {autoindex: false}), ()=>getNodeExclusionConstraintSchema(treeNodeInfo, itemNodeData, pgBrowser, true), @@ -274,46 +276,47 @@ export class LikeSchema extends BaseUISchema { id: 'like_default_value', label: gettext('With default values?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineNext: true, + inlineGroup: 'like_relelation', },{ id: 'like_constraints', label: gettext('With constraints?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineNext: true, + inlineGroup: 'like_relelation', },{ id: 'like_indexes', label: gettext('With indexes?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineNext: true, + inlineGroup: 'like_relelation', },{ id: 'like_storage', label: gettext('With storage?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineNext: true, + inlineGroup: 'like_relelation', },{ id: 'like_comments', label: gettext('With comments?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineNext: true, + inlineGroup: 'like_relelation', },{ id: 'like_compression', label: gettext('With compression?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - min_version: 140000, inlineNext: true, + min_version: 140000, inlineGroup: 'like_relelation', },{ id: 'like_generated', label: gettext('With generated?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - min_version: 120000, inlineNext: true, + min_version: 120000, inlineGroup: 'like_relelation', },{ id: 'like_identity', label: gettext('With identity?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineNext: true, + inlineGroup: 'like_relelation', },{ id: 'like_statistics', label: gettext('With statistics?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), + inlineGroup: 'like_relelation', } ]; } @@ -484,6 +487,12 @@ export default class TableSchema extends BaseUISchema { } }; } + },{ + id: 'columns', type: 'group', label: gettext('Columns'), + },{ + id: 'advanced', label: gettext('Advanced'), type: 'group', + },{ + id: 'constraints', label: gettext('Constraints'), type: 'group', },{ id: 'partition', type: 'group', label: gettext('Partitions'), mode: ['edit', 'create'], min_version: 100000, @@ -494,6 +503,12 @@ export default class TableSchema extends BaseUISchema { // Always show in case of create mode return (obj.isNew(state) || state.is_partitioned); }, + },{ + type: 'group', id: 'parameters', label: gettext('Parameters'), + visible: !this.inErd, + },{ + id: 'security_group', type: 'group', label: gettext('Security'), + visible: !this.inErd, },{ id: 'is_partitioned', label:gettext('Partitioned table?'), cell: 'switch', type: 'switch', mode: ['properties', 'create', 'edit'], @@ -510,9 +525,12 @@ export default class TableSchema extends BaseUISchema { mode: ['properties', 'create', 'edit'], disabled: this.inCatalog, },{ id: 'coll_inherits', label: gettext('Inherited from table(s)'), - type: 'select', group: gettext('Columns'), + type: 'select', group: 'columns', deps: ['typname', 'is_partitioned'], mode: ['create', 'edit'], - controlProps: { multiple: true, allowClear: false, placeholder: gettext('Select to inherit from...')}, + controlProps: { + multiple: true, allowClear: false, + placeholder: gettext('Select to inherit from...') + }, options: this.fieldOptions.coll_inherits, visible: !this.inErd, optionsLoaded: (res)=>obj.inheritedTableList=res, disabled: (state)=>{ @@ -611,16 +629,13 @@ export default class TableSchema extends BaseUISchema { } }); }, - },{ - id: 'advanced', label: gettext('Advanced'), type: 'group', - visible: true, }, { id: 'rlspolicy', label: gettext('RLS Policy?'), cell: 'switch', type: 'switch', mode: ['properties','edit', 'create'], group: 'advanced', min_version: 90600, depChange: (state)=>{ - if (state.rlspolicy && this.origData.rlspolicy != state.rlspolicy) { + if (state.rlspolicy && this.sessData.rlspolicy != state.rlspolicy) { pgAdmin.Browser.notifier.alert( gettext('Check Policy?'), gettext('Please check if any policy exists. If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified by other users') @@ -654,12 +669,9 @@ export default class TableSchema extends BaseUISchema { },{ // Tab control for columns id: 'columns', label: gettext('Columns'), type: 'collection', - group: gettext('Columns'), - schema: this.columnsSchema, - mode: ['create', 'edit'], - disabled: this.inCatalog, - deps: ['typname', 'is_partitioned'], - depChange: (state, source, topState, actionObj)=>{ + group: 'columns', schema: this.columnsSchema, mode: ['create', 'edit'], + disabled: this.inCatalog, deps: ['typname', 'is_partitioned'], + depChange: (state, source, topState, actionObj) => { if(source[0] === 'columns') { /* In ERD, attnum is an imp let for setting the links Here, attnum is set to max avail value. @@ -718,7 +730,7 @@ export default class TableSchema extends BaseUISchema { allowMultipleEmptyRow: false, },{ // Here we will create tab control for constraints - type: 'nested-tab', group: gettext('Constraints'), + type: 'nested-tab', group: 'constraints', mode: ['edit', 'create'], schema: obj.constraintsObj, },{ @@ -995,17 +1007,12 @@ export default class TableSchema extends BaseUISchema { '', ].join(''), min_version: 100000, - },{ - type: 'group', id: 'parameters', label: gettext('Parameters'), - visible: !this.inErd, },{ // Here - we will create tab control for storage parameters // (auto vacuum). type: 'nested-tab', group: 'parameters', mode: ['edit', 'create'], deps: ['is_partitioned'], schema: this.vacuumSettingsSchema, visible: !this.inErd, - },{ - id: 'security_group', type: 'group', label: gettext('Security'), visible: !this.inErd, }, { id: 'relacl_str', label: gettext('Privileges'), disabled: this.inCatalog, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js index 1995428a86c..907b66fab5a 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js @@ -1024,7 +1024,7 @@ class DataTypeSchema extends BaseUISchema { return [{ id: 'type', label: gettext('Data Type'), - group: gettext('Definition'), + group: gettext('Data Type'), mode: ['edit', 'create'], disabled: false, readonly: function (state) { @@ -1056,7 +1056,7 @@ class DataTypeSchema extends BaseUISchema { } },{ id: 'maxsize', - group: gettext('Definition'), + group: gettext('Data Type'), label: gettext('Size'), type: 'int', deps: ['typtype'], diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js index 96d477ecad1..e1521f07351 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js @@ -154,7 +154,7 @@ export default class MViewSchema extends BaseUISchema { if (state.definition) { obj.warningText = null; - if (obj.origData.oid !== undefined && state.definition !== obj.origData.definition) { + if (obj.sessData.oid !== undefined && state.definition !== obj.sessData.definition) { obj.warningText = gettext( 'Updating the definition will drop and re-create the materialized view. It may result in loss of information about its dependent objects.' ) + '

' + gettext('Do you want to continue?') + ''; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js index 7885792d0dd..6ab6338e587 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js @@ -137,15 +137,15 @@ export default class ViewSchema extends BaseUISchema { if (state.definition) { if (!(obj.nodeInfo.server.server_type == 'pg' && // No need to check this when creating a view - obj.origData.oid !== undefined + obj.sessData.oid !== undefined ) || ( - state.definition === obj.origData.definition + state.definition === obj.sessData.definition )) { obj.warningText = null; return false; } - let old_def = obj.origData.definition?.replace( + let old_def = obj.sessData.definition?.replace( /\s/gi, '' ).split('FROM'), new_def = []; diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js index f13c4d7c438..3b0cf68e515 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js @@ -284,7 +284,7 @@ export default class DatabaseSchema extends BaseUISchema { obj.informText = undefined; } - if(!_.isEqual(obj.origData.schema_res, state.schema_res)) { + if(!_.isEqual(obj.sessData.schema_res, state.schema_res)) { obj.informText = gettext( 'Please refresh the Schemas node to make changes to the schema restriction take effect.' ); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js index f6775c187df..0415c918211 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js @@ -129,7 +129,7 @@ export default class SubscriptionSchema extends BaseUISchema{ id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], min: 1, max: 65535, depChange: (state)=>{ - if(obj.origData.port != state.port && !obj.isNew(state) && state.connected){ + if(obj.sessData.port != state.port && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); @@ -145,7 +145,7 @@ export default class SubscriptionSchema extends BaseUISchema{ id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], depChange: (state)=>{ - if(obj.origData.username != state.username && !obj.isNew(state) && state.connected){ + if(obj.sessData.username != state.username && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js index 34e3e57ffa1..3ca48a917cc 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js @@ -136,14 +136,14 @@ export default class ServerSchema extends BaseUISchema { id: 'shared_username', label: gettext('Shared Username'), type: 'text', controlProps: { maxLength: 64}, mode: ['properties', 'create', 'edit'], deps: ['shared', 'username'], - readonly: (s)=>{ - return !(!this.origData.shared && s.shared); + readonly: (s) => { + return !(!this.sessData.shared && s.shared); }, visible: ()=>{ return current_user.is_admin && pgAdmin.server_mode == 'True'; }, depChange: (state, source, _topState, actionObj)=>{ let ret = {}; - if(this.origData.shared) { + if(this.sessData.shared) { return ret; } if(source == 'username' && actionObj.oldState.username == state.shared_username) { @@ -169,7 +169,7 @@ export default class ServerSchema extends BaseUISchema { id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], disabled: obj.isShared, depChange: (state)=>{ - if(obj.origData.host != state.host && !obj.isNew(state) && state.connected){ + if(obj.sessData.host != state.host && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); @@ -182,7 +182,7 @@ export default class ServerSchema extends BaseUISchema { id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: obj.isShared, depChange: (state)=>{ - if(obj.origData.port != state.port && !obj.isNew(state) && state.connected){ + if(obj.sessData.port != state.port && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); @@ -198,7 +198,7 @@ export default class ServerSchema extends BaseUISchema { id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], depChange: (state)=>{ - if(obj.origData.username != state.username && !obj.isNew(state) && state.connected){ + if(obj.sessData.username != state.username && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js index 9c867bda767..0f01355e942 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/variable.ui.js @@ -148,7 +148,7 @@ export default class VariableSchema extends BaseUISchema { editable: function(state) { return obj.isNew(state) || !obj.allReadOnly; }, - cell: ()=>({ + cell: () => ({ cell: 'select', options: this.vnameOptions, optionsLoaded: (options)=>{obj.setVarTypes(options);}, diff --git a/web/pgadmin/browser/server_groups/static/js/server_group.ui.js b/web/pgadmin/browser/server_groups/static/js/server_group.ui.js index b81c8dd853f..84044484528 100644 --- a/web/pgadmin/browser/server_groups/static/js/server_group.ui.js +++ b/web/pgadmin/browser/server_groups/static/js/server_group.ui.js @@ -10,6 +10,7 @@ import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; + export default class ServerGroupSchema extends BaseUISchema { constructor() { super({ @@ -28,7 +29,7 @@ export default class ServerGroupSchema extends BaseUISchema { id: 'name', label: gettext('Name'), type: 'text', group: null, mode: ['properties', 'edit', 'create'], noEmpty: true, disabled: false, - } + }, ]; } } diff --git a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx index 4aa0718de45..99ee891b2d4 100644 --- a/web/pgadmin/misc/properties/ObjectNodeProperties.jsx +++ b/web/pgadmin/misc/properties/ObjectNodeProperties.jsx @@ -52,7 +52,8 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD const treeNodeId = objToString(treeNodeInfo); let schema = useMemo( - () => node.getSchema(treeNodeInfo, nodeData), [treeNodeId, isActive] + () => node.getSchema(treeNodeInfo, nodeData), + [treeNodeId] ); // We only have two actionTypes, 'create' and 'edit' to initiate the dialog, diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx index 400fc933792..fe8e5115d70 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -86,6 +86,7 @@ const StyledBox = styled(Box)(({theme}) => ({ }, })); + class PreferencesSchema extends BaseUISchema { constructor(initValues = {}, schemaFields = []) { super({ @@ -99,8 +100,8 @@ class PreferencesSchema extends BaseUISchema { return 'id'; } - setSelectedCategory(category) { - this.category = category; + categoryUpdated() { + this.state?.validate(this.sessData); } get baseFields() { @@ -109,7 +110,8 @@ class PreferencesSchema extends BaseUISchema { } -function RightPanel({ schema, ...props }) { +function RightPanel({ schema, refreshKey, ...props }) { + const schemaViewRef = React.useRef(null); let initData = () => new Promise((resolve, reject) => { try { resolve(props.initValues); @@ -117,20 +119,31 @@ function RightPanel({ schema, ...props }) { reject(error instanceof Error ? error : Error(gettext('Something went wrong'))); } }); + useEffect(() => { + const timeID = setTimeout(() => { + const focusableElement = schemaViewRef.current?.querySelector( + 'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + if (focusableElement) focusableElement.focus(); + }, 50); + return () => clearTimeout(timeID); + }, [refreshKey]); return ( - { - props.onDataChange(changedData); - }} - /> +
+ { + props.onDataChange(changedData); + }} + /> +
); } @@ -143,6 +156,7 @@ RightPanel.propTypes = { export default function PreferencesComponent({ ...props }) { + const [refreshKey, setRefreshKey] = React.useState(0); const [disableSave, setDisableSave] = React.useState(true); const prefSchema = React.useRef(new PreferencesSchema({}, [])); const prefChangedData = React.useRef({}); @@ -213,12 +227,17 @@ export default function PreferencesComponent({ ...props }) { setPrefTreeData(preferencesTreeData); setInitValues(preferencesValues); // set Preferences schema - prefSchema.current = new PreferencesSchema(preferencesValues, preferencesData); + prefSchema.current = new PreferencesSchema( + preferencesValues, preferencesData, + ); }).catch((err) => { pgAdmin.Browser.notifier.alert(err); }); }, []); - function setPreferences(node, subNode, nodeData, preferencesValues, preferencesData) { + + function setPreferences( + node, subNode, nodeData, preferencesValues, preferencesData + ) { let addBinaryPathNote = false; subNode.preferences.forEach((element) => { let note = ''; @@ -334,9 +353,10 @@ export default function PreferencesComponent({ ...props }) { preferencesData.push( { id: _.uniqueId('note') + subNode.id, - type: 'note', text: note, + type: 'note', + text: note, + 'parentId': nodeData['id'], visible: false, - 'parentId': nodeData['id'] }, ); } @@ -350,28 +370,25 @@ export default function PreferencesComponent({ ...props }) { } useEffect(() => { - let initTreeTimeout = null; let firstElement = null; // Listen selected preferences tree node event and show the appropriate components in right panel. pgAdmin.Browser.Events.on('preferences:tree:selected', (event, item) => { if (item.type == FileType.File) { - prefSchema.current.setSelectedCategory(item._metadata.data.name); prefSchema.current.schemaFields.forEach((field) => { - field.visible = field.parentId === item._metadata.data.id && !field?.hidden ; + field.visible = field.parentId === item._metadata.data.id && + !field?.hidden ; + if(field.visible && _.isNull(firstElement)) { firstElement = field; } - field.labelTooltip = item._parent._metadata.data.name.toLowerCase() + ':' + item._metadata.data.name + ':' + field.name; + + field.labelTooltip = + item._parent._metadata.data.name.toLowerCase() + ':' + + item._metadata.data.name + ':' + field.name; }); - setLoadTree(crypto.getRandomValues(new Uint16Array(1))); - initTreeTimeout = setTimeout(() => { - prefTreeInit.current = true; - if(firstElement) { - //set focus on first element on right side panel. - document.getElementsByName(firstElement.id.toString())[0].focus(); - firstElement = ''; - } - }, 10); + prefSchema.current.categoryUpdated(item._metadata.data.id); + setLoadTree(Date.now()); + setRefreshKey(Date.now()); } else { selectChildNode(item, prefTreeInit); @@ -385,10 +402,6 @@ export default function PreferencesComponent({ ...props }) { // Listen added preferences tree node event to expand the newly added node on tree load. pgAdmin.Browser.Events.on('preferences:tree:added', addPrefTreeNode); - /* Clear the initTreeTimeout timeout if unmounted */ - return () => { - clearTimeout(initTreeTimeout); - }; }, []); function addPrefTreeNode(event, item) { @@ -596,29 +609,50 @@ export default function PreferencesComponent({ ...props }) { { - useMemo(() => (prefTreeData && props.renderTree(prefTreeData)), [prefTreeData]) + useMemo( + () => (prefTreeData && props.renderTree(prefTreeData)), + [prefTreeData] + ) } { prefSchema.current && loadTree > 0 && - { - Object.keys(changedData).length > 0 ? setDisableSave(false) : setDisableSave(true); - prefChangedData.current = changedData; - }}> + { + Object.keys(changedData).length > 0 ? + setDisableSave(false) : setDisableSave(true); + prefChangedData.current = changedData; + }} + > } - } title={gettext('Help for this dialog.')} /> + } title={gettext('Help for this dialog.')} + /> - { props.closeModal();}} startIcon={ { props.closeModal();}} />}> + { props.closeModal();}} + startIcon={ + { props.closeModal();}} /> + }> {gettext('Cancel')} - } disabled={disableSave} onClick={() => { savePreferences(prefChangedData, initValues); }}> + } + disabled={disableSave} + onClick={() => { + savePreferences(prefChangedData, initValues); + }}> {gettext('Save')} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx deleted file mode 100644 index aab6f7fec5e..00000000000 --- a/web/pgadmin/static/js/SchemaView/DataGridView.jsx +++ /dev/null @@ -1,625 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2024, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -/* The DataGridView component is based on react-table component */ - -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Box } from '@mui/material'; -import AddIcon from '@mui/icons-material/AddOutlined'; -import { - useReactTable, - getCoreRowModel, - getSortedRowModel, - getFilteredRowModel, - getExpandedRowModel, - flexRender, -} from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import PropTypes from 'prop-types'; -import _ from 'lodash'; -import { DndProvider, useDrag, useDrop } from 'react-dnd'; -import {HTML5Backend} from 'react-dnd-html5-backend'; - -import { usePgAdmin } from 'sources/BrowserComponent'; -import { PgIconButton } from 'sources/components/Buttons'; -import { - PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, - PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent, - getDeleteCell, getEditCell, getReorderCell -} from 'sources/components/PgReactTableStyled'; -import CustomPropTypes from 'sources/custom_prop_types'; -import { useIsMounted } from 'sources/custom_hooks'; -import { InputText } from 'sources/components/FormComponents'; -import gettext from 'sources/gettext'; -import { evalFunc, requestAnimationAndFocus } from 'sources/utils'; - -import FormView from './FormView'; -import { MappedCellControl } from './MappedControl'; -import { - SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData, - isModeSupportedByField -} from './common'; -import { StyleDataGridBox } from './StyledComponents'; - - -function DataTableRow({ - index, row, totalRows, isResizing, isHovered, schema, schemaRef, accessPath, - moveRow, setHoverIndex, viewHelperProps -}) { - - const [key, setKey] = useState(false); - const schemaState = useContext(SchemaStateContext); - const rowRef = useRef(null); - const dragHandleRef = useRef(null); - - /* Memoize the row to avoid unnecessary re-render. - * If table data changes, then react-table re-renders the complete tables - * We can avoid re-render by if row data is not changed - */ - let depsMap = [JSON.stringify(row.original)]; - const externalDeps = useMemo(()=>{ - let retVal = []; - /* Calculate the fields which depends on the current field - deps has info on fields which the current field depends on. */ - schema.fields.forEach((field)=>{ - (evalFunc(null, field.deps) || []).forEach((dep)=>{ - let source = accessPath.concat(dep); - if(_.isArray(dep)) { - source = dep; - /* If its an array, then dep is from the top schema and external */ - retVal.push(source); - } - }); - }); - return retVal; - }, []); - - useEffect(()=>{ - schemaRef.current.fields.forEach((field)=>{ - /* Self change is also dep change */ - if(field.depChange || field.deferredDepChange) { - schemaState?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange); - } - (evalFunc(null, field.deps) || []).forEach((dep)=>{ - let source = accessPath.concat(dep); - if(_.isArray(dep)) { - source = dep; - } - if(field.depChange) { - schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange); - } - }); - }); - - return ()=>{ - /* Cleanup the listeners when unmounting */ - schemaState?.removeDepListener(accessPath); - }; - }, []); - - const [{ handlerId }, drop] = useDrop({ - accept: 'row', - collect(monitor) { - return { - handlerId: monitor.getHandlerId(), - }; - }, - hover(item, monitor) { - if (!rowRef.current) { - return; - } - item.hoverIndex = null; - // Don't replace items with themselves - if (item.index === index) { - return; - } - // Determine rectangle on screen - const hoverBoundingRect = rowRef.current?.getBoundingClientRect(); - // Determine mouse position - const clientOffset = monitor.getClientOffset(); - // Get pixels to the top - const hoverClientY = clientOffset.y - hoverBoundingRect.top; - // Only perform the move when the mouse has crossed certain part of the items height - // Dragging downwards - if (item.index < index && hoverClientY < (hoverBoundingRect.bottom - hoverBoundingRect.top)/3) { - return; - } - // Dragging upwards - if (item.index > index && hoverClientY > ((hoverBoundingRect.bottom - hoverBoundingRect.top)*2/3)) { - return; - } - setHoverIndex(index); - item.hoverIndex = index; - }, - }); - - const [, drag] = useDrag({ - type: 'row', - item: () => { - return {index}; - }, - end: (item)=>{ - // Time to actually perform the action - setHoverIndex(null); - if(item.hoverIndex >= 0) { - moveRow(item.index, item.hoverIndex); - } - } - }); - - /* External deps values are from top schema sess data */ - depsMap = depsMap.concat(externalDeps.map((source)=>_.get(schemaRef.current.top?.sessData, source))); - depsMap = depsMap.concat([totalRows, row.getIsExpanded(), key, isResizing, isHovered]); - - drag(dragHandleRef); - drop(rowRef); - - return useMemo(()=> - - {row.getVisibleCells().map((cell) => { - // Let's not render the cell, which are not supported in this mode. - if (cell.column.field && !isModeSupportedByField( - cell.column.field, viewHelperProps - )) return; - - const content = flexRender(cell.column.columnDef.cell, { - key: cell.column.columnDef.cell?.type ?? cell.column.columnDef.id, - ...cell.getContext(), - reRenderRow: ()=>{setKey((currKey)=>!currKey);} - }); - - return ( - - {content} - - ); - })} -
-
, depsMap); -} - -export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTextChange}) { - const [searchText, setSearchText] = useState(''); - return ( - - { label && - {label} - } - { canSearch && - - { - onSearchTextChange(value); - setSearchText(value); - }} - placeholder={gettext('Search')}> - - - } - - {canAdd && { - setSearchText(''); - onSearchTextChange(''); - onAddClick(); - }} icon={} className='DataGridView-gridControlsButton' />} - - - ); -} -DataGridHeader.propTypes = { - label: PropTypes.string, - canAdd: PropTypes.bool, - onAddClick: PropTypes.func, - canSearch: PropTypes.bool, - onSearchTextChange: PropTypes.func, -}; - -function getMappedCell({ - field, - schemaRef, - viewHelperProps, - accessPath, - dataDispatch -}) { - const Cell = ({row, ...other}) => { - const value = other.getValue(); - /* Make sure to take the latest field info from schema */ - field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field; - - let {editable, disabled, modeSupported} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps); - - if(_.isUndefined(field.cell)) { - console.error('cell is required ', field); - } - - return modeSupported && { - if(field.radioType) { - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.BULK_UPDATE, - path: accessPath, - value: changeValue, - id: field.id - }); - } - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.SET_VALUE, - path: accessPath.concat([row.index, field.id]), - value: changeValue, - }); - }} - reRenderRow={other.reRenderRow} - />; - }; - - Cell.displayName = 'Cell'; - Cell.propTypes = { - row: PropTypes.object.isRequired, - value: PropTypes.any, - onCellChange: PropTypes.func, - }; - - return Cell; -} - -export default function DataGridView({ - value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName, - fixedRows, ...props -}) { - - const schemaState = useContext(SchemaStateContext); - const checkIsMounted = useIsMounted(); - const [hoverIndex, setHoverIndex] = useState(); - const newRowIndex = useRef(); - const pgAdmin = usePgAdmin(); - const [searchVal, setSearchVal] = useState(''); - - /* Using ref so that schema variable is not frozen in columns closure */ - const schemaRef = useRef(schema); - const columns = useMemo( - ()=>{ - let cols = []; - if(props.canReorder) { - let colInfo = { - header: <> , - id: 'btn-reorder', - accessorFn: ()=>{/*This is intentional (SonarQube)*/}, - enableResizing: false, - enableSorting: false, - dataType: 'reorder', - size: 36, - maxSize: 26, - minSize: 26, - cell: getReorderCell(), - }; - cols.push(colInfo); - } - if(props.canEdit) { - let colInfo = { - header: <> , - id: 'btn-edit', - accessorFn: ()=>{/*This is intentional (SonarQube)*/}, - enableResizing: false, - enableSorting: false, - dataType: 'edit', - size: 26, - maxSize: 26, - minSize: 26, - cell: getEditCell({ - isDisabled: (row)=>{ - let canEditRow = true; - if(props.canEditRow) { - canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {}); - } - return !canEditRow; - }, - title: gettext('Edit row'), - }) - }; - cols.push(colInfo); - } - if(props.canDelete) { - let colInfo = { - header: <> , - id: 'btn-delete', - accessorFn: ()=>{/*This is intentional (SonarQube)*/}, - enableResizing: false, - enableSorting: false, - dataType: 'delete', - size: 26, - maxSize: 26, - minSize: 26, - cell: getDeleteCell({ - title: gettext('Delete row'), - isDisabled: (row)=>{ - let canDeleteRow = true; - if(props.canDeleteRow) { - canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {}); - } - return !canDeleteRow; - }, - onClick: (row)=>{ - const deleteRow = ()=> { - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.DELETE_ROW, - path: accessPath, - value: row.index, - }); - return true; - }; - - if (props.onDelete){ - props.onDelete(row.original || {}, deleteRow); - } else { - pgAdmin.Browser.notifier.confirm( - props.customDeleteTitle || gettext('Delete Row'), - props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'), - deleteRow, - function() { - return true; - } - ); - } - } - }), - }; - cols.push(colInfo); - } - - cols = cols.concat( - schemaRef.current.fields.filter((f) => ( - _.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true - )).sort((firstF, secondF) => ( - _.isArray(props.columns) ? (( - props.columns.indexOf(firstF.id) < - props.columns.indexOf(secondF.id) - ) ? -1 : 1) : 0 - )).map((field) => { - let widthParms = {}; - if(field.width) { - widthParms.size = field.width; - widthParms.minSize = field.width; - } else { - widthParms.size = 75; - widthParms.minSize = 75; - } - if(field.minWidth) { - widthParms.minSize = field.minWidth; - } - if(field.maxWidth) { - widthParms.maxSize = field.maxWidth; - } - widthParms.enableResizing = - _.isUndefined(field.enableResizing) ? true : Boolean( - field.enableResizing - ); - - let colInfo = { - header: field.label||<> , - accessorKey: field.id, - field: field, - enableResizing: true, - enableSorting: false, - ...widthParms, - cell: getMappedCell({ - field: field, - schemaRef: schemaRef, - viewHelperProps: viewHelperProps, - accessPath: accessPath, - dataDispatch: dataDispatch, - }), - }; - - return colInfo; - }) - ); - return cols; - },[props.canEdit, props.canDelete, props.canReorder] - ); - - const columnVisibility = useMemo(()=>{ - const ret = {}; - - columns.forEach(column => { - ret[column.id] = isModeSupportedByField(column.field, viewHelperProps); - }); - - return ret; - }, [columns, viewHelperProps]); - - const table = useReactTable({ - columns, - data: value, - autoResetAll: false, - state: { - globalFilter: searchVal, - columnVisibility: columnVisibility, - }, - columnResizeMode: 'onChange', - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getExpandedRowModel: getExpandedRowModel(), - }); - - const rows = table.getRowModel().rows; - - const onAddClick = useCallback(()=>{ - if(!props.canAddRow) { - return; - } - let newRow = schemaRef.current.getNewData(); - - const current_macros = schemaRef.current?._top?._sessData?.macro || null; - if (current_macros){ - newRow = schemaRef.current.getNewData(current_macros); - } - - newRowIndex.current = props.addOnTop ? 0 : rows.length; - - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.ADD_ROW, - path: accessPath, - value: newRow, - addOnTop: props.addOnTop - }); - }, [props.canAddRow, rows?.length]); - - useEffect(() => { - let rowsPromise = fixedRows; - - // If fixedRows is defined, fetch the details. - if(typeof rowsPromise === 'function') { - rowsPromise = rowsPromise(); - } - - if(rowsPromise) { - Promise.resolve(rowsPromise) - .then((res) => { - /* If component unmounted, dont update state */ - if(checkIsMounted()) { - schemaState.setUnpreparedData(accessPath, res); - } - }); - } - }, []); - - useEffect(()=>{ - if(newRowIndex.current >= 0) { - virtualizer.scrollToIndex(newRowIndex.current); - - // Try autofocus on newly added row. - setTimeout(() => { - const rowInput = tableRef.current?.querySelector( - `.pgrt-row[data-index="${newRowIndex.current}"] input` - ); - if(!rowInput) return; - - requestAnimationAndFocus(tableRef.current.querySelector( - `.pgrt-row[data-index="${newRowIndex.current}"] input` - )); - props.expandEditOnAdd && props.canEdit && - rows[newRowIndex.current]?.toggleExpanded(true); - newRowIndex.current = undefined; - }, 50); - } - }, [rows?.length]); - - const tableRef = useRef(); - - const moveRow = (dragIndex, hoverIndex) => { - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.MOVE_ROW, - path: accessPath, - oldIndex: dragIndex, - newIndex: hoverIndex, - }); - }; - - const isResizing = _.flatMap(table.getHeaderGroups(), headerGroup => headerGroup.headers.map(header=>header.column.getIsResizing())).includes(true); - - const virtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => tableRef.current, - estimateSize: () => 42, - measureElement: - typeof window !== 'undefined' && - navigator.userAgent.indexOf('Firefox') === -1 - ? element => element?.getBoundingClientRect().height - : undefined, - overscan: viewHelperProps.virtualiseOverscan ?? 10, - }); - - if(!props.visible) { - return <>; - } - - return ( - - - {(props.label || props.canAdd) && { - setSearchVal(value || undefined); - }} - />} - - - - - {virtualizer.getVirtualItems().map((virtualRow) => { - const row = rows[virtualRow.index]; - - return virtualizer.measureElement(node)} - style={{ - transform: `translateY(${virtualRow.start}px)`, // this should always be a `style` as it changes on scroll - }}> - - {props.canEdit && - - { - requestAnimationAndFocus(ele); - }}/> - - } - ; - })} - - - - - - ); -} - -DataGridView.propTypes = { - label: PropTypes.string, - value: PropTypes.array, - viewHelperProps: PropTypes.object, - schema: CustomPropTypes.schemaUI, - accessPath: PropTypes.array.isRequired, - dataDispatch: PropTypes.func, - containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), - fixedRows: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]), - columns: PropTypes.array, - canEdit: PropTypes.bool, - canAdd: PropTypes.bool, - canDelete: PropTypes.bool, - canReorder: PropTypes.bool, - visible: PropTypes.bool, - canAddRow: PropTypes.oneOfType([ - PropTypes.bool, PropTypes.func, - ]), - canEditRow: PropTypes.oneOfType([ - PropTypes.bool, PropTypes.func, - ]), - canDeleteRow: PropTypes.oneOfType([ - PropTypes.bool, PropTypes.func, - ]), - expandEditOnAdd: PropTypes.bool, - customDeleteTitle: PropTypes.string, - customDeleteMsg: PropTypes.string, - canSearch: PropTypes.bool, - onDelete: PropTypes.func, - addOnTop: PropTypes.bool -}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/SearchBox.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/SearchBox.jsx new file mode 100644 index 00000000000..a8dab903565 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/SearchBox.jsx @@ -0,0 +1,47 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext } from 'react'; + +import { + SEARCH_INPUT_ALIGNMENT, SEARCH_INPUT_SIZE, SearchInputText, +} from 'sources/components/SearchInputText'; + +import { SchemaStateContext } from '../SchemaState'; + +import { DataGridContext } from './context'; +import { GRID_STATE } from './utils'; + +export const SEARCH_STATE_PATH = [GRID_STATE, '__searchText']; + +export function SearchBox() { + const schemaState = useContext(SchemaStateContext); + const { + accessPath, field, options: { canSearch } + } = useContext(DataGridContext); + + if (!canSearch) return <>; + + const searchText = schemaState.state(accessPath.concat(SEARCH_STATE_PATH)); + const searchTextChange = (value) => { + schemaState.setState(accessPath.concat(SEARCH_STATE_PATH), value); + }; + + const searchOptions = field.searchOptions || { + size: SEARCH_INPUT_SIZE.HALF, + alignment: SEARCH_INPUT_ALIGNMENT.RIGHT, + }; + + return ( + + ); +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/context.js b/web/pgadmin/static/js/SchemaView/DataGridView/context.js new file mode 100644 index 00000000000..9cf54ec946c --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/context.js @@ -0,0 +1,14 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { createContext } from 'react'; + +export const DataGridContext = createContext(); +export const DataGridRowContext = createContext(); + diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/common.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/common.jsx new file mode 100644 index 00000000000..6c44275bf16 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/common.jsx @@ -0,0 +1,21 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; + +export const ACTION_COLUMN = { + header: <> , + accessorFn: ()=>{/*This is intentional (SonarQube)*/}, + enableResizing: false, + enableSorting: false, + dataType: 'reorder', + size: 36, + maxSize: 26, + minSize: 26, +}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/deletable.js b/web/pgadmin/static/js/SchemaView/DataGridView/features/deletable.js new file mode 100644 index 00000000000..7a18d508371 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/deletable.js @@ -0,0 +1,95 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; + +import { getDeleteCell } from 'sources/components/PgReactTableStyled'; +import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState'; +import gettext from 'sources/gettext'; + +import { + canAddOrDelete, evalIfNotDisabled, registerOptionEvaluator +} from '../../options'; + +import { SchemaStateContext } from '../../SchemaState'; +import { useFieldOptions } from '../../hooks'; +import { DataGridRowContext } from '../context'; +import { ACTION_COLUMN } from './common'; +import Feature from './feature'; + + +// Register the 'canDelete' options for the collection +registerOptionEvaluator('canDelete', canAddOrDelete, false, ['collection']); + +// Register the 'canDeleteRow' option for the table row +registerOptionEvaluator('canDeleteRow', evalIfNotDisabled, true, ['row']); + + +export default class DeletableRow extends Feature { + // Always add 'edit' column at the start of the columns list + // (but - not before the reorder column). + static priority = 50; + + constructor() { + super(); + this.canDelete = false; + } + + generateColumns({pgAdmin, columns, columnVisibility, options}) { + this.canDelete = options.canDelete; + + if (!this.canDelete) return; + + const instance = this; + const field = instance.field; + const accessPath = instance.accessPath; + const dataDispatch = instance.dataDispatch; + + columnVisibility['btn-delete'] = true; + + columns.splice(0, 0, { + ...ACTION_COLUMN, + id: 'btn-delete', + dataType: 'delete', + cell: getDeleteCell({ + isDisabled: () => { + const schemaState = React.useContext(SchemaStateContext); + const { rowAccessPath } = React.useContext(DataGridRowContext); + const options = useFieldOptions(rowAccessPath, schemaState); + + return !options.canDeleteRow; + }, + title: gettext('Delete row'), + onClick: (row) => { + const deleteRow = () => { + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.DELETE_ROW, + path: accessPath, + value: row.index, + }); + return true; + }; + + if (field.onDelete){ + field.onDelete(row?.original || {}, deleteRow); + } else { + pgAdmin.Browser.notifier.confirm( + field.customDeleteTitle || gettext('Delete Row'), + field.customDeleteMsg || gettext( + 'Are you sure you wish to delete this row?' + ), + deleteRow, + function() { return true; } + ); + } + }, + }), + }); + } +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx new file mode 100644 index 00000000000..57a03af2315 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx @@ -0,0 +1,90 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; +import { getExpandedRowModel } from '@tanstack/react-table'; + +import { getEditCell } from 'sources/components/PgReactTableStyled'; +import gettext from 'sources/gettext'; +import FormView from 'sources/SchemaView/FormView'; +import { requestAnimationAndFocus } from 'sources/utils'; + +import { SchemaStateContext } from '../../SchemaState'; +import { useFieldOptions } from '../../hooks'; +import { DataGridRowContext } from '../context'; +import { ACTION_COLUMN } from './common'; +import Feature from './feature'; + + +export default class ExpandedFormView extends Feature { + // Always add 'edit' column at the start of the columns list + // (but - not before the reorder column). + static priority = 70; + + constructor() { + super(); + this.canEdit = false; + } + + generateColumns({columns, columnVisibility, options}) { + this.canEdit = options.canEdit; + + if (!this.canEdit) return; + + columnVisibility['btn-edit'] = true; + + columns.splice(0, 0, { + ...ACTION_COLUMN, + id: 'btn-edit', + dataType: 'edit', + cell: getEditCell({ + isDisabled: () => { + const schemaState = React.useContext(SchemaStateContext); + const { rowAccessPath } = React.useContext(DataGridRowContext); + const options = useFieldOptions(rowAccessPath, schemaState); + + return !options.canEditRow; + }, + title: gettext('Edit row'), + }), + }); + } + + onTable({table}) { + table.setOptions(prev => ({ + ...prev, + getExpandedRowModel: getExpandedRowModel(), + state: { + ...prev.state, + } + })); + } + + onRow({row, expandedRowContents, rowOptions}) { + const instance = this; + + if (rowOptions.canEditRow && row?.getIsExpanded()) { + expandedRowContents.splice( + 0, 0, { + requestAnimationAndFocus(ele); + }}/> + ); + } + } +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/feature.js b/web/pgadmin/static/js/SchemaView/DataGridView/features/feature.js new file mode 100644 index 00000000000..888d91e9eea --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/feature.js @@ -0,0 +1,134 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +// Feature class + +// Let's not expose the features directory. +let _featureClasses = []; + +export default class Feature { + static priority = 1; + + constructor() { + this.accessPath = this.field = this.schema = this.table = this.cols = + this.viewHelperProps = null; + } + + setContext({ + accessPath, field, schema, viewHelperProps, dataDispatch, schemaState + }) { + this.accessPath = accessPath; + this.field = field; + this.schema = schema; + this.dataDispatch = dataDispatch; + this.viewHelperProps = viewHelperProps; + this.schemaState = schemaState; + } + + generateColumns(/* { pgAdmin, columns, columnVisibility, options } */) {} + onTable(/* { table, options, classList } */) {} + onRow(/* { + index, row, rowRef, classList, attributes, + expandedRowContents, rowOptions, tableOptions + } */) {} +} + +function isValidFeatureClass(cls) { + // Check if provided class is direct decendent of the Feature class + try { + if (Reflect.getPrototypeOf(cls) != Feature) { + console.error(cls, 'Not a valid Feature class:'); + console.trace(); + return false; + } + } catch(err) { + console.trace(); + console.error('Error while checking type:\n', err); + return false; + } + + return true; +} + +function addToSortedList(_list, _item, _comparator = (a, b) => (a < b)) { + // Insert the given feature class in sorted list based on the priority. + let idx = 0; + + for (; idx < _list.length; idx++) { + if (_comparator(_item, _list[idx])) { + _list.splice(idx, 0, _item); + return; + } + } + + _list.splice(idx, 0, _item); +} + +const featurePriorityCompare = (f1, f2) => (f1.priorty < f2.priority); + +export function register(cls) { + + if (!isValidFeatureClass(cls)) return; + + addToSortedList(_featureClasses, cls, featurePriorityCompare); +} + +export class FeatureSet { + constructor() { + this.id = Date.now(); + this.features = _featureClasses.map((cls) => new cls()); + } + + addFeatures(features) { + features.forEach((feature) => { + if (!(feature instanceof Feature)) { + console.error(feature, 'is not a valid feature!\n'); + console.trace(); + return; + } + addToSortedList( + this.features, feature, featurePriorityCompare + ); + }); + } + + setContext({ + accessPath, field, schema, viewHelperProps, dataDispatch, schemaState + }) { + this.features.forEach((feature) => { + feature.setContext({ + accessPath, field, schema, viewHelperProps, dataDispatch, schemaState + }); + }); + } + + generateColumns({pgAdmin, columns, columnVisibility, options}) { + this.features.forEach((feature) => { + feature.generateColumns({pgAdmin, columns, columnVisibility, options}); + }); + } + + onTable({table, options, classList}) { + this.features.forEach((feature) => { + feature.onTable({table, options, classList}); + }); + } + + onRow({ + index, row, rowRef, classList, attributes, expandedRowContents, + rowOptions, tableOptions + }) { + this.features.forEach((feature) => { + feature.onRow({ + index, row, rowRef, classList, attributes, expandedRowContents, + rowOptions, tableOptions + }); + }); + } +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/fixedrows.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/fixedrows.jsx new file mode 100644 index 00000000000..4916b40edc1 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/fixedrows.jsx @@ -0,0 +1,46 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useContext, useEffect } from 'react'; + +import { useIsMounted } from 'sources/custom_hooks'; +import { SchemaStateContext } from 'sources/SchemaView/SchemaState'; +import Feature from './feature'; + + +export default class FixedRows extends Feature { + + onTable() { + const instance = this; + const schemaState = useContext(SchemaStateContext); + const checkIsMounted = useIsMounted(); + + useEffect(() => { + let rowsPromise = instance.field.fixedRows; + + // Fixed rows is supported only in 'create' mode. + if (instance.viewHelperProps.mode !== 'create') return; + + // If fixedRows is defined, fetch the details. + if(typeof rowsPromise === 'function') { + rowsPromise = rowsPromise(); + } + + if(rowsPromise) { + Promise.resolve(rowsPromise) + .then((res) => { + // If component is unmounted, don't update state. + if(checkIsMounted()) { + schemaState.setUnpreparedData(instance.accessPath, res); + } + }); + } + }, []); + } +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx new file mode 100644 index 00000000000..d1dcc08909b --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx @@ -0,0 +1,29 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +// The DataGridView component is feature support better extendability. + +import { Feature, FeatureSet, register } from './feature'; +import FixedRows from './fixedrows'; +import Reorder from './reorder'; +import ExpandedFormView from './expandabledFormView'; +import DeletableRow from './deletable'; +import GlobalSearch from './search'; + +register(FixedRows); +register(DeletableRow); +register(ExpandedFormView); +register(GlobalSearch); +register(Reorder); + +export { + Feature, + FeatureSet, + register +}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/reorder.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/reorder.jsx new file mode 100644 index 00000000000..991fe96fcd6 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/reorder.jsx @@ -0,0 +1,160 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useRef } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded'; + +import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState'; + +import { booleanEvaluator, registerOptionEvaluator } from '../../options'; + +import { ACTION_COLUMN } from './common'; +import Feature from './feature'; + + +// Register the 'canReorder' options for the collection +registerOptionEvaluator('canReorder', booleanEvaluator, false, ['collection']); + +export default class Reorder extends Feature { + // Always add reorder column at the start of the columns list. + static priority = 100; + + constructor() { + super(); + this.canReorder = false; + this.hoverIndex = null; + this.moveRow = null; + } + + setHoverIndex(index) { + this.hoverIndex = index; + } + + generateColumns({columns, columnVisibility, options}) { + this.canReorder = options.canReorder; + + if (!this.canReorder) return; + + columnVisibility['reorder-cell'] = true; + + const Cell = function({row}) { + const dragHandleRef = row?.reorderDragHandleRef; + const handlerId = row?.dragHandlerId; + + if (!dragHandleRef) return <>; + + return ( +
+ +
+ ); + }; + + Cell.displayName = 'ReorderCell'; + + columns.splice(0, 0, { + ...ACTION_COLUMN, + id: 'btn-reorder', + dataType: 'reorder', + cell: Cell, + }); + } + + onTable() { + if (this.canReorder) { + this.moveRow = (dragIndex, hoverIndex) => { + this.dataDispatch?.({ + type: SCHEMA_STATE_ACTIONS.MOVE_ROW, + path: this.accessPath, + oldIndex: dragIndex, + newIndex: hoverIndex, + }); + }; + } + } + + onRow({index, row, rowRef, classList}) { + const instance = this; + const reorderDragHandleRef = useRef(null); + + const [{ handlerId }, drop] = useDrop({ + accept: 'row', + collect(monitor) { + return { + handlerId: monitor.getHandlerId(), + }; + }, + hover(item, monitor) { + if (!rowRef.current) return; + + item.hoverIndex = null; + // Don't replace items with themselves + if (item.index === index) return; + + // Determine rectangle on screen + const hoverBoundry = rowRef.current?.getBoundingClientRect(); + + // Determine mouse position + const clientOffset = monitor.getClientOffset(); + + // Get pixels to the top + const hoverClientY = clientOffset.y - hoverBoundry.top; + + // Only perform the move when the mouse has crossed certain part of the + // items height dragging downwards. + if ( + item.index < index && + hoverClientY < (hoverBoundry.bottom - hoverBoundry.top)/3 + ) return; + + // Dragging upwards + if ( + item.index > index && + hoverClientY > ((hoverBoundry.bottom - hoverBoundry.top) * 2 / 3) + ) return; + + instance.setHoverIndex(index); + item.hoverIndex = index; + }, + }); + + const [, drag, preview] = useDrag({ + type: 'row', + item: () => { + return {index}; + }, + end: (item) => { + // Time to actually perform the action + instance.setHoverIndex(null); + if(item.hoverIndex >= 0) { + instance.moveRow(item.index, item.hoverIndex); + } + } + }); + + if (!this.canReorder || !row) return; + + if (row) + row.reorderDragHandleRef = reorderDragHandleRef; + + drag(row.reorderDragHandleRef); + drop(rowRef); + preview(rowRef); + + if (index == this.hoverIndex) { + classList?.append('DataGridView-tableRowHovered'); + } + + row.dragHandlerId = handlerId; + } +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/search.js b/web/pgadmin/static/js/SchemaView/DataGridView/features/search.js new file mode 100644 index 00000000000..ef99186b3d3 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/search.js @@ -0,0 +1,54 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { + booleanEvaluator, registerOptionEvaluator +} from '../../options'; + +import Feature from './feature'; +import { SEARCH_STATE_PATH } from '../SearchBox'; + + +registerOptionEvaluator('canSearch', booleanEvaluator, false, ['collection']); + + +export default class GlobalSearch extends Feature { + constructor() { + super(); + } + + onTable({table, options}) { + + if (!options.canSearch) { + const searchText = ''; + + table.setOptions((prev) => ({ + ...prev, + state: { + ...prev.state, + globalFilter: searchText, + } + })); + + return; + } + + const searchText = this.schemaState.state( + this.accessPath.concat(SEARCH_STATE_PATH) + ); + + table.setOptions((prev) => ({ + ...prev, + state: { + ...prev.state, + globalFilter: searchText, + } + })); + } +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/formHeader.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/formHeader.jsx new file mode 100644 index 00000000000..bd5c3d55ed6 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/formHeader.jsx @@ -0,0 +1,166 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { + useCallback, useContext, useEffect, useRef, useState +} from 'react'; +import { styled } from '@mui/material/styles'; +import Box from '@mui/material/Box'; + +import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState'; +import { DefaultButton } from 'sources/components/Buttons'; +import CustomPropTypes from 'sources/custom_prop_types'; +import gettext from 'sources/gettext'; +import { requestAnimationAndFocus } from 'sources/utils'; + +import { SchemaStateContext } from '../SchemaState'; +import { booleanEvaluator, registerOptionEvaluator } from '../options'; +import { View } from '../registry'; + +import { SearchBox, SEARCH_STATE_PATH } from './SearchBox'; +import { DataGridContext } from './context'; + + +// Register the 'headerFormVisible' options for the collection +registerOptionEvaluator( + 'headerFormVisible', booleanEvaluator, false, ['collection'] +); + +const StyledBox = styled(Box)(({theme}) => ({ + '& .DataGridFormHeader-border': { + ...theme.mixins.panelBorder, + borderBottom: 0, + '& .DataGridFormHeader-body': { + padding: '0', + backgroundColor: theme.palette.grey[400], + '& .FormView-singleCollectionPanel': { + paddingBottom: 0, + }, + '& .DataGridFormHeader-btn-group' :{ + display: 'flex', + padding: theme.spacing(1), + paddingTop: 0, + '& .DataGridFormHeader-addBtn': { + marginLeft: 'auto', + }, + }, + '& [data-test="tabpanel"]': { + overflow: 'unset', + }, + }, + }, +})); + +export function DataGridFormHeader({tableEleRef}) { + + const { + accessPath, field, dataDispatch, options, virtualizer, table, + viewHelperProps, + } = useContext(DataGridContext); + const { + addOnTop, canAddRow, canEdit, expandEditOnAdd, headerFormVisible + } = options; + const rows = table.getRowModel().rows; + + const label = field.label || ''; + const newRowIndex = useRef(-1); + const schemaState = useContext(SchemaStateContext); + const headerFormData = useRef({}); + const [addDisabled, setAddDisabled] = useState(canAddRow); + const {headerSchema} = field; + + const onAddClick = useCallback(() => { + + if(!canAddRow) { + return; + } + + let newRow = headerSchema.getNewData(headerFormData.current); + + newRowIndex.current = addOnTop ? 0 : rows.length; + + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.ADD_ROW, + path: accessPath, + value: newRow, + addOnTop: addOnTop + }); + + schemaState.setState(accessPath.concat(SEARCH_STATE_PATH), ''); + headerSchema.state?.validate(headerSchema._defaults || {}); + }, [canAddRow, rows?.length, addOnTop]); + + useEffect(() => { + if (newRowIndex.current < -1) return; + + virtualizer.scrollToIndex(newRowIndex.current); + + // Try autofocus on newly added row. + setTimeout(() => { + const rowInput = tableEleRef.current?.querySelector( + `.pgrt-row[data-index="${newRowIndex.current}"] input` + ); + + if(!rowInput) return; + + requestAnimationAndFocus(tableEleRef.current.querySelector( + `.pgrt-row[data-index="${newRowIndex.current}"] input` + )); + + expandEditOnAdd && canEdit && + rows[newRowIndex.current]?.toggleExpanded(true); + + newRowIndex.current = undefined; + }, 50); + }, [rows?.length]); + + const SchemaView = View('SchemaView'); + + return ( + + + + {label && {label}} + + + + + {headerFormVisible && + + Promise.resolve({})} + schema={headerSchema} + viewHelperProps={viewHelperProps} + showFooter={false} + onDataChange={(isDataChanged, dataChanged)=>{ + headerFormData.current = dataChanged; + setAddDisabled( + canAddRow && headerSchema.addDisabled(headerFormData.current) + ); + }} + hasSQL={false} + isTabView={false} + /> + + + {gettext('Add')} + + + } + + + ); +} + +DataGridFormHeader.propTypes = { + tableEleRef: CustomPropTypes.ref, +}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx new file mode 100644 index 00000000000..9c8ff2baa43 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx @@ -0,0 +1,198 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { + useContext, useEffect, useMemo, useRef, useState, +} from 'react'; + +import Box from '@mui/material/Box'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getFilteredRowModel, +} from '@tanstack/react-table'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import { DndProvider } from 'react-dnd'; +import {HTML5Backend} from 'react-dnd-html5-backend'; + +import { usePgAdmin } from 'sources/BrowserComponent'; +import { + PgReactTable, PgReactTableBody, PgReactTableHeader, + PgReactTableRow, +} from 'sources/components/PgReactTableStyled'; +import CustomPropTypes from 'sources/custom_prop_types'; + +import { StyleDataGridBox } from '../StyledComponents'; +import { SchemaStateContext } from '../SchemaState'; +import { useFieldOptions, useFieldValue } from '../hooks'; +import { registerView } from '../registry'; +import { listenDepChanges } from '../utils'; + +import { DataGridContext } from './context'; +import { DataGridHeader } from './header'; +import { DataGridRow } from './row'; +import { FeatureSet } from './features'; +import { createGridColumns, GRID_STATE } from './utils'; + + +export default function DataGridView({ + field, viewHelperProps, accessPath, dataDispatch, containerClassName +}) { + const pgAdmin = usePgAdmin(); + const [refreshKey, setRefreshKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const options = useFieldOptions( + accessPath, schemaState, refreshKey, setRefreshKey + ); + const value = useFieldValue(accessPath, schemaState); + const schema = field.schema; + const features = useRef(); + + // Update refresh key on changing the number of rows. + useFieldValue( + [...accessPath, 'length'], schemaState, refreshKey, + (newKey) => { + setRefreshKey(newKey); + } + ); + + useEffect(() => { + return schemaState.subscribe( + accessPath.concat(GRID_STATE), + () => setRefreshKey(Date.now()), 'states' + ); + }, [refreshKey]); + + listenDepChanges(accessPath, field, options.visible, schemaState); + + if (!features.current) { + features.current = new FeatureSet(); + }; + + features.current.setContext({ + accessPath, field, schema: schema, dataDispatch, viewHelperProps, + schemaState, + }); + + const [columns, columnVisibility] = useMemo(() => { + + const [columns, columnVisibility] = createGridColumns({ + schema, field, accessPath, viewHelperProps, dataDispatch, + }); + + features.current?.generateColumns({ + pgAdmin, columns, columnVisibility, options + }); + + return [columns, columnVisibility]; + + }, [options]); + + const table = useReactTable({ + columns: columns|| [], + data: value || [], + autoResetAll: false, + state: { + columnVisibility: columnVisibility || {}, + }, + columnResizeMode: 'onChange', + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + const classList = [].concat(containerClassName); + features.current?.onTable({table, classList, options}); + + const rows = table.getRowModel().rows; + const tableEleRef = useRef(); + + const isResizing = _.flatMap( + table.getHeaderGroups(), + headerGroup => headerGroup.headers.map( + header => header.column.getIsResizing() + ) + ).includes(true); + + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableEleRef.current, + estimateSize: () => 50, + measureElement: + typeof window !== 'undefined' && + navigator.userAgent.indexOf('Firefox') === -1 + ? element => element?.getBoundingClientRect().height + : undefined, + overscan: viewHelperProps.virtualiseOverscan ?? 10, + }); + + const GridHeader = field.GridHeader || DataGridHeader; + const GridRow = field.GridRow || DataGridRow; + + if (!options.visible) return (<>); + + return ( + + + + + + + + + { + virtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow.index]; + return ( + virtualizer.measureElement(node)} + style={{ + // This should always be a `style` as it changes on + // scroll. + transform: `translateY(${virtualRow.start}px)`, + }} + > + + + ); + }) + } + + + + + + + ); +} + +DataGridView.propTypes = { + viewHelperProps: PropTypes.object, + schema: CustomPropTypes.schemaUI, + accessPath: PropTypes.array.isRequired, + dataDispatch: PropTypes.func, + containerClassName: PropTypes.oneOfType([ + PropTypes.object, PropTypes.string + ]), + field: PropTypes.object, +}; + +registerView(DataGridView); diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx new file mode 100644 index 00000000000..80c305b1031 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx @@ -0,0 +1,103 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useCallback, useContext, useEffect, useRef } from 'react'; +import Box from '@mui/material/Box'; +import AddIcon from '@mui/icons-material/AddOutlined'; + +import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState'; +import { PgIconButton } from 'sources/components/Buttons'; +import CustomPropTypes from 'sources/custom_prop_types'; +import gettext from 'sources/gettext'; +import { requestAnimationAndFocus } from 'sources/utils'; + +import { SchemaStateContext } from '../SchemaState'; +import { SearchBox, SEARCH_STATE_PATH } from './SearchBox'; +import { DataGridContext } from './context'; + + +export function DataGridHeader({tableEleRef}) { + const { + accessPath, field, dataDispatch, options, virtualizer, table, + } = useContext(DataGridContext); + const { + addOnTop, canAdd, canAddRow, canEdit, expandEditOnAdd + } = options; + const rows = table.getRowModel().rows; + + const label = field.label || ''; + const newRowIndex = useRef(-1); + const schemaState = useContext(SchemaStateContext); + + const onAddClick = useCallback(() => { + + if(!canAddRow) { + return; + } + + const newRow = field.schema.getNewData(); + + newRowIndex.current = addOnTop ? 0 : rows.length; + + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.ADD_ROW, + path: accessPath, + value: newRow, + addOnTop: addOnTop + }); + + schemaState.setState(accessPath.concat(SEARCH_STATE_PATH), ''); + }, [canAddRow, rows?.length]); + + useEffect(() => { + if (newRowIndex.current < -1) return; + + virtualizer.scrollToIndex(newRowIndex.current); + + // Try autofocus on newly added row. + setTimeout(() => { + const rowInput = tableEleRef.current?.querySelector( + `.pgrt-row[data-index="${newRowIndex.current}"] input` + ); + + if(!rowInput) return; + + requestAnimationAndFocus(tableEleRef.current.querySelector( + `.pgrt-row[data-index="${newRowIndex.current}"] input` + )); + + expandEditOnAdd && canEdit && + rows[newRowIndex.current]?.toggleExpanded(true); + + newRowIndex.current = undefined; + }, 50); + }, [rows?.length]); + + return ( + + {label && {label}} + + + + + { canAdd && + } className='DataGridView-gridControlsButton' + /> + } + + + ); +} + +DataGridHeader.propTypes = { + tableEleRef: CustomPropTypes.ref, +}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/index.js b/web/pgadmin/static/js/SchemaView/DataGridView/index.js new file mode 100644 index 00000000000..40206f1cbc3 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/index.js @@ -0,0 +1,24 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +/* The DataGridView component is based on react-table component */ + +import { DataGridFormHeader } from './formHeader.jsx'; +import { DataGridHeader } from './header'; +import { getMappedCell } from './mappedCell'; +import DataGridView from './grid'; + + +export default DataGridView; + +export { + DataGridFormHeader, + DataGridHeader, + getMappedCell, +}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx new file mode 100644 index 00000000000..05f002cab8a --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx @@ -0,0 +1,111 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext, useMemo, useState } from 'react'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; + +import { evalFunc } from 'sources/utils'; + +import { MappedCellControl } from '../MappedControl'; +import { SCHEMA_STATE_ACTIONS, SchemaStateContext } from '../SchemaState'; +import { flatternObject } from '../common'; +import { useFieldOptions, useFieldValue } from '../hooks'; +import { listenDepChanges } from '../utils'; + +import { DataGridContext, DataGridRowContext } from './context'; + + +export function getMappedCell({field}) { + const Cell = ({reRenderRow, getValue}) => { + + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const { dataDispatch } = useContext(DataGridContext); + const { rowAccessPath, row } = useContext(DataGridRowContext); + + // When cell is dynamic, it should be rerendered on change of row value. + const refreshOnRowChange = ( + (field.id && _.isFunction(field.cell)) ? [key, setKey] : [] + ); + + const colAccessPath = schemaState.accessPath(rowAccessPath, field.id); + let colOptions = useFieldOptions(colAccessPath, schemaState, key, setKey); + let value = useFieldValue(colAccessPath, schemaState, key, setKey); + let rowValue = useFieldValue( + rowAccessPath, schemaState, ...refreshOnRowChange + ); + + listenDepChanges(colAccessPath, field, true, schemaState); + + if (!field.id) { + console.error(`No id set for the field: ${field}`); + value = getValue(); + rowValue = row.original; + colOptions = { disabled: true, readonly: true }; + } else { + colOptions['readonly'] = !colOptions['editable']; + } + + let cellProps = {}; + + if (_.isFunction(field.cell) && field.id) { + cellProps = evalFunc(null, field.cell, rowValue); + + if (typeof (cellProps) !== 'object') + cellProps = {cell: cellProps}; + } + + const props = { + ...field, + ...cellProps, + ...colOptions, + visible: true, + rowIndex: row.index, + value, + row, + dataDispatch, + onCellChange: (changeValue) => { + if (colOptions.disabled) return; + if(field.radioType) { + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.BULK_UPDATE, + path: rowAccessPath, + value: changeValue, + id: field.id + }); + } + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + path: colAccessPath, + value: changeValue, + }); + }, + reRenderRow: reRenderRow + }; + + if(_.isUndefined(field.cell)) { + console.error('cell is required ', field); + props.cell = 'unknown'; + } + + return useMemo( + () => , + [...flatternObject(colOptions), value, row.index] + ); + }; + + Cell.displayName = 'Cell'; + Cell.propTypes = { + reRenderRow: PropTypes.func, + getValue: PropTypes.func, + }; + + return Cell; +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx new file mode 100644 index 00000000000..7c6ce2ac213 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -0,0 +1,98 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext, useMemo, useRef, useState } from 'react'; + +import { flexRender } from '@tanstack/react-table'; + +import { + PgReactTableCell, PgReactTableRowContent, PgReactTableRowExpandContent, +} from 'sources/components/PgReactTableStyled'; + +import { SchemaStateContext } from '../SchemaState'; +import { useFieldOptions } from '../hooks'; +import { listenDepChanges } from '../utils'; + +import { DataGridContext, DataGridRowContext } from './context'; + + +export function DataGridRow({rowId, isResizing}) { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + + const { accessPath, field, options, table, features } = useContext( + DataGridContext + ); + + const rowAccessPath = schemaState.accessPath(accessPath, rowId); + const rowOptions = useFieldOptions(rowAccessPath, schemaState, key, setKey); + + const rowRef = useRef(null); + const row = table.getRowModel().rows[rowId]; + + listenDepChanges(rowAccessPath, field, true, schemaState); + + /* + * Memoize the row to avoid unnecessary re-render. If table data changes, + * then react-table re-renders the complete tables. + * + * We can avoid re-render by if row data has not changed. + */ + let depsMap = [JSON.stringify(row?.original)]; + let classList = []; + let attributes = {}; + let expandedRowContents = []; + + features.current?.onRow({ + index: rowId, row, rowRef, classList, attributes, expandedRowContents, + rowOptions, tableOptions: options + }); + + depsMap = depsMap.concat([ + row?.getIsExpanded(), key, isResizing, expandedRowContents.length + ]); + + return useMemo(() => ( + !row ? <> : + + + { + row?.getVisibleCells().map((cell) => { + const columnDef = cell.column.columnDef; + const content = flexRender( + columnDef.cell, { + key: columnDef.cell?.type ?? columnDef.id, + row: row, + getValue: cell.getValue, + } + ); + + return ( + + {content} + + ); + }) + } +
+
+ { + expandedRowContents.length ? + + {expandedRowContents} + : <> + } +
+ ), depsMap); +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/utils/createGridColumns.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/utils/createGridColumns.jsx new file mode 100644 index 00000000000..954ecec8748 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/utils/createGridColumns.jsx @@ -0,0 +1,68 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; + +import { isModeSupportedByField } from 'sources/SchemaView/common'; +import { getMappedCell } from '../mappedCell'; + + +export function createGridColumns({schema, field, viewHelperProps}) { + + const columns = field.columns; + const colunnFilterExp = _.isArray(columns) ? + ((f) => (columns.indexOf(f.id) > -1)) : (() => true); + const sortExp = _.isArray(columns) ? + ((firstF, secondF) => ( + (columns.indexOf(firstF.id) < columns.indexOf(secondF.id)) ? -1 : 1 + )) : (() => 0); + const columnVisibility = {}; + + const cols = schema.fields.filter(colunnFilterExp).sort(sortExp).map( + (field) => { + let widthParms = {}; + + if(field.width) { + widthParms.size = field.width; + widthParms.minSize = field.width; + } else { + widthParms.size = 75; + widthParms.minSize = 75; + } + + if(field.minWidth) { + widthParms.minSize = field.minWidth; + } + + if(field.maxWidth) { + widthParms.maxSize = field.maxWidth; + } + + widthParms.enableResizing = + _.isUndefined(field.enableResizing) ? true : Boolean( + field.enableResizing + ); + columnVisibility[field.id] = isModeSupportedByField( + field, viewHelperProps + ); + + return { + header: field.label||<> , + accessorKey: field.id, + field: field, + enableResizing: true, + enableSorting: false, + ...widthParms, + cell: getMappedCell({field}), + }; + } + ); + + return [cols, columnVisibility]; +} diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/utils/index.js b/web/pgadmin/static/js/SchemaView/DataGridView/utils/index.js new file mode 100644 index 00000000000..5b8a481855a --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/utils/index.js @@ -0,0 +1,16 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { createGridColumns } from './createGridColumns'; + +export const GRID_STATE = '__gridState'; + +export { + createGridColumns, +}; diff --git a/web/pgadmin/static/js/SchemaView/DepListener.js b/web/pgadmin/static/js/SchemaView/DepListener.js index b8ea2344a72..fe7eaeb7548 100644 --- a/web/pgadmin/static/js/SchemaView/DepListener.js +++ b/web/pgadmin/static/js/SchemaView/DepListener.js @@ -37,7 +37,10 @@ export class DepListener { if(dataPath.length > 0) { data = _.get(state, dataPath); } - _.assign(data, listener.callback?.(data, listener.source, state, actionObj) || {}); + _.assign( + data, + listener.callback?.(data, listener.source, state, actionObj) || {} + ); return state; } @@ -70,7 +73,10 @@ export class DepListener { getDeferredDepChange(currPath, state, actionObj) { let deferredList = []; - let allListeners = _.filter(this._depListeners, (entry)=>_.join(currPath, '|').startsWith(_.join(entry.source, '|'))); + let allListeners = _.filter(this._depListeners, (entry) => _.join( + currPath, '|' + ).startsWith(_.join(entry.source, '|'))); + if(allListeners) { for(const listener of allListeners) { if(listener.defCallback) { diff --git a/web/pgadmin/static/js/SchemaView/FieldControl.jsx b/web/pgadmin/static/js/SchemaView/FieldControl.jsx new file mode 100644 index 00000000000..a5358b93115 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/FieldControl.jsx @@ -0,0 +1,27 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useMemo } from 'react'; + +export const FieldControl = ({schemaId, item}) => { + const Control = item.control; + const props = item.controlProps; + const children = item.controls; + + return useMemo(() => + + { + children && + children.map( + (child, idx) => + ) + } + , [schemaId, Control, props, children] + ); +}; diff --git a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx index 0812ef133a6..6bdad064fc9 100644 --- a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx +++ b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx @@ -7,156 +7,70 @@ // ////////////////////////////////////////////////////////////// -import React, { useContext, useEffect } from 'react'; - -import Grid from '@mui/material/Grid'; -import _ from 'lodash'; +import React, { useContext, useMemo } from 'react'; import PropTypes from 'prop-types'; import FieldSet from 'sources/components/FieldSet'; import CustomPropTypes from 'sources/custom_prop_types'; -import { evalFunc } from 'sources/utils'; - -import { MappedFormControl } from './MappedControl'; -import { - getFieldMetaData, SCHEMA_STATE_ACTIONS, SchemaStateContext -} from './common'; +import { FieldControl } from './FieldControl'; +import { SchemaStateContext } from './SchemaState'; +import { useFieldOptions } from './hooks'; +import { registerView } from './registry'; +import { createFieldControls, listenDepChanges } from './utils'; -const INLINE_COMPONENT_ROWGAP = '8px'; export default function FieldSetView({ - value, schema={}, viewHelperProps, accessPath, dataDispatch, - controlClassName, isDataGridForm=false, label, visible + field, accessPath, dataDispatch, viewHelperProps, controlClassName, }) { + const schema = field.schema; const schemaState = useContext(SchemaStateContext); + const options = useFieldOptions(accessPath, schemaState); + const label = field.label; - useEffect(() => { - // Calculate the fields which depends on the current field. - if(!isDataGridForm && schemaState) { - schema.fields.forEach((field) => { - /* Self change is also dep change */ - if(field.depChange || field.deferredDepChange) { - schemaState?.addDepListener( - accessPath.concat(field.id), accessPath.concat(field.id), - field.depChange, field.deferredDepChange - ); - } - (evalFunc(null, field.deps) || []).forEach((dep) => { - let source = accessPath.concat(dep); - if(_.isArray(dep)) { - source = dep; - } - if(field.depChange) { - schemaState?.addDepListener( - source, accessPath.concat(field.id), field.depChange - ); - } - }); - }); - } - }, []); + listenDepChanges(accessPath, field, options.visible, schemaState); - let viewFields = []; - let inlineComponents = []; + const fieldGroups = useMemo( + () => createFieldControls({ + schema, schemaState, accessPath, viewHelperProps, dataDispatch + }), + [schema, schemaState, accessPath, viewHelperProps, dataDispatch] + ); - if(!visible) { + // We won't show empty feldset too. + if(!options.visible || !fieldGroups.length) { return <>; } - // Prepare the array of components based on the types. - for(const field of schema.fields) { - const { - visible, disabled, readonly, modeSupported - } = getFieldMetaData(field, schema, value, viewHelperProps); - - if(!modeSupported) continue; - - // Its a form control. - const hasError = (field.id === schemaState?.errors.name); - - /* - * When there is a change, the dependent values can also change. - * Let's pass these changes to dependent for take them into effect to - * generate new values. - */ - const currentControl = { - /* Get the changes on dependent fields as well */ - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.SET_VALUE, - path: accessPath.concat(field.id), - value: changeValue, - }); - }} - hasError={hasError} - className={controlClassName} - memoDeps={[ - value[field.id], - readonly, - disabled, - visible, - hasError, - controlClassName, - ...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]), - ]} - />; - - if(field.inlineNext) { - inlineComponents.push(React.cloneElement(currentControl, { - withContainer: false, controlGridBasis: 3 - })); - } else if(inlineComponents?.length > 0) { - inlineComponents.push(React.cloneElement(currentControl, { - withContainer: false, controlGridBasis: 3 - })); - viewFields.push( - - {inlineComponents} - - ); - inlineComponents = []; - } else { - viewFields.push(currentControl); - } - } - - if(inlineComponents?.length > 0) { - viewFields.push( - - {inlineComponents} - + if (fieldGroups.length > 1) { + throw new Error( + 'Developers: Avoid using multiple groups within a fieldSet.' + + JSON.stringify(field?.id) + JSON.stringify(fieldGroups) ); } return (
- {viewFields} + {fieldGroups.map( + (fieldGroup, gidx) => ( + + {fieldGroup.controls.map( + (item, idx) => + )} + + ) + )}
); } FieldSetView.propTypes = { - value: PropTypes.any, - schema: CustomPropTypes.schemaUI.isRequired, viewHelperProps: PropTypes.object, - isDataGridForm: PropTypes.bool, accessPath: PropTypes.array.isRequired, dataDispatch: PropTypes.func, controlClassName: CustomPropTypes.className, - label: PropTypes.string, - visible: PropTypes.oneOfType([ - PropTypes.bool, PropTypes.func, - ]), + field: PropTypes.object, }; + +registerView(FieldSetView); diff --git a/web/pgadmin/static/js/SchemaView/FormLoader.jsx b/web/pgadmin/static/js/SchemaView/FormLoader.jsx new file mode 100644 index 00000000000..ee9af8d6bcb --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/FormLoader.jsx @@ -0,0 +1,30 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext, useEffect, useState, useMemo } from 'react'; + +import Loader from 'sources/components/Loader'; + +import { SchemaStateContext } from './SchemaState'; + + +export const FormLoader = () => { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const message = schemaState.loadingMessage; + + useEffect(() => { + // Refresh on message changes. + return schemaState.subscribe( + ['message'], () => setKey(Date.now()), 'states' + ); + }, [key]); + + return useMemo(() => , [message, key]); +}; diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index b43ee505a68..5f7e77c161e 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -8,80 +8,86 @@ ////////////////////////////////////////////////////////////// import React, { - useContext, useEffect, useMemo, useRef, useState + useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { Box, Tab, Tabs, Grid } from '@mui/material'; +import { Box, Tab, Tabs } from '@mui/material'; import _ from 'lodash'; import PropTypes from 'prop-types'; -import { FormNote, InputSQL } from 'sources/components/FormComponents'; + +import { + FormFooterMessage, MESSAGE_TYPE, FormNote +} from 'sources/components/FormComponents'; import TabPanel from 'sources/components/TabPanel'; import { useOnScreen } from 'sources/custom_hooks'; import CustomPropTypes from 'sources/custom_prop_types'; import gettext from 'sources/gettext'; -import { evalFunc } from 'sources/utils'; - -import DataGridView from './DataGridView'; -import { MappedFormControl } from './MappedControl'; -import FieldSetView from './FieldSetView'; -import { - SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData -} from './common'; +import { FieldControl } from './FieldControl'; +import { SQLTab } from './SQLTab'; import { FormContentBox } from './StyledComponents'; +import { SchemaStateContext } from './SchemaState'; +import { useFieldOptions } from './hooks'; +import { registerView, View } from './registry'; +import { createFieldControls, listenDepChanges } from './utils'; +const ErrorMessageBox = () => { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const onErrClose = useCallback(() => { + const err = { ...schemaState.errors, message: '' }; + // Unset the error message, but not the name. + schemaState.setError(err); + }, [schemaState]); + const errors = schemaState.errors; + const message = errors?.message || ''; -/* Optional SQL tab */ -function SQLTab({active, getSQLValue}) { - const [sql, setSql] = useState('Loading...'); - useEffect(()=>{ - let unmounted = false; - if(active) { - setSql('Loading...'); - getSQLValue().then((value)=>{ - if(!unmounted) { - setSql(value); - } - }); - } - return ()=>{unmounted=true;}; - }, [active]); + useEffect(() => { + // Refresh on message changes. + return schemaState.subscribe( + ['errors', 'message'], () => setKey(Date.now()), 'states' + ); + }, [key]); - return ; -} - -SQLTab.propTypes = { - active: PropTypes.bool, - getSQLValue: PropTypes.func.isRequired, }; -/* The first component of schema view form */ +// The first component of schema view form. export default function FormView({ - value, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab, - getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) { - let defaultTab = gettext('General'); - let tabs = {}; - let tabsClassname = {}; - const [tabValue, setTabValue] = useState(0); + accessPath, schema=null, isNested=false, dataDispatch, className, + hasSQLTab, getSQLValue, isTabView=true, viewHelperProps, field, + showError=false +}) { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const { visible } = useFieldOptions(accessPath, schemaState); - const firstEleID = useRef(); + const [tabValue, setTabValue] = useState(0); const formRef = useRef(); const onScreenTracker = useRef(false); - let groupLabels = {}; - const schemaRef = useRef(schema); - const schemaState = useContext(SchemaStateContext); - let isOnScreen = useOnScreen(formRef); - useEffect(()=>{ + if (!schema) schema = field.schema; + + useEffect(() => { + // Refresh on message changes. + return schemaState.subscribe( + ['errors', 'message'], + (newState, prevState) => { + if (_.isUndefined(newState) || _.isUndefined(prevState)); + setKey(Date.now()); + }, + 'states' + ); + }, [key]); + + + useEffect(() => { + if (!visible) return; + if(isOnScreen) { /* Don't do it when the form is alredy visible */ if(!onScreenTracker.current) { @@ -93,347 +99,181 @@ export default function FormView({ onScreenTracker.current = false; } }, [isOnScreen]); + + listenDepChanges(accessPath, field, visible, schemaState); - useEffect(()=>{ - /* Calculate the fields which depends on the current field */ - if(!isDataGridForm) { - schemaRef.current.fields.forEach((field)=>{ - /* Self change is also dep change */ - if(field.depChange || field.deferredDepChange) { - schemaState?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange); - } - (evalFunc(null, field.deps) || []).forEach((dep)=>{ - // when dep is a string then prepend the complete accessPath - let source = accessPath.concat(dep); - - // but when dep is an array, then the intention is to provide the exact accesspath - if(_.isArray(dep)) { - source = dep; - } - if(field.depChange || field.deferredDepChange) { - schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange); - } - if(field.depChange || field.deferredDepChange) { - schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange); - } - }); - }); - return ()=>{ - /* Cleanup the listeners when unmounting */ - schemaState?.removeDepListener(accessPath); - }; - } - }, []); - - /* Upon reset, set the tab to first */ - useEffect(()=>{ + // Upon reset, set the tab to first. + useEffect(() => { + if (!visible) return; if (schemaState?.isReady) setTabValue(0); }, [schemaState?.isReady]); - let fullTabs = []; - let inlineComponents = []; - let inlineCompGroup = null; - - /* Prepare the array of components based on the types */ - for(const field of schemaRef.current.fields) { - let { - visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder, - canAddRow, modeSupported - } = getFieldMetaData(field, schema, value, viewHelperProps); - - if(!modeSupported) continue; - - let {group, CustomControl} = field; - - if(field.type === 'group') { - groupLabels[field.id] = field.label; - - if(!visible) { - schemaRef.current.filterGroups.push(field.label); - } - continue; - } - - group = groupLabels[group] || group || defaultTab; - - if(!tabs[group]) tabs[group] = []; - - // Lets choose the path based on type. - if(field.type === 'nested-tab') { - /* Pass on the top schema */ - if(isNested) { - field.schema.top = schemaRef.current.top; - } else { - field.schema.top = schema; - } - tabs[group].push( - - ); - } else if(field.type === 'nested-fieldset') { - /* Pass on the top schema */ - if(isNested) { - field.schema.top = schemaRef.current.top; - } else { - field.schema.top = schema; - } - tabs[group].push( - - ); - } else if(field.type === 'collection') { - /* If its a collection, let data grid view handle it */ - /* Pass on the top schema */ - if(isNested) { - field.schema.top = schemaRef.current.top; - } else { - field.schema.top = schemaRef.current; - } - - if(!_.isUndefined(field.fixedRows)) { - canAdd = false; - canDelete = false; - } - - const ctrlProps = { - key: field.id, ...field, - value: value[field.id] || [], viewHelperProps: viewHelperProps, - schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch, - containerClassName: 'FormView-controlRow', - canAdd: canAdd, canReorder: canReorder, - canEdit: canEdit, canDelete: canDelete, - visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch, - expandEditOnAdd: field.expandEditOnAdd, - fixedRows: (viewHelperProps.mode == 'create' ? field.fixedRows : undefined), - addOnTop: Boolean(field.addOnTop) - }; - - if(CustomControl) { - tabs[group].push(); - } else { - tabs[group].push(); - } - } else { - /* Its a form control */ - const hasError = _.isEqual( - accessPath.concat(field.id), schemaState.errors?.name - ); - /* When there is a change, the dependent values can change - * lets pass the new changes to dependent and get the new values - * from there as well. - */ - if(field.isFullTab) { - tabsClassname[group] ='FormView-fullSpace'; - fullTabs.push(group); - } - - const id = field.id || `control${tabs[group].length}`; - if(visible && !disabled && !firstEleID.current) { - firstEleID.current = field.id; - } - - let currentControl = { - if(firstEleRef && firstEleID.current === field.id) { - if(typeof firstEleRef == 'function') { - firstEleRef(ele); - } else { - firstEleRef.current = ele; - } - } - }} - state={value} - key={id} - viewHelperProps={viewHelperProps} - name={id} - value={value[id]} - {...field} - id={id} - readonly={readonly} - disabled={disabled} - visible={visible} - onChange={(changeValue)=>{ - /* Get the changes on dependent fields as well */ - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.SET_VALUE, - path: accessPath.concat(id), - value: changeValue, - }); - }} - hasError={hasError} - className='FormView-controlRow' - noLabel={field.isFullTab} - memoDeps={[ - value[id], - readonly, - disabled, - visible, - hasError, - 'FormView-controlRow', - ...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]), - ]} - />; - - if(field.isFullTab && field.helpMessage) { - currentControl = ( - - {currentControl} - ); - } - - if(field.inlineNext) { - inlineComponents.push(React.cloneElement(currentControl, { - withContainer: false, controlGridBasis: 3 - })); - inlineCompGroup = group; - } else if(inlineComponents?.length > 0) { - inlineComponents.push(React.cloneElement(currentControl, { - withContainer: false, controlGridBasis: 3 - })); - tabs[group].push( - - {inlineComponents} - - ); - inlineComponents = []; - inlineCompGroup = null; - } else { - tabs[group].push(currentControl); - } - } - } - - if(inlineComponents?.length > 0) { - tabs[inlineCompGroup].push( - - {inlineComponents} - - ); - } - - let finalTabs = _.pickBy( - tabs, (v, tabName) => schemaRef.current.filterGroups.indexOf(tabName) <= -1 + const finalGroups = useMemo( + () => createFieldControls({ + schema, schemaState, accessPath, viewHelperProps, dataDispatch + }), + [schema, schemaState, accessPath, viewHelperProps, dataDispatch] ); - // Add the SQL tab (if required) - let sqlTabActive = false; - let sqlTabName = gettext('SQL'); - - if(hasSQLTab) { - sqlTabActive = (Object.keys(finalTabs).length === tabValue); - // Re-render and fetch the SQL tab when it is active. - finalTabs[sqlTabName] = [ - , - ]; - tabsClassname[sqlTabName] = 'FormView-fullSpace'; - fullTabs.push(sqlTabName); - } - - useEffect(() => { - onTabChange?.(tabValue, Object.keys(tabs)[tabValue], sqlTabActive); - }, [tabValue]); - - const isSingleCollection = useMemo(()=>{ - // we can check if it is a single-collection. - // in that case, we could force virtualization of the collection. - if(isTabView) return false; - - const visibleEle = Object.values(finalTabs)[0].filter( - (c) => c.props.visible - ); - return visibleEle.length == 1 && visibleEle[0]?.type == DataGridView; - }, [isTabView, finalTabs]); - // Check whether form is kept hidden by visible prop. - if(!_.isUndefined(visible) && !visible) { + if(!finalGroups || (!_.isUndefined(visible) && !visible)) { return <>; } - if(isTabView) { + const isSingleCollection = () => { + const DataGridView = View('DataGridView'); return ( - - - { setTabValue(selTabValue); }} - variant="scrollable" - scrollButtons="auto" - action={(ref)=>ref?.updateIndicator()} - > - {Object.keys(finalTabs).map((tabName)=>{ - return ; - })} - - - {Object.keys(finalTabs).map((tabName, i)=>{ - let contentClassName = [( - schemaState.errors?.message ? 'FormView-errorMargin': null - )]; + finalGroups.length == 1 && + finalGroups[0].controls.length == 1 && + finalGroups[0].controls[0].control == DataGridView + ); + }; - if(fullTabs.indexOf(tabName) == -1) { - contentClassName.push('FormView-nestedControl'); - } else { - contentClassName.push('FormView-fullControl'); + if(isTabView) { + return ( + <> + + + { setTabValue(nextTabIndex); }} + variant="scrollable" + scrollButtons="auto" + action={(ref) => ref?.updateIndicator()} + >{ + finalGroups.map((tabGroup, idx) => + + ) + }{hasSQLTab && + + } + + { + finalGroups.map((group, idx) => { + let contentClassName = [ + group.isFullTab ? + 'FormView-fullControl' : 'FormView-nestedControl', + schemaState.errors?.message ? 'FormView-errorMargin' : null + ]; + + let id = group.id.replace(' ', ''); + + return ( + + { + group.isFullTab && group.field?.helpMessage ? + : + <> + } + { + group.controls.map( + (item, idx) => + ) + } + + ); + }) } - - return ( - - {finalTabs[tabName]} - - ); - })} - + { + hasSQLTab && + + + + } + + { showError && } + ); } else { let contentClassName = [ - isSingleCollection ? 'FormView-singleCollectionPanelContent' : + isSingleCollection() ? 'FormView-singleCollectionPanelContent' : 'FormView-nonTabPanelContent', (schemaState.errors?.message ? 'FormView-errorMargin' : null) ]; return ( - - - {Object.keys(finalTabs).map((tabName) => { - return ( - - {finalTabs[tabName]} - - ); - })} - - + <> + + + { + finalGroups.map((group, idx) => + { + group.controls.map( + (item, idx) => + ) + } + ) + } + { + hasSQLTab && + } + + + { showError && } + ); } } + FormView.propTypes = { - value: PropTypes.any, - schema: CustomPropTypes.schemaUI.isRequired, + schema: CustomPropTypes.schemaUI, viewHelperProps: PropTypes.object, isNested: PropTypes.bool, - isDataGridForm: PropTypes.bool, isTabView: PropTypes.bool, - visible: PropTypes.oneOfType([ - PropTypes.bool, PropTypes.func, - ]), accessPath: PropTypes.array.isRequired, dataDispatch: PropTypes.func, hasSQLTab: PropTypes.bool, getSQLValue: PropTypes.func, - onTabChange: PropTypes.func, - firstEleRef: CustomPropTypes.ref, className: CustomPropTypes.className, + field: PropTypes.object, + showError: PropTypes.bool, }; + +registerView(FormView); diff --git a/web/pgadmin/static/js/SchemaView/InlineView.jsx b/web/pgadmin/static/js/SchemaView/InlineView.jsx new file mode 100644 index 00000000000..1398e0310c3 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/InlineView.jsx @@ -0,0 +1,56 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext } from 'react'; +import { Grid } from '@mui/material'; +import _ from 'lodash'; +import PropTypes from 'prop-types'; + +import { SchemaStateContext } from './SchemaState'; +import { useFieldOptions } from './hooks'; +import { registerView } from './registry'; +import { listenDepChanges } from './utils'; + + +// The first component of schema view form. +export default function InlineView({ + accessPath, field, children, viewHelperProps +}) { + const { mode } = (viewHelperProps || {}); + const isPropertyMode = mode === 'properties'; + const schemaState = useContext(SchemaStateContext); + const { visible } = + accessPath ? useFieldOptions(accessPath, schemaState) : { visible: true }; + + if (!accessPath || isPropertyMode) + listenDepChanges(accessPath, field, visible, schemaState); + + // Check whether form is kept hidden by visible prop. + // We don't support inline-view in 'property' mode + if((!_.isUndefined(visible) && !visible) || isPropertyMode) { + return <>; + } + return ( + + {children} + + ); +} + + +InlineView.propTypes = { + accessPath: PropTypes.array, + field: PropTypes.object, + children : PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node + ]) +}; + +registerView(InlineView); diff --git a/web/pgadmin/static/js/SchemaView/MappedControl.jsx b/web/pgadmin/static/js/SchemaView/MappedControl.jsx index fd032406e23..1bdd9880076 100644 --- a/web/pgadmin/static/js/SchemaView/MappedControl.jsx +++ b/web/pgadmin/static/js/SchemaView/MappedControl.jsx @@ -7,22 +7,38 @@ // ////////////////////////////////////////////////////////////// -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import _ from 'lodash'; +import PropTypes from 'prop-types'; + import { - FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor, - FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString, - InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio, FormButton, InputTree -} from '../components/FormComponents'; -import Privilege from '../components/Privilege'; + FormButton, FormInputCheckbox, FormInputColor, FormInputDateTimePicker, + FormInputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, + FormInputSQL, FormInputSelect, FormInputSelectThemes, FormInputSwitch, + FormInputText, FormInputToggle, FormNote, InputCheckbox, InputDateTimePicker, + InputFileSelect, InputRadio, InputSQL,InputSelect, InputSwitch, InputText, + InputTree, PlainString, +} from 'sources/components/FormComponents'; +import { SelectRefresh } from 'sources/components/SelectRefresh'; +import Privilege from 'sources/components/Privilege'; +import { useIsMounted } from 'sources/custom_hooks'; +import CustomPropTypes from 'sources/custom_prop_types'; import { evalFunc } from 'sources/utils'; -import PropTypes from 'prop-types'; -import CustomPropTypes from '../custom_prop_types'; -import { SelectRefresh } from '../components/SelectRefresh'; + +import { SchemaStateContext } from './SchemaState'; +import { isValueEqual } from './common'; +import { + useFieldOptions, useFieldValue, useFieldError +} from './hooks'; +import { listenDepChanges } from './utils'; + /* Control mapping for form view */ -function MappedFormControlBase({ type, value, id, onChange, className, visible, inputRef, noLabel, onClick, withContainer, controlGridBasis, ...props }) { - const name = id; +function MappedFormControlBase({ + id, type, state, onChange, className, inputRef, visible, + withContainer, controlGridBasis, noLabel, ...props +}) { + let name = id; const onTextChange = useCallback((e) => { let val = e; if(e?.target) { @@ -30,6 +46,7 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible, } onChange?.(val); }, []); + const value = state; const onSqlChange = useCallback((changedValue) => { onChange?.(changedValue); @@ -43,58 +60,114 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible, return <>; } + if (name && _.isNumber(name)) { + name = String('name'); + } + /* The mapping uses Form* components as it comes with labels */ switch (type) { case 'int': - return ; + return ; case 'numeric': - return ; + return ; case 'tel': - return ; + return ; case 'text': - return ; + return ; case 'multiline': - return ; + return ; case 'password': - return ; + return ; case 'select': - return ; + return ; case 'select-refresh': - return ; + return ; case 'switch': - return onTextChange(e.target.checked, e.target.name)} className={className} + return onTextChange(e.target.checked, e.target.name)} withContainer={withContainer} controlGridBasis={controlGridBasis} - {...props} />; + {...props} + />; case 'checkbox': - return onTextChange(e.target.checked, e.target.name)} className={className} - {...props} />; + return onTextChange(e.target.checked, e.target.name)} + {...props} + />; case 'toggle': - return ; + return ; case 'color': - return ; + return ; case 'file': - return ; + return ; case 'sql': - return ; + return ; case 'note': return ; case 'datetimepicker': - return ; + return ; case 'keyboardShortcut': - return ; + return ; case 'threshold': - return ; + return ; case 'theme': - return ; + return ; case 'button': - return ; + return ; case 'tree': - return ; + return ; default: return ; } @@ -105,7 +178,7 @@ MappedFormControlBase.propTypes = { PropTypes.string, PropTypes.func, ]).isRequired, value: PropTypes.any, - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), onChange: PropTypes.func, className: PropTypes.oneOfType([ PropTypes.string, PropTypes.object, @@ -116,12 +189,17 @@ MappedFormControlBase.propTypes = { onClick: PropTypes.func, withContainer: PropTypes.bool, controlGridBasis: PropTypes.number, - treeData: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]), + treeData: PropTypes.oneOfType([ + PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func] + ), }; /* Control mapping for grid cell view */ -function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, inputRef, ...props }) { - const name = id; +function MappedCellControlBase({ + cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, inputRef, + ...props +}) { + let name = id; const onTextChange = useCallback((e) => { let val = e; if (e?.target) { @@ -156,6 +234,10 @@ function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, v return <>; } + if (name && _.isNumber(name)) { + name = String('name'); + } + /* The mapping does not need Form* components as labels are not needed for grid cells */ switch(cell) { case 'int': @@ -211,8 +293,9 @@ const ALLOWED_PROPS_FIELD_COMMON = [ 'mode', 'value', 'readonly', 'disabled', 'hasError', 'id', 'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef', 'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis', - 'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName', 'hidden', - 'withContainer', 'controlGridBasis', 'hasCheckbox', 'treeData', 'labelTooltip' + 'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', + 'btnName', 'hidden', 'withContainer', 'controlGridBasis', 'hasCheckbox', + 'treeData', 'labelTooltip' ]; const ALLOWED_PROPS_FIELD_FORM = [ @@ -220,50 +303,128 @@ const ALLOWED_PROPS_FIELD_FORM = [ ]; const ALLOWED_PROPS_FIELD_CELL = [ - 'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType', 'hideBrowseButton', 'hidden' + 'cell', 'onCellChange', 'reRenderRow', 'validate', 'disabled', + 'readonly', 'radioType', 'hideBrowseButton', 'hidden', 'row', ]; +export const StaticMappedFormControl = ({accessPath, field, ...props}) => { + const schemaState = useContext(SchemaStateContext); + const state = schemaState.value(accessPath); + const newProps = { + ...props, + state, + noLabel: field.isFullTab, + ...field, + onChange: () => { /* Do nothing */ }, + }; + const visible = evalFunc(null, field.visible, state); + + if (visible === false) return <>; + + return useMemo( + () => , [] + ); +}; + -export const MappedFormControl = ({memoDeps, ...props}) => { - let newProps = { ...props }; - let typeProps = evalFunc(null, newProps.type, newProps.state); - if (typeof (typeProps) === 'object') { +export const MappedFormControl = ({ + accessPath, dataDispatch, field, onChange, ...props +}) => { + const checkIsMounted = useIsMounted(); + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const state = schemaState.data; + const avoidRenderingWhenNotMounted = (newKey) => { + if (checkIsMounted()) { + setKey(newKey); + } + }; + const value = useFieldValue( + accessPath, schemaState, key, avoidRenderingWhenNotMounted + ); + const options = useFieldOptions( + accessPath, schemaState, key, avoidRenderingWhenNotMounted + ); + const { hasError } = useFieldError( + accessPath, schemaState, key, avoidRenderingWhenNotMounted + ); + + const origOnChange = onChange; + + onChange = (changedValue) => { + if (!origOnChange || !checkIsMounted()) return; + + // We don't want the 'onChange' to be executed for the same value to avoid + // rerendering of the control, top component may still be rerendered on the + // change of the value. + const currValue = schemaState.value(accessPath); + + if (!isValueEqual(changedValue, currValue)) origOnChange(changedValue); + }; + + listenDepChanges(accessPath, field, options.visible, schemaState); + + let newProps = { + ...props, + state: value, + noLabel: field.isFullTab, + ...field, + onChange: onChange, + dataDispatch: dataDispatch, + ...options, + hasError, + }; + + if (typeof (field.type) === 'function') { + const typeProps = evalFunc(null, field.type, state); newProps = { ...newProps, ...typeProps, }; - } else { - newProps.type = typeProps; } let origOnClick = newProps.onClick; newProps.onClick = ()=>{ origOnClick?.(); - /* Consider on click as change for button. - Just increase state val by 1 to inform the deps and self depChange */ - newProps.onChange?.((newProps.state[props.id]||0)+1); }; + // FIXME:: Get this list from the option registry. + const memDeps = ['disabled', 'visible', 'readonly'].map( + option => options[option] + ); + memDeps.push(value); + memDeps.push(hasError); + memDeps.push(key); + memDeps.push(JSON.stringify(accessPath)); - /* Filter out garbage props if any using ALLOWED_PROPS_FIELD */ - return useMemo(()=>, memoDeps??[]); + // Filter out garbage props if any using ALLOWED_PROPS_FIELD. + return useMemo( + () => , [...memDeps] + ); }; MappedFormControl.propTypes = { - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }; export const MappedCellControl = (props) => { - let newProps = { ...props }; - let cellProps = evalFunc(null, newProps.cell, newProps.row.original); - if (typeof (cellProps) === 'object') { - newProps = { - ...newProps, - ...cellProps, - }; - } else { - newProps.cell = cellProps; - } + const newProps = _.pick( + props, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL) + );; - /* Filter out garbage props if any using ALLOWED_PROPS_FIELD */ - return ; + // Filter out garbage props if any using ALLOWED_PROPS_FIELD. + return ; }; diff --git a/web/pgadmin/static/js/SchemaView/ResetButton.jsx b/web/pgadmin/static/js/SchemaView/ResetButton.jsx new file mode 100644 index 00000000000..3c275eb5718 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/ResetButton.jsx @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext, useEffect, useState } from 'react'; + +import { DefaultButton } from 'sources/components/Buttons'; +import { SchemaStateContext } from './SchemaState'; + + +export function ResetButton({label, Icon, onClick}) { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const checkDisabled = (state) => (state.isSaving || !state.isDirty); + const currState = schemaState.state(); + const isDisabled = checkDisabled(currState); + + useEffect(() => { + if (!schemaState) return; + + const refreshOnDisableStateChanged = (newState) => { + if (isDisabled !== checkDisabled(newState)) setKey(Date.now()); + }; + + return schemaState.subscribe([], refreshOnDisableStateChanged, 'states'); + }, [key]); + + return ( + + { label } + + ); +} diff --git a/web/pgadmin/static/js/SchemaView/SQLTab.jsx b/web/pgadmin/static/js/SchemaView/SQLTab.jsx new file mode 100644 index 00000000000..22d29cea14d --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SQLTab.jsx @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { InputSQL } from 'sources/components/FormComponents'; + + +// Optional SQL tab. +export function SQLTab({active, getSQLValue}) { + const [sql, setSql] = useState('Loading...'); + useEffect(() => { + let unmounted = false; + if(active) { + setSql('Loading...'); + getSQLValue().then((value) => { + if(!unmounted) { + setSql(value); + } + }); + } + return () => {unmounted=true;}; + }, [active]); + + return ; +} + +SQLTab.propTypes = { + active: PropTypes.bool, + getSQLValue: PropTypes.func.isRequired, +}; diff --git a/web/pgadmin/static/js/SchemaView/SaveButton.jsx b/web/pgadmin/static/js/SchemaView/SaveButton.jsx new file mode 100644 index 00000000000..a78c4395bda --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SaveButton.jsx @@ -0,0 +1,50 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React, { useContext, useEffect, useState } from 'react'; + +import { PrimaryButton } from 'sources/components/Buttons'; +import { SchemaStateContext } from './SchemaState'; + + +export function SaveButton({ + label, Icon, checkDirtyOnEnableSave, onClick, mode, +}) { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const checkDisabled = (state) => { + const {isDirty, isSaving, errors} = state; + return ( + isSaving || + !(mode === 'edit' || checkDirtyOnEnableSave ? isDirty : true) || + Boolean(errors.name) + ); + }; + const currState = schemaState.state(); + const isDisabled = checkDisabled(currState); + + useEffect(() => { + if (!schemaState) return; + + const refreshOnDisableStateChanged = (newState) => { + if (isDisabled !== checkDisabled(newState)) setKey(Date.now()); + }; + + return schemaState.subscribe([], refreshOnDisableStateChanged, 'states'); + }, [key]); + + return ( + + {label} + + ); +} diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index e7983b37606..dfcd11cc697 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -7,9 +7,7 @@ // ////////////////////////////////////////////////////////////// -import React, { - useCallback, useEffect, useRef, useState, -} from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import DoneIcon from '@mui/icons-material/Done'; @@ -25,23 +23,21 @@ import PropTypes from 'prop-types'; import { parseApiError } from 'sources/api_instance'; import { usePgAdmin } from 'sources/BrowserComponent'; -import Loader from 'sources/components/Loader'; import { useIsMounted } from 'sources/custom_hooks'; import { - PrimaryButton, DefaultButton, PgIconButton + DefaultButton, PgIconButton } from 'sources/components/Buttons'; -import { - FormFooterMessage, MESSAGE_TYPE -} from 'sources/components/FormComponents'; import CustomPropTypes from 'sources/custom_prop_types'; import gettext from 'sources/gettext'; +import { FormLoader } from './FormLoader'; import FormView from './FormView'; +import { ResetButton } from './ResetButton'; +import { SaveButton } from './SaveButton'; +import { SchemaStateContext } from './SchemaState'; import { StyledBox } from './StyledComponents'; -import { useSchemaState } from './useSchemaState'; -import { - getForQueryParams, SchemaStateContext -} from './common'; +import { useSchemaState } from './hooks'; +import { getForQueryParams } from './common'; /* If its the dialog */ @@ -50,30 +46,23 @@ export default function SchemaDialogView({ isTabView=true, checkDirtyOnEnableSave=false, ...props }) { // View helper properties - const { mode, keepCid } = viewHelperProps; const onDataChange = props.onDataChange; - // Message to the user on long running operations. - const [loaderText, setLoaderText] = useState(''); - // Schema data state manager const {schemaState, dataDispatch, sessData, reset} = useSchemaState({ schema: schema, getInitData: getInitData, immutableData: {}, - mode: mode, keepCid: keepCid, onDataChange: onDataChange, - }); - - const [{isNew, isDirty, isReady, errors}, updateSchemaState] = useState({ - isNew: true, isDirty: false, isReady: false, errors: {} + viewHelperProps: viewHelperProps, onDataChange: onDataChange, + loadingText, }); // Is saving operation in progress? - const [saving, setSaving] = useState(false); + const setSaving = (val) => schemaState.isSaving = val; + const setLoaderText = (val) => schemaState.setMessage(val); // First element to be set by the FormView to set the focus after loading // the data. const firstEleRef = useRef(); const checkIsMounted = useIsMounted(); - const [data, setData] = useState({}); // Notifier object. const pgAdmin = usePgAdmin(); @@ -93,21 +82,11 @@ export default function SchemaDialogView({ }; }, []); - useEffect(() => { - setLoaderText(schemaState.message); - }, [schemaState.message]); - - useEffect(() => { - setData(sessData); - updateSchemaState(schemaState); - }, [sessData.__changeId]); - useEffect(()=>{ if (!props.resetKey) return; reset(); }, [props.resetKey]); - const onResetClick = () => { const resetIt = () => { firstEleRef.current?.focus(); @@ -128,7 +107,7 @@ export default function SchemaDialogView({ }; const save = (changeData) => { - props.onSave(isNew, changeData) + props.onSave(schemaState.isNew, changeData) .then(()=>{ if(schema.informText) { Notifier.alert( @@ -151,7 +130,10 @@ export default function SchemaDialogView({ const onSaveClick = () => { // Do nothing when there is no change or there is an error - if (!schemaState.changes || errors.name) return; + if ( + !schemaState.changes || Object.keys(schemaState.changes) === 0 || + schemaState.errors.name + ) return; setSaving(true); setLoaderText('Saving...'); @@ -164,7 +146,7 @@ export default function SchemaDialogView({ Notifier.confirm( gettext('Warning'), schema.warningText, - ()=> { save(schemaState.Changes(true)); }, + () => { save(schemaState.Changes(true)); }, () => { setSaving(false); setLoaderText(''); @@ -173,20 +155,13 @@ export default function SchemaDialogView({ ); }; - const onErrClose = useCallback(() => { - const err = { ...errors, message: '' }; - // Unset the error message, but not the name. - schemaState.setError(err); - updateSchemaState({isNew, isDirty, isReady, errors: err}); - }); - const getSQLValue = () => { // Called when SQL tab is active. - if(!isDirty) { + if(!schemaState.isDirty) { return Promise.resolve('-- ' + gettext('No updates.')); } - if(errors.name) { + if(schemaState.errors.name) { return Promise.resolve('-- ' + gettext('Definition incomplete.')); } @@ -195,7 +170,7 @@ export default function SchemaDialogView({ * Call the passed incoming getSQLValue func to get the SQL * return of getSQLValue should be a promise. */ - return props.getSQLValue(isNew, getForQueryParams(changeData)); + return props.getSQLValue(schemaState.isNew, getForQueryParams(changeData)); }; const getButtonIcon = () => { @@ -207,29 +182,23 @@ export default function SchemaDialogView({ return ; }; - const disableSaveBtn = saving || - !isReady || - !(mode === 'edit' || checkDirtyOnEnableSave ? isDirty : true) || - Boolean(errors.name && errors.name !== 'apierror'); - let ButtonIcon = getButtonIcon(); /* I am Groot */ - return ( + return useMemo(() => - - + - + className={props.formClassName} + showError={true} + /> {showFooter && @@ -237,13 +206,13 @@ export default function SchemaDialogView({ (!props.disableSqlHelp || !props.disableDialogHelp) && props.onHelp(true, isNew)} + onClick={()=>props.onHelp(true, schemaState.isNew)} icon={} disabled={props.disableSqlHelp} className='Dialog-buttonMargin' title={ gettext('SQL help for this object type.') } /> props.onHelp(false, isNew)} + onClick={()=>props.onHelp(false, schemaState.isNew)} icon={} disabled={props.disableDialogHelp} title={ gettext('Help for this dialog.') } /> @@ -254,23 +223,21 @@ export default function SchemaDialogView({ startIcon={} className='Dialog-buttonMargin'> { gettext('Close') } - } - disabled={(!isDirty) || saving } - className='Dialog-buttonMargin'> - { gettext('Reset') } - - { - props.customSaveBtnName || gettext('Save') - } - + } + label={ gettext('Reset') }/> + } - + , [schema._id, viewHelperProps.mode] ); } diff --git a/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx b/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx index 63ceb912320..9dd26df8636 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx @@ -7,7 +7,7 @@ // ////////////////////////////////////////////////////////////// -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo } from 'react'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import InfoIcon from '@mui/icons-material/InfoRounded'; @@ -16,178 +16,94 @@ import Box from '@mui/material/Box'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; -import _ from 'lodash'; import PropTypes from 'prop-types'; import { usePgAdmin } from 'sources/BrowserComponent'; import gettext from 'sources/gettext'; -import Loader from 'sources/components/Loader'; import { PgIconButton, PgButtonGroup } from 'sources/components/Buttons'; import CustomPropTypes from 'sources/custom_prop_types'; -import DataGridView from './DataGridView'; -import FieldSetView from './FieldSetView'; -import { MappedFormControl } from './MappedControl'; -import { useSchemaState } from './useSchemaState'; -import { getFieldMetaData } from './common'; - +import { FieldControl } from './FieldControl'; +import { FormLoader } from './FormLoader'; +import { SchemaStateContext } from './SchemaState'; import { StyledBox } from './StyledComponents'; +import { useSchemaState } from './hooks'; +import { createFieldControls } from './utils'; /* If its the properties tab */ export default function SchemaPropertiesView({ getInitData, viewHelperProps, schema={}, updatedData, ...props }) { - let defaultTab = 'General'; - let tabs = {}; - let tabsClassname = {}; - let groupLabels = {}; - const [loaderText, setLoaderText] = useState(''); const pgAdmin = usePgAdmin(); const Notifier = pgAdmin.Browser.notifier; - const { mode, keepCid } = viewHelperProps; // Schema data state manager - const {schemaState, sessData} = useSchemaState({ + const {schemaState} = useSchemaState({ schema: schema, getInitData: getInitData, immutableData: updatedData, - mode: mode, keepCid: keepCid, onDataChange: null, + viewHelperProps: viewHelperProps, onDataChange: null, }); - const [data, setData] = useState({}); useEffect(() => { if (schemaState.errors?.response) Notifier.pgRespErrorNotify(schemaState.errors.response); }, [schemaState.errors?.name]); - useEffect(() => { - setData(sessData); - }, [sessData.__changeId]); - - useEffect(() => { - setLoaderText(schemaState.message); - }, [schemaState.message]); - - /* A simple loop to get all the controls for the fields */ - schema.fields.forEach((field) => { - let {group} = field; - const { - visible, disabled, readonly, modeSupported - } = getFieldMetaData(field, schema, data, viewHelperProps); - group = group || defaultTab; - - if(field.isFullTab) { - tabsClassname[group] = 'Properties-noPadding'; - } - - if(!modeSupported) return; - - group = groupLabels[group] || group || defaultTab; - if (field.helpMessageMode?.indexOf(viewHelperProps.mode) == -1) - field.helpMessage = ''; - - if(!tabs[group]) tabs[group] = []; - - if(field && field.type === 'nested-fieldset') { - tabs[group].push( - - ); - } else if(field.type === 'collection') { - tabs[group].push( - - ); - } else if(field.type === 'group') { - groupLabels[field.id] = field.label; - - if(!visible) { - schema.filterGroups.push(field.label); - } - } else { - tabs[group].push( - - ); - } - }); - - let finalTabs = _.pickBy( - tabs, (v, tabName) => schema.filterGroups.indexOf(tabName) <= -1 + const finalTabs = useMemo( + () => createFieldControls({ + schema, schemaState, viewHelperProps, dataDispatch: null, accessPath: [] + }), + [schema._id, schemaState, viewHelperProps] ); + if (!finalTabs) return <>; + return ( - - - - props.onHelp(true, false)} - icon={} disabled={props.disableSqlHelp} - title="SQL help for this object type." /> - } - title={gettext('Edit object...')} /> - - - - - {Object.keys(finalTabs).map((tabName)=>{ - let id = tabName.replace(' ', ''); - return ( - - } - aria-controls={`${id}-content`} - id={`${id}-header`} - > - {tabName} - - - - {finalTabs[tabName]} - - - - ); - })} + + + + + props.onHelp(true, false)} + icon={} disabled={props.disableSqlHelp} + title="SQL help for this object type." /> + } + title={gettext('Edit object...')} /> + + + + + {finalTabs.map((group)=>{ + let id = group.id.replace(' ', ''); + return ( + + } + aria-controls={`${id}-content`} + id={`${id}-header`} + > + {group.label} + + + + { + group.controls.map( + (item, idx) => + + ) + } + + + + ); + })} + - + ); } diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js new file mode 100644 index 00000000000..b74bd4d155d --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js @@ -0,0 +1,328 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import _ from 'lodash'; + +import { parseApiError } from 'sources/api_instance'; +import gettext from 'sources/gettext'; + +import { prepareData } from '../common'; +import { DepListener } from '../DepListener'; +import { FIELD_OPTIONS, schemaOptionsEvalulator } from '../options'; + +import { + SCHEMA_STATE_ACTIONS, + flatPathGenerator, + getSchemaDataDiff, + validateSchema, +} from './common'; +import { createStore } from './store'; + + +export const LOADING_STATE = { + INIT: 'initializing', + LOADING: 'loading', + LOADED: 'loaded', + ERROR: 'Error' +}; + +const PATH_SEPARATOR = '/'; + +export class SchemaState extends DepListener { + constructor( + schema, getInitData, immutableData, onDataChange, viewHelperProps, + loadingText + ) { + super(); + + ////// Helper variables + + // BaseUISchema instance + this.schema = schema; + this.viewHelperProps = viewHelperProps; + // Current mode of operation ('create', 'edit', 'properties') + this.mode = viewHelperProps.mode; + // Keep the 'cid' object during diff calculations. + this.keepcid = viewHelperProps.keepCid; + // Initialization callback + this.getInitData = getInitData; + // Data change callback + this.onDataChange = onDataChange; + + ////// State variables + + // Diff between the current snapshot and initial data. + this.changes = {}; + // Current Loading state + this.loadingState = LOADING_STATE.INIT; + this.customLoadingText = loadingText; + + ////// Schema instance data + + // Initial data after the ready state + this.initData = {}; + + // Immutable data + this.immutableData = immutableData; + // Pre-ready queue + this.preReadyQueue = []; + + this.optionStore = createStore({}); + this.dataStore = createStore({}); + this.stateStore = createStore({ + isNew: true, isDirty: false, isReady: false, + isSaving: false, errors: {}, + message: '', + }); + + // Memoize the path using flatPathGenerator + this.__pathGenerator = flatPathGenerator(PATH_SEPARATOR); + + this._id = Date.now(); + } + + updateOptions() { + let options = _.cloneDeep(this.optionStore.getState()); + + schemaOptionsEvalulator({ + schema: this.schema, data: this.data, options: options, + viewHelperProps: this.viewHelperProps, + }); + + this.optionStore.setState(options); + } + + setState(state, value) { + this.stateStore.set((prev) => _.set(prev, [].concat(state), value)); + } + + setError(err) { + this.setState('errors', err); + } + + get errors() { + return this.stateStore.get(['errors']); + } + + set errors(val) { + throw new Error('Property \'errors\' is readonly.', val); + } + + get isReady() { + return this.stateStore.get(['isReady']); + } + + setReady(val) { + this.setState('isReady', val); + } + + get isSaving() { + return this.stateStore.get(['isSaving']); + } + + set isSaving(val) { + this.setState('isSaving', val); + } + + get loadingMessage() { + return this.stateStore.get(['message']); + } + + setLoadingState(loadingState) { + this.loadingState = loadingState; + } + + setMessage(msg) { + this.setState('message', msg); + } + + // Initialise the data, and fetch the data from the backend (if required). + // 'force' flag can be used for reloading the data from the backend. + initialise(dataDispatch, force) { + let state = this; + + // Don't attempt to initialize again (if it's already in progress). + if ( + state.loadingState !== LOADING_STATE.INIT || + (force && state.loadingState === LOADING_STATE.LOADING) + ) return; + + state.setLoadingState(LOADING_STATE.LOADING); + state.setMessage(state.customLoadingText || gettext('Loading...')); + + /* + * Fetch the data using getInitData(..) callback. + * `getInitData(..)` must be present in 'edit' mode. + */ + if(state.mode === 'edit' && !state.getInitData) { + throw new Error('getInitData must be passed for edit'); + } + + const initDataPromise = state.getInitData?.() || + Promise.resolve({}); + + initDataPromise.then((data) => { + data = data || {}; + + if(state.mode === 'edit') { + // Set the origData to incoming data, useful for comparing. + state.initData = prepareData({...data, ...state.immutableData}); + } else { + // In create mode, merge with defaults. + state.initData = prepareData({ + ...state.schema.defaults, ...data, ...state.immutableData + }, true); + } + + state.schema.initialise(state.initData); + + dataDispatch({ + type: SCHEMA_STATE_ACTIONS.INIT, + payload: state.initData, + }); + + state.setLoadingState(LOADING_STATE.LOADED); + state.setMessage(''); + state.setReady(true); + state.setState('isNew', state.schema.isNew(state.initData)); + }).catch((err) => { + state.setMessage(''); + state.setError({ + name: 'apierror', + response: err, + message: _.escape(parseApiError(err)), + }); + state.setLoadingState(LOADING_STATE.ERROR); + state.setReady(true); + }); + } + + validate(sessData) { + let state = this, + schema = state.schema; + + // If schema does not have the data or does not have any 'onDataChange' + // callback, there is no need to validate the current data. + if(!state.isReady) return; + + if( + !validateSchema(schema, sessData, (path, message) => { + message && state.setError({ + name: state.accessPath(path), message: _.escape(message) + }); + }) + ) state.setError({}); + + state.data = sessData; + state.changes = state.Changes(); + state.updateOptions(); + state.onDataChange && state.onDataChange(state.isDirty, state.changes); + } + + Changes(includeSkipChange=false) { + const state = this; + const sessData = this.data; + const schema = state.schema; + + // Check if anything changed. + let dataDiff = getSchemaDataDiff( + schema, state.initData, sessData, + state.mode, state.keepCid, false, includeSkipChange + ); + + const isDirty = Object.keys(dataDiff).length > 0; + state.setState('isDirty', isDirty); + + + // Inform the callbacks about change in the data. + if(state.mode !== 'edit') { + // Merge the changed data with origData in 'create' mode. + dataDiff = _.assign({}, state.initData, dataDiff); + + // Remove internal '__changeId' attribute. + delete dataDiff.__changeId; + + // In case of 'non-edit' mode, changes are always there. + return dataDiff; + } + + if (!isDirty) return {}; + + const idAttr = schema.idAttribute; + const idVal = state.initData[idAttr]; + + // Append 'idAttr' only if it actually exists + if (idVal) dataDiff[idAttr] = idVal; + + return dataDiff; + } + + get isNew() { + return this.stateStore.get(['isNew']); + } + + set isNew(val) { + throw new Error('Property \'isNew\' is readonly.', val); + } + + get isDirty() { + return this.stateStore.get(['isDirty']); + } + + set isDirty(val) { + throw new Error('Property \'isDirty\' is readonly.', val); + } + + get data() { + return this.dataStore.getState(); + } + + set data(_data) { + this.dataStore.setState(_data); + } + + accessPath(path=[], key) { + return this.__pathGenerator.cached( + _.isUndefined(key) ? path : path.concat(key) + ); + } + + value(path) { + return _.get(this.data, path); + } + + options(path) { + return this.optionStore.get(path.concat(FIELD_OPTIONS)); + } + + state(_state) { + return _state ? + this.stateStore.get([].concat(_state)) : this.stateStore.getState(); + } + + subscribe(path, listener, kind='options') { + switch(kind) { + case 'options': + return this.optionStore.subscribeForPath( + path.concat(FIELD_OPTIONS), listener + ); + case 'states': + return this.stateStore.subscribeForPath(path, listener); + default: + return this.dataStore.subscribeForPath(path, listener); + } + } + + subscribeOption(option, path, listener) { + return this.optionStore.subscribeForPath( + path.concat(FIELD_OPTIONS, option), listener + ); + } + +} diff --git a/web/pgadmin/static/js/SchemaView/schemaUtils.js b/web/pgadmin/static/js/SchemaView/SchemaState/common.js similarity index 87% rename from web/pgadmin/static/js/SchemaView/schemaUtils.js rename to web/pgadmin/static/js/SchemaView/SchemaState/common.js index 0fc204f50af..0dfc9b66271 100644 --- a/web/pgadmin/static/js/SchemaView/schemaUtils.js +++ b/web/pgadmin/static/js/SchemaView/SchemaState/common.js @@ -11,13 +11,27 @@ import diffArray from 'diff-arrays-of-objects'; import _ from 'lodash'; import gettext from 'sources/gettext'; +import { memoizeFn } from 'sources/utils'; import { minMaxValidator, numberValidator, integerValidator, emptyValidator, checkUniqueCol, isEmptyString } from 'sources/validators'; -import BaseUISchema from './base_schema.ui'; -import { isModeSupportedByField, isObjectEqual, isValueEqual } from './common'; +import BaseUISchema from '../base_schema.ui'; +import { isModeSupportedByField, isObjectEqual, isValueEqual } from '../common'; + + +export const SCHEMA_STATE_ACTIONS = { + INIT: 'init', + SET_VALUE: 'set_value', + ADD_ROW: 'add_row', + DELETE_ROW: 'delete_row', + MOVE_ROW: 'move_row', + RERENDER: 'rerender', + CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue', + DEFERRED_DEPCHANGE: 'deferred_depchange', + BULK_UPDATE: 'bulk_update', +}; // Remove cid key added by prepareData const cleanCid = (coll, keepCid=false) => ( @@ -331,3 +345,40 @@ export function validateSchema( sessData, (id, message) => setError(accessPath.concat(id), message) ); } + +export const getDepChange = (currPath, newState, oldState, action) => { + if(action.depChange) { + newState = action.depChange(currPath, newState, { + type: action.type, + path: action.path, + value: action.value, + oldState: _.cloneDeep(oldState), + listener: action.listener, + }); + } + return newState; +}; + +// It will help us generating the flat path, and it will return the same +// object for the same path, which will help with the React componet rendering, +// as it uses `Object.is(...)` for the comparison of the arguments. +export const flatPathGenerator = (separator = '.' ) => { + const flatPathMap = new Map; + + const setter = memoizeFn((path) => { + const flatPath = path.join(separator); + flatPathMap.set(flatPath, path); + return flatPath; + }); + + const getter = (flatPath) => { + return flatPathMap.get(flatPath); + }; + + return { + flatPath: setter, + path: getter, + // Get the same object every time. + cached: (path) => (getter(setter(path))), + }; +}; diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/context.js b/web/pgadmin/static/js/SchemaView/SchemaState/context.js new file mode 100644 index 00000000000..fc6fbe48826 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/context.js @@ -0,0 +1,12 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; + +export const SchemaStateContext = React.createContext(); diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/index.js b/web/pgadmin/static/js/SchemaView/SchemaState/index.js new file mode 100644 index 00000000000..4efcaf27493 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/index.js @@ -0,0 +1,21 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { SchemaState } from './SchemaState'; +import { SchemaStateContext } from './context'; +import { SCHEMA_STATE_ACTIONS } from './common'; +import { sessDataReducer } from './reducer'; + + +export { + SCHEMA_STATE_ACTIONS, + SchemaState, + SchemaStateContext, + sessDataReducer, +}; diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/reducer.js b/web/pgadmin/static/js/SchemaView/SchemaState/reducer.js new file mode 100644 index 00000000000..f80f815c948 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/reducer.js @@ -0,0 +1,123 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import _ from 'lodash'; +import { + SCHEMA_STATE_ACTIONS, getDepChange, +} from './common'; + +const getDeferredDepChange = (currPath, newState, oldState, action) => { + if(action.deferredDepChange) { + return action.deferredDepChange(currPath, newState, { + type: action.type, + path: action.path, + value: action.value, + depChange: action.depChange, + oldState: _.cloneDeep(oldState), + }); + } +}; + +/* + * The main function which manipulates the session state based on actions. + * + * The state is managed based on path array of a particular key. + * For Eg. if the state is + * { + * key1: { + * ckey1: [ + * {a: 0, b: 0}, + * {a: 1, b: 1} + * ] + * } + * } + * + * The path for b in first row will be '[key1, ckey1, 0, b]'. + * The path for second row of ckey1 will be '[key1, ckey1, 1]'. + * + * The path for key1 is '[key1]'. + * The state starts with path '[]'. + */ +export const sessDataReducer = (state, action) => { + let data = _.cloneDeep(state); + let rows, cid, deferredList; + data.__deferred__ = data.__deferred__ || []; + + switch(action.type) { + case SCHEMA_STATE_ACTIONS.INIT: + data = action.payload; + break; + + case SCHEMA_STATE_ACTIONS.BULK_UPDATE: + rows = _.get(data, action.path) || []; + rows.forEach((row) => { row[action.id] = false; }); + _.set(data, action.path, rows); + break; + + case SCHEMA_STATE_ACTIONS.SET_VALUE: + _.set(data, action.path, action.value); + // If there is any dep listeners get the changes. + data = getDepChange(action.path, data, state, action); + deferredList = getDeferredDepChange(action.path, data, state, action); + data.__deferred__ = deferredList || []; + break; + + case SCHEMA_STATE_ACTIONS.ADD_ROW: + // Create id to identify a row uniquely, usefull when getting diff. + cid = _.uniqueId('c'); + action.value['cid'] = cid; + + if (action.addOnTop) { + rows = [].concat(action.value).concat(_.get(data, action.path) || []); + } else { + rows = (_.get(data, action.path) || []).concat(action.value); + } + + _.set(data, action.path, rows); + + // If there is any dep listeners get the changes. + data = getDepChange(action.path, data, state, action); + + break; + + case SCHEMA_STATE_ACTIONS.DELETE_ROW: + rows = _.get(data, action.path)||[]; + rows.splice(action.value, 1); + + _.set(data, action.path, rows); + + // If there is any dep listeners get the changes. + data = getDepChange(action.path, data, state, action); + + break; + + case SCHEMA_STATE_ACTIONS.MOVE_ROW: + rows = _.get(data, action.path)||[]; + var row = rows[action.oldIndex]; + rows.splice(action.oldIndex, 1); + rows.splice(action.newIndex, 0, row); + + _.set(data, action.path, rows); + + break; + + case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE: + data.__deferred__ = []; + return data; + + case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE: + data = getDepChange(action.path, data, state, action); + break; + } + + data.__changeId = (data.__changeId || 0) + 1; + + return data; +}; + diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/store.js b/web/pgadmin/static/js/SchemaView/SchemaState/store.js new file mode 100644 index 00000000000..67eb0e0eb6e --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/store.js @@ -0,0 +1,78 @@ +import _ from 'lodash'; + +import { isValueEqual } from '../common'; +import { flatPathGenerator } from './common'; + + +export const createStore = (initialState) => { + let state = initialState; + + const listeners = new Set(); + const gen = flatPathGenerator('/'); + const pathListeners = new Set(); + + // Exposed functions + // Don't attempt to manipulate the state directly. + const getState = () => state; + const setState = (nextState) => { + const prevState = state; + state = _.clone(nextState); + + if (isValueEqual(state, prevState)) return; + + listeners.forEach((listener) => { + listener(); + }); + + const changeMemo = new Map(); + + pathListeners.forEach((pathListener) => { + const [ path, listener ] = pathListener; + const flatPath = gen.flatPath(path); + + if (!changeMemo.has(flatPath)) { + const pathNextValue = + flatPath == '' ? nextState : _.get(nextState, path, undefined); + const pathPrevValue = + flatPath == '' ? prevState : _.get(prevState, path, undefined); + + changeMemo.set(flatPath, [ + isValueEqual(pathNextValue, pathPrevValue), + pathNextValue, + pathPrevValue, + ]); + } + + const [isSame, pathNextValue, pathPrevValue] = changeMemo.get(flatPath); + + if (!isSame) { + listener(pathNextValue, pathPrevValue); + } + }); + }; + const get = (path = []) => (_.get(state, path)); + const set = (arg) => { + let nextState = _.isFunction(arg) ? arg(_.cloneDeep(state)) : arg; + setState(nextState); + }; + const subscribe = (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }; + const subscribeForPath = (path, listner) => { + const data = [path, listner]; + + pathListeners.add(data); + + return () => pathListeners.delete(data); + }; + + return { + getState, + setState, + get, + set, + subscribe, + subscribeForPath, + }; +}; diff --git a/web/pgadmin/static/js/SchemaView/SchemaView.jsx b/web/pgadmin/static/js/SchemaView/SchemaView.jsx index 3da1d55ea94..61aa9ae919e 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaView.jsx @@ -15,6 +15,7 @@ import ErrorBoundary from 'sources/helpers/ErrorBoundary'; import SchemaDialogView from './SchemaDialogView'; import SchemaPropertiesView from './SchemaPropertiesView'; +import { registerView } from './registry'; export default function SchemaView({formType, ...props}) { @@ -32,3 +33,5 @@ export default function SchemaView({formType, ...props}) { SchemaView.propTypes = { formType: PropTypes.oneOf(['tab', 'dialog']), }; + +registerView(SchemaView); diff --git a/web/pgadmin/static/js/SchemaView/StyledComponents.jsx b/web/pgadmin/static/js/SchemaView/StyledComponents.jsx index 3529007a8c1..1d67efc4a33 100644 --- a/web/pgadmin/static/js/SchemaView/StyledComponents.jsx +++ b/web/pgadmin/static/js/SchemaView/StyledComponents.jsx @@ -41,7 +41,7 @@ export const StyledBox = styled(Box)(({theme}) => ({ padding: theme.spacing(1), overflow: 'auto', flexGrow: 1, - '& .Properties-controlRow': { + '& .Properties-controlRow:not(:last-child)': { marginBottom: theme.spacing(1), }, }, diff --git a/web/pgadmin/static/js/SchemaView/base_schema.ui.js b/web/pgadmin/static/js/SchemaView/base_schema.ui.js index 4c6578a57ea..955f1f6ef15 100644 --- a/web/pgadmin/static/js/SchemaView/base_schema.ui.js +++ b/web/pgadmin/static/js/SchemaView/base_schema.ui.js @@ -9,6 +9,8 @@ import _ from 'lodash'; +import { memoizeFn } from 'sources/utils'; + /* This is the base schema class for SchemaView. * A UI schema must inherit this to use SchemaView for UI. */ @@ -63,11 +65,11 @@ export default class BaseUISchema { /* * The session data, can be useful but setting this will not affect UI. - * this._sessData is set by SchemaView directly. set sessData should not be + * this.sessData is set by SchemaView directly. set sessData should not be * allowed anywhere. */ get sessData() { - return this._sessData || {}; + return this.state?.data; } set sessData(val) { @@ -93,19 +95,28 @@ export default class BaseUISchema { concat base fields with extraFields. */ get fields() { - return this.baseFields - .filter((field)=>{ - let retval; - - /* If any groups are to be filtered */ - retval = this.filterGroups.indexOf(field.group) == -1; + if (!this.__filteredFields) { + // Memoize the results + this.__filteredFields = memoizeFn( + (baseFields, keys, filterGroups) => baseFields.filter((field) => { + let retval; + + // If any groups are to be filtered. + retval = filterGroups.indexOf(field.group) == -1; + + // Select only keys, if specified. + if(retval && keys) { + retval = keys.indexOf(field.id) > -1; + } + + return retval; + }) + ); + } - /* Select only keys, if specified */ - if(this.keys) { - retval = retval && this.keys.indexOf(field.id) > -1; - } - return retval; - }); + return this.__filteredFields( + this.baseFields, this.keys, this.filterGroups + ); } initialise() { @@ -190,4 +201,8 @@ export default class BaseUISchema { } return res; } + + toJSON() { + return this._id; + } } diff --git a/web/pgadmin/static/js/SchemaView/common.js b/web/pgadmin/static/js/SchemaView/common.js index 7ad31c79b42..3c14727b89b 100644 --- a/web/pgadmin/static/js/SchemaView/common.js +++ b/web/pgadmin/static/js/SchemaView/common.js @@ -7,32 +7,16 @@ // ////////////////////////////////////////////////////////////// -import React from 'react'; import { evalFunc } from 'sources/utils'; -export const SCHEMA_STATE_ACTIONS = { - INIT: 'init', - SET_VALUE: 'set_value', - ADD_ROW: 'add_row', - DELETE_ROW: 'delete_row', - MOVE_ROW: 'move_row', - RERENDER: 'rerender', - CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue', - DEFERRED_DEPCHANGE: 'deferred_depchange', - BULK_UPDATE: 'bulk_update', -}; - -export const SchemaStateContext = React.createContext(); - export function generateTimeBasedRandomNumberString() { return new Date().getTime() + '' + Math.floor(Math.random() * 1000001); } -export function isModeSupportedByField(field, helperProps) { - if (!field || !field.mode) return true; - return (field.mode.indexOf(helperProps.mode) > -1); -} +export const isModeSupportedByField = (field, helperProps) => ( + !field.mode || field.mode.indexOf(helperProps.mode) > -1 +); export function getFieldMetaData( field, schema, value, viewHelperProps @@ -81,13 +65,14 @@ export function getFieldMetaData( retData.editable = !( viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties') ); + if(retData.editable) { retData.editable = evalFunc( schema, (_.isUndefined(editable) ? true : editable), value ); } - let {canAdd, canEdit, canDelete, canReorder, canAddRow } = field; + let {canAdd, canEdit, canDelete, canAddRow } = field; retData.canAdd = _.isUndefined(canAdd) ? retData.canAdd : evalFunc(schema, canAdd, value); retData.canAdd = !retData.disabled && retData.canAdd; @@ -99,10 +84,6 @@ export function getFieldMetaData( schema, canDelete, value ); retData.canDelete = !retData.disabled && retData.canDelete; - retData.canReorder = - _.isUndefined(canReorder) ? retData.canReorder : evalFunc( - schema, canReorder, value - ); retData.canAddRow = _.isUndefined(canAddRow) ? retData.canAddRow : evalFunc( schema, canAddRow, value @@ -165,3 +146,38 @@ export function getForQueryParams(data) { }); return retData; } + +export function prepareData(val, createMode=false) { + if(_.isPlainObject(val)) { + _.forIn(val, function (el) { + if (_.isObject(el)) { + prepareData(el, createMode); + } + }); + } else if(_.isArray(val)) { + val.forEach(function(el) { + if (_.isPlainObject(el)) { + /* The each row in collection need to have an id to identify them uniquely + This helps in easily getting what has changed */ + /* Nested collection rows may or may not have idAttribute. + So to decide whether row is new or not set, the cid starts with + nn (not new) for existing rows. Newly added will start with 'c' (created) + */ + el['cid'] = createMode ? _.uniqueId('c') : _.uniqueId('nn'); + prepareData(el, createMode); + } + }); + } + return val; +} + +export const flatternObject = (obj, base=[]) => Object.keys(obj).sort().reduce( + (r, k) => { + r = r.concat(k); + const value = obj[k]; + if (_.isFunction(value)) return r; + if (_.isArray(value)) return r.concat(...value); + if (_.isPlainObject(value)) return flatternObject(value, r); + return r.concat(value); + }, base +); diff --git a/web/pgadmin/static/js/SchemaView/hooks/index.js b/web/pgadmin/static/js/SchemaView/hooks/index.js new file mode 100644 index 00000000000..d026c8ae839 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/index.js @@ -0,0 +1,21 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useFieldError } from './useFieldError'; +import { useFieldOptions } from './useFieldOptions'; +import { useFieldValue } from './useFieldValue'; +import { useSchemaState } from './useSchemaState'; + + +export { + useFieldError, + useFieldOptions, + useFieldValue, + useSchemaState, +}; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js new file mode 100644 index 00000000000..9c2540ca390 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js @@ -0,0 +1,34 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useEffect } from 'react'; + + +export const useFieldError = ( + path, schemaState, key, setRefreshKey +) => { + useEffect(() => { + if (!schemaState || !setRefreshKey) return; + + const checkPathError = (newState, prevState) => { + if (prevState.name !== path && newState.name !== path) return; + // We don't need to redraw the control on message change. + if (prevState.name === newState.name) return; + + setRefreshKey({id: Date.now()}); + }; + + return schemaState.subscribe(['errors'], checkPathError, 'states'); + }, [key]); + + const errors = schemaState?.errors || {}; + const error = errors.name === path ? errors.message : null; + + return {hasError: !_.isNull(error), error}; +}; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js new file mode 100644 index 00000000000..f21d17e3c0f --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js @@ -0,0 +1,25 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useEffect } from 'react'; + + +export const useFieldOptions = ( + path, schemaState, key, setRefreshKey +) => { + useEffect(() => { + if (!schemaState) return; + + return schemaState.subscribe( + path, () => setRefreshKey?.({id: Date.now()}), 'options' + ); + }, [key]); + + return schemaState?.options(path) || {visible: true}; +}; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js new file mode 100644 index 00000000000..ffbe04e12e6 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js @@ -0,0 +1,25 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useEffect } from 'react'; + + +export const useFieldValue = ( + path, schemaState, key, setRefreshKey +) => { + useEffect(() => { + if (!schemaState || !setRefreshKey) return; + + return schemaState.subscribe( + path, () => setRefreshKey({id: Date.now()}), 'value' + ); + }, [key]); + + return schemaState?.value(path); +}; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js b/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js new file mode 100644 index 00000000000..0e9fda386ce --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js @@ -0,0 +1,136 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useEffect, useReducer } from 'react'; +import _ from 'lodash'; + +import { prepareData } from '../common'; +import { + SCHEMA_STATE_ACTIONS, + SchemaState, + sessDataReducer, +} from '../SchemaState'; + + +export const useSchemaState = ({ + schema, getInitData, immutableData, onDataChange, viewHelperProps, + loadingText, +}) => { + let state = schema.state; + + if (!state) { + schema.state = state = new SchemaState( + schema, getInitData, immutableData, onDataChange, viewHelperProps, + loadingText, + ); + state.updateOptions(); + } + + const [sessData, sessDispatch] = useReducer( + sessDataReducer, {...(_.cloneDeep(state.data)), __changeId: 0} + ); + + const sessDispatchWithListener = (action) => { + let dispatchPayload = { + ...action, + depChange: (...args) => state.getDepChange(...args), + deferredDepChange: (...args) => state.getDeferredDepChange(...args), + }; + /* + * All the session changes coming before init should be queued up. + * They will be processed later when form is ready. + */ + let preReadyQueue = state.preReadyQueue; + + preReadyQueue ? + preReadyQueue.push(dispatchPayload) : + sessDispatch(dispatchPayload); + }; + + state.setUnpreparedData = (path, value) => { + if(path) { + let data = prepareData(value); + _.set(schema.initData, path, data); + sessDispatchWithListener({ + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + path: path, + value: data, + }); + } + }; + + const resetData = () => { + const initData = _.cloneDeep(state.initData); + initData.__changeId = sessData.__changeId; + sessDispatch({ + type: SCHEMA_STATE_ACTIONS.INIT, + payload: initData, + }); + }; + + const reload = () => { + state.initialise(sessDispatch, true); + }; + + useEffect(() => { + state.initialise(sessDispatch); + }, [state.loadingState]); + + useEffect(() => { + let preReadyQueue = state.preReadyQueue; + + if (!state.isReady || !preReadyQueue) return; + + for (const payload of preReadyQueue) { + sessDispatch(payload); + } + + // Destroy the queue so that no one uses it. + state.preReadyQueue = null; + }, [state.isReady]); + + useEffect(() => { + // Validate the schema on the change of the data. + if (state.isReady) state.validate(sessData); + }, [state.isReady, sessData.__changeId]); + + useEffect(() => { + const items = sessData.__deferred__ || []; + + if (items.length == 0) return; + + sessDispatch({ + type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE, + }); + + items.forEach((item) => { + item.promise.then((resFunc) => { + sessDispatch({ + type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE, + path: item.action.path, + depChange: item.action.depChange, + listener: { + ...item.listener, + callback: resFunc, + }, + }); + }); + }); + }, [sessData.__deferred__?.length]); + + state.reload = reload; + state.reset = resetData; + + return { + schemaState: state, + dataDispatch: sessDispatchWithListener, + sessData, + reset: resetData, + }; +}; diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 11f6bfff480..985dd88765f 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -10,40 +10,41 @@ import DataGridView from './DataGridView'; import FieldSetView from './FieldSetView'; import FormView from './FormView'; +import InlineView from './InlineView'; import SchemaDialogView from './SchemaDialogView'; import SchemaPropertiesView from './SchemaPropertiesView'; import SchemaView from './SchemaView'; import BaseUISchema from './base_schema.ui'; -import { useSchemaState } from './useSchemaState'; +import { useSchemaState, useFieldState } from './hooks'; import { - SCHEMA_STATE_ACTIONS, - SchemaStateContext, generateTimeBasedRandomNumberString, - isModeSupportedByField, - getFieldMetaData, isValueEqual, isObjectEqual, getForQueryParams } from './common'; +import { + SCHEMA_STATE_ACTIONS, + SchemaStateContext, +} from './SchemaState'; export default SchemaView; export { + SCHEMA_STATE_ACTIONS, + BaseUISchema, DataGridView, FieldSetView, FormView, + InlineView, SchemaDialogView, SchemaPropertiesView, SchemaView, - BaseUISchema, - useSchemaState, - SCHEMA_STATE_ACTIONS, SchemaStateContext, + getForQueryParams, generateTimeBasedRandomNumberString, - isModeSupportedByField, - getFieldMetaData, isValueEqual, isObjectEqual, - getForQueryParams + useFieldState, + useSchemaState, }; diff --git a/web/pgadmin/static/js/SchemaView/options/common.js b/web/pgadmin/static/js/SchemaView/options/common.js new file mode 100644 index 00000000000..e27475119c3 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/options/common.js @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import _ from 'lodash'; +import { evalFunc } from 'sources/utils'; + + +export const FIELD_OPTIONS = '__fieldOptions'; + +export const booleanEvaluator = ({ + schema, field, option, value, viewHelperProps, options, defaultVal, +}) => ( + _.isUndefined(field?.[option]) ? defaultVal : + Boolean(evalFunc(schema, field[option], value, viewHelperProps, options)) +); + +export const evalIfNotDisabled = ({ options, ...params }) => ( + !options.disabled && + booleanEvaluator({ options, ...params }) +); + +export const canAddOrDelete = ({ + options, viewHelperProps, field, ...params +}) => ( + viewHelperProps?.mode != 'properties' && + !(field?.fixedRow) && + !options.disabled && + booleanEvaluator({ options, viewHelperProps, field, ...params }) +); + +export const evalInNonPropertyMode = ({ viewHelperProps, ...params }) => ( + viewHelperProps?.mode != 'properties' && + booleanEvaluator({ viewHelperProps, ...params }) +); diff --git a/web/pgadmin/static/js/SchemaView/options/index.js b/web/pgadmin/static/js/SchemaView/options/index.js new file mode 100644 index 00000000000..dbdd6ce2001 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/options/index.js @@ -0,0 +1,176 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { evalFunc } from 'sources/utils'; +import { + booleanEvaluator, + canAddOrDelete, + evalIfNotDisabled, + evalInNonPropertyMode, + FIELD_OPTIONS +} from './common'; +import { + evaluateFieldOptions, + evaluateFieldsOption, + registerOptionEvaluator, + schemaOptionsEvalulator, +} from './registry'; + +export { + FIELD_OPTIONS, + booleanEvaluator, + canAddOrDelete, + evaluateFieldOptions, + evaluateFieldsOption, + evalIfNotDisabled, + registerOptionEvaluator, + schemaOptionsEvalulator, +}; + +const VISIBLE = 'visible'; + +// Default evaluators +// 1. disabled +// 2. visible (It also checks for the supported mode) +// 3. readonly + +registerOptionEvaluator('disabled'); +registerOptionEvaluator( + VISIBLE, + // Evaluator + ({schema, field, value, viewHelperProps}) => ( + ( + !field.mode || field.mode.indexOf(viewHelperProps.mode) > -1 + ) && ( + // serverInfo not found + _.isUndefined(viewHelperProps.serverInfo) || + // serverInfo found and it's within range + (( + _.isUndefined(field.server_type) ? true : + (viewHelperProps.serverInfo.type in field.server_type) + ) && ( + _.isUndefined(field.min_version) ? true : + (viewHelperProps.serverInfo.version >= field.min_version) + ) && ( + _.isUndefined(field.max_version) ? true : + (viewHelperProps.serverInfo.version <= field.max_version) + )) + ) && ( + _.isUndefined(field[VISIBLE]) ? true : + Boolean(evalFunc(schema, field[VISIBLE], value)) + )), +); + +registerOptionEvaluator( + 'readonly', + // Evaluator + ({viewHelperProps, ...args}) => ( + viewHelperProps.inCatalog || + viewHelperProps.mode === 'properties' || + booleanEvaluator({viewHelperProps, ...args }) + ), + // Default value + false +); + + +// Collection evaluators +// 1. canAdd +// 2. canEdit +// 3. canAddRow +// 4. expandEditOnAdd +// 5. addOnTop +// 6. canSearch +registerOptionEvaluator( + 'canAdd', + // Evaluator + canAddOrDelete, + // Default value + true, + ['collection'] +); + +registerOptionEvaluator( + 'canEdit', + // Evaluator + ({viewHelperProps, options, ...args}) => ( + !viewHelperProps.inCatalog && + viewHelperProps.mode !== 'properties' && + !options.disabled && + booleanEvaluator({viewHelperProps, options, ...args }) + ), + // Default value + false, + ['collection'] +); + +registerOptionEvaluator( + 'canAddRow', + // Evaluator + ({options, ...args}) => ( + options.canAdd && + booleanEvaluator({options, ...args }) + ), + // Default value + true, + ['collection'] +); + +registerOptionEvaluator( + 'expandEditOnAdd', + // Evaluator + evalInNonPropertyMode, + // Default value + false, + ['collection'] +); + +registerOptionEvaluator( + 'addOnTop', + // Evaluator + evalInNonPropertyMode, + // Default value + false, + ['collection'] +); + +registerOptionEvaluator( + 'canSearch', + // Evaluator + evalInNonPropertyMode, + // Default value + false, + ['collection'] +); + +// Row evaluators +// 1. canEditRow +registerOptionEvaluator( + 'canEditRow', + // Evaluator + evalInNonPropertyMode, + // Default value + true, + ['row'] +); + +// Grid cell evaluatiors +// 1. editable +registerOptionEvaluator( + 'editable', + // Evaluator + ({viewHelperProps, ...args}) => ( + !viewHelperProps.inCatalog && + viewHelperProps.mode !== 'properties' && + booleanEvaluator({viewHelperProps, ...args }) + ), + // Default value + true, + ['cell'] +); diff --git a/web/pgadmin/static/js/SchemaView/options/registry.js b/web/pgadmin/static/js/SchemaView/options/registry.js new file mode 100644 index 00000000000..dce57bcc021 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/options/registry.js @@ -0,0 +1,156 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import _ from 'lodash'; +import { isModeSupportedByField } from '../common'; +import { FIELD_OPTIONS, booleanEvaluator } from './common'; + + +const COMMON_OPTIONS = '__common'; +const _optionEvaluators = { }; + + +export function registerOptionEvaluator(option, evaluator, defaultVal, types) { + types = types || [COMMON_OPTIONS]; + evaluator = evaluator || booleanEvaluator; + defaultVal = _.isUndefined(defaultVal) ? false : defaultVal; + + types.forEach((type) => { + const evaluators = _optionEvaluators[type] = + (_optionEvaluators[type] || []); + + evaluators.push([option, evaluator, defaultVal]); + }); +} + +export function evaluateFieldOption({ + option, schema, value, viewHelperProps, field, options, parentOptions, +}) { + if (option && option in _optionEvaluators) { + const evaluators = _optionEvaluators[option]; + evaluators?.forEach(([option, evaluator, defaultVal]) => { + options[option] = evaluator({ + schema, field, option, value, viewHelperProps, options, defaultVal, + parentOptions + }); + }); + } +} + +export function evaluateFieldOptions({ + schema, value, viewHelperProps, field, options={}, parentOptions=null +}) { + evaluateFieldOption({ + option: COMMON_OPTIONS, schema, value, viewHelperProps, field, options, + parentOptions + }); + evaluateFieldOption({ + option: field.type, schema, value, viewHelperProps, field, options, + parentOptions + }); +} + +export function schemaOptionsEvalulator({ + schema, data, accessPath=[], viewHelperProps, options, parentOptions=null, + inGrid=false +}) { + schema?.fields?.forEach((field) => { + // We could have multiple entries of same `field.id` for each mode, hence - + // we should process the options only if the current field is support for + // the given mode. + if (!isModeSupportedByField(field, viewHelperProps)) return; + + switch (field.type) { + case 'nested-tab': + case 'nested-fieldset': + case 'inline-groups': + { + if (!field.schema) return; + if (!field.schema.top) field.schema.top = schema.top || schema; + + const path = field.id ? [...accessPath, field.id] : accessPath; + + schemaOptionsEvalulator({ + schema: field.schema, data, path, viewHelperProps, options, + parentOptions + }); + } + + break; + + case 'collection': + { + if (!field.schema) return; + if (!field.schema.top) field.schema.top = schema.top || schema; + + const fieldPath = [...accessPath, field.id]; + const fieldOptionsPath = [...fieldPath, FIELD_OPTIONS]; + const fieldOptions = _.get(options, fieldOptionsPath, {}); + const rows = data[field.id]; + + evaluateFieldOptions({ + schema, value: data, viewHelperProps, field, + options: fieldOptions, parentOptions, + }); + + _.set(options, fieldOptionsPath, fieldOptions); + + const rowIndexes = [FIELD_OPTIONS]; + + rows?.forEach((row, idx) => { + const schemaPath = [...fieldPath, idx]; + const schemaOptions = _.get(options, schemaPath, {}); + + _.set(options, schemaPath, schemaOptions); + + schemaOptionsEvalulator({ + schema: field.schema, data: row, accessPath: [], + viewHelperProps, options: schemaOptions, + parentOptions: fieldOptions, inGrid: true + }); + + const rowPath = [...schemaPath, FIELD_OPTIONS]; + const rowOptions = _.get(options, rowPath, {}); + _.set(options, rowPath, rowOptions); + + evaluateFieldOption({ + option: 'row', schema: field.schema, value: row, viewHelperProps, + field, options: rowOptions, parentOptions: fieldOptions + }); + + rowIndexes.push(idx); + }); + + } + break; + + default: + { + const fieldPath = [...accessPath, field.id]; + const fieldOptionsPath = [...fieldPath, FIELD_OPTIONS]; + const fieldOptions = _.get(options, fieldOptionsPath, {}); + + evaluateFieldOptions({ + schema, value: data, viewHelperProps, field, options: fieldOptions, + parentOptions, + }); + + if (inGrid) { + evaluateFieldOption({ + option: 'cell', schema, value: data, viewHelperProps, field, + options: fieldOptions, parentOptions, + }); + } + + _.set(options, fieldOptionsPath, fieldOptions); + } + break; + } + }); +} diff --git a/web/pgadmin/static/js/SchemaView/registry.js b/web/pgadmin/static/js/SchemaView/registry.js new file mode 100644 index 00000000000..0c454f0b0d3 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/registry.js @@ -0,0 +1,42 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +/* + * Using the factory pattern (registry) to avoid circular imports of the views. + */ +const _views = {}; + +export function registerView(viewFunc, name) { + name = name || viewFunc.name; + + if (name in _views) { + throw new Error( + `View type '${name}' is alredy registered.` + ); + } + + if (typeof viewFunc !== 'function') { + throw new Error( + `View '${name}' must be a function.` + ); + } + + _views[name] = viewFunc; +} + +export function View(name) { + const view = _views[name]; + + if (view) return view; + throw new Error('View is not found in the registry.'); +} + +export function hasView(name) { + return (name in _views); +} diff --git a/web/pgadmin/static/js/SchemaView/useSchemaState.js b/web/pgadmin/static/js/SchemaView/useSchemaState.js deleted file mode 100644 index 7bfc0308b63..00000000000 --- a/web/pgadmin/static/js/SchemaView/useSchemaState.js +++ /dev/null @@ -1,489 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2024, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import React, { useEffect, useReducer } from 'react'; - -import _ from 'lodash'; - -import { parseApiError } from 'sources/api_instance'; -import gettext from 'sources/gettext'; - -import { DepListener } from './DepListener'; -import { - getSchemaDataDiff, - validateSchema, -} from './schemaUtils'; - - -export const SchemaStateContext = React.createContext(); - -export const SCHEMA_STATE_ACTIONS = { - INIT: 'init', - SET_VALUE: 'set_value', - ADD_ROW: 'add_row', - DELETE_ROW: 'delete_row', - MOVE_ROW: 'move_row', - RERENDER: 'rerender', - CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue', - DEFERRED_DEPCHANGE: 'deferred_depchange', - BULK_UPDATE: 'bulk_update', -}; - -const getDepChange = (currPath, newState, oldState, action) => { - if(action.depChange) { - newState = action.depChange(currPath, newState, { - type: action.type, - path: action.path, - value: action.value, - oldState: _.cloneDeep(oldState), - listener: action.listener, - }); - } - return newState; -}; - -const getDeferredDepChange = (currPath, newState, oldState, action) => { - if(action.deferredDepChange) { - return action.deferredDepChange(currPath, newState, { - type: action.type, - path: action.path, - value: action.value, - depChange: action.depChange, - oldState: _.cloneDeep(oldState), - }); - } -}; - -/* - * The main function which manipulates the session state based on actions. - * - * The state is managed based on path array of a particular key. - * For Eg. if the state is - * { - * key1: { - * ckey1: [ - * {a: 0, b: 0}, - * {a: 1, b: 1} - * ] - * } - * } - * - * The path for b in first row will be '[key1, ckey1, 0, b]'. - * The path for second row of ckey1 will be '[key1, ckey1, 1]'. - * - * The path for key1 is '[key1]'. - * The state starts with path '[]'. - */ -const sessDataReducer = (state, action) => { - let data = _.cloneDeep(state); - let rows, cid, deferredList; - data.__deferred__ = data.__deferred__ || []; - - switch(action.type) { - case SCHEMA_STATE_ACTIONS.INIT: - data = action.payload; - break; - - case SCHEMA_STATE_ACTIONS.BULK_UPDATE: - rows = (_.get(data, action.path)||[]); - rows.forEach((row) => { row[action.id] = false; }); - _.set(data, action.path, rows); - break; - - case SCHEMA_STATE_ACTIONS.SET_VALUE: - _.set(data, action.path, action.value); - // If there is any dep listeners get the changes. - data = getDepChange(action.path, data, state, action); - deferredList = getDeferredDepChange(action.path, data, state, action); - data.__deferred__ = deferredList || []; - break; - - case SCHEMA_STATE_ACTIONS.ADD_ROW: - // Create id to identify a row uniquely, usefull when getting diff. - cid = _.uniqueId('c'); - action.value['cid'] = cid; - if (action.addOnTop) { - rows = [].concat(action.value).concat(_.get(data, action.path)||[]); - } else { - rows = (_.get(data, action.path)||[]).concat(action.value); - } - _.set(data, action.path, rows); - // If there is any dep listeners get the changes. - data = getDepChange(action.path, data, state, action); - break; - - case SCHEMA_STATE_ACTIONS.DELETE_ROW: - rows = _.get(data, action.path)||[]; - rows.splice(action.value, 1); - _.set(data, action.path, rows); - // If there is any dep listeners get the changes. - data = getDepChange(action.path, data, state, action); - break; - - case SCHEMA_STATE_ACTIONS.MOVE_ROW: - rows = _.get(data, action.path)||[]; - var row = rows[action.oldIndex]; - rows.splice(action.oldIndex, 1); - rows.splice(action.newIndex, 0, row); - _.set(data, action.path, rows); - break; - - case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE: - data.__deferred__ = []; - return data; - - case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE: - data = getDepChange(action.path, data, state, action); - break; - } - - data.__changeId = (data.__changeId || 0) + 1; - - return data; -}; - -function prepareData(val, createMode=false) { - if(_.isPlainObject(val)) { - _.forIn(val, function (el) { - if (_.isObject(el)) { - prepareData(el, createMode); - } - }); - } else if(_.isArray(val)) { - val.forEach(function(el) { - if (_.isPlainObject(el)) { - /* The each row in collection need to have an id to identify them uniquely - This helps in easily getting what has changed */ - /* Nested collection rows may or may not have idAttribute. - So to decide whether row is new or not set, the cid starts with - nn (not new) for existing rows. Newly added will start with 'c' (created) - */ - el['cid'] = createMode ? _.uniqueId('c') : _.uniqueId('nn'); - prepareData(el, createMode); - } - }); - } - return val; -} - -const LOADING_STATE = { - INIT: 'initializing', - LOADING: 'loading', - LOADED: 'loaded', - ERROR: 'Error' -}; - -export class SchemaState extends DepListener { - - constructor( - schema, getInitData, immutableData, mode, keepCid, onDataChange - ) { - super(); - - ////// Helper variables - - // BaseUISchema instance - this.schema = schema; - // Current mode of operation ('create', 'edit', 'properties') - this.mode = mode; - // Keep the 'cid' object during diff calculations. - this.keepcid = keepCid; - // Initialization callback - this.getInitData = getInitData; - // Data change callback - this.onDataChange = onDataChange; - - ////// State variables - - // Is is ready to be consumed? - this.isReady = false; - // Diff between the current snapshot and initial data. - this.changes = null; - // Loading message (if any) - this.message = null; - // Current Loading state - this.loadingState = LOADING_STATE.INIT; - this.hasChanges = false; - - ////// Schema instance data - - // Initial data after the ready state - this.initData = {}; - // Current state of the data - this.data = {}; - // Immutable data - this.immutableData = immutableData; - // Current error - this.errors = {}; - // Pre-ready queue - this.preReadyQueue = []; - - this._id = Date.now(); - } - - setError(err) { - this.errors = err; - } - - setReady(state) { - this.isReady = state; - } - - setLoadingState(loadingState) { - this.loadingState = loadingState; - } - - setLoadingMessage(msg) { - this.message = msg; - } - - // Initialise the data, and fetch the data from the backend (if required). - // 'force' flag can be used for reloading the data from the backend. - initialise(dataDispatch, force) { - let state = this; - - // Don't attempt to initialize again (if it's already in progress). - if ( - state.loadingState !== LOADING_STATE.INIT || - (force && state.loadingState === LOADING_STATE.LOADING) - ) return; - - state.setLoadingState(LOADING_STATE.LOADING); - state.setLoadingMessage(gettext('Loading...')); - - /* - * Fetch the data using getInitData(..) callback. - * `getInitData(..)` must be present in 'edit' mode. - */ - if(state.mode === 'edit' && !state.getInitData) { - throw new Error('getInitData must be passed for edit'); - } - - const initDataPromise = state.getInitData?.() || - Promise.resolve({}); - - initDataPromise.then((data) => { - data = data || {}; - - if(state.mode === 'edit') { - // Set the origData to incoming data, useful for comparing. - state.initData = prepareData({...data, ...state.immutableData}); - } else { - // In create mode, merge with defaults. - state.initData = prepareData({ - ...state.schema.defaults, ...data, ...state.immutableData - }, true); - } - - state.schema.initialise(state.initData); - - dataDispatch({ - type: SCHEMA_STATE_ACTIONS.INIT, - payload: state.initData, - }); - - state.setLoadingState(LOADING_STATE.LOADED); - state.setLoadingMessage(''); - state.setReady(true); - }).catch((err) => { - state.setLoadingMessage(''); - state.setError({ - name: 'apierror', - response: err, - message: _.escape(parseApiError(err)), - }); - state.setLoadingState(LOADING_STATE.ERROR); - state.setReady(true); - }); - } - - validate(sessData) { - let state = this, - schema = state.schema; - - // If schema does not have the data or does not have any 'onDataChange' - // callback, there is no need to validate the current data. - if(!state.isReady) return; - - if( - !validateSchema(schema, sessData, (path, message) => { - message && state.setError({ name: path, message: _.escape(message) }); - }) - ) state.setError({}); - - state.data = sessData; - state.changes = state.Changes(); - state.onDataChange && state.onDataChange(state.hasChanges, state.changes); - } - - Changes(includeSkipChange=false) { - const state = this; - const sessData = this.data; - const schema = state.schema; - - // Check if anything changed. - let dataDiff = getSchemaDataDiff( - schema, state.initData, sessData, - state.mode, state.keepCid, false, includeSkipChange - ); - state.hasChanges = Object.keys(dataDiff).length > 0; - - // Inform the callbacks about change in the data. - if(state.mode !== 'edit') { - // Merge the changed data with origData in 'create' mode. - dataDiff = _.assign({}, state.initData, dataDiff); - - // Remove internal '__changeId' attribute. - delete dataDiff.__changeId; - - // In case of 'non-edit' mode, changes are always there. - return dataDiff; - } else if (state.hasChanges) { - const idAttr = schema.idAttribute; - const idVal = state.initData[idAttr]; - // Append 'idAttr' only if it actually exists - if (idVal) dataDiff[idAttr] = idVal; - - return dataDiff; - } - - return {}; - } - - get isNew() { - return this.schema.isNew(this.initData); - } - - set isNew(val) { - throw new Error('Property \'isNew\' is readonly.', val); - } - - get isDirty() { - return this.hasChanges; - } - - set isDirty(val) { - throw new Error('Property \'isDirty\' is readonly.', val); - } -} - -export const useSchemaState = ({ - schema, getInitData, immutableData, mode, keepCid, onDataChange, -}) => { - let schemaState = schema.state; - - if (!schemaState) { - schemaState = new SchemaState( - schema, getInitData, immutableData, mode, keepCid, onDataChange - ); - schema.state = schemaState; - } - - const [sessData, sessDispatch] = useReducer( - sessDataReducer, {...(_.cloneDeep(schemaState.data)), __changeId: 0} - ); - - const sessDispatchWithListener = (action) => { - let dispatchPayload = { - ...action, - depChange: (...args) => schemaState.getDepChange(...args), - deferredDepChange: (...args) => schemaState.getDeferredDepChange(...args), - }; - /* - * All the session changes coming before init should be queued up. - * They will be processed later when form is ready. - */ - let preReadyQueue = schemaState.preReadyQueue; - - preReadyQueue ? - preReadyQueue.push(dispatchPayload) : - sessDispatch(dispatchPayload); - }; - - schemaState.setUnpreparedData = (path, value) => { - if(path) { - let data = prepareData(value); - _.set(schema.initData, path, data); - sessDispatchWithListener({ - type: SCHEMA_STATE_ACTIONS.SET_VALUE, - path: path, - value: data, - }); - } - }; - - const resetData = () => { - const initData = _.cloneDeep(schemaState.initData); - initData.__changeId = sessData.__changeId; - sessDispatch({ - type: SCHEMA_STATE_ACTIONS.INIT, - payload: initData, - }); - }; - - const reload = () => { - schemaState.initialise(sessDispatch, true); - }; - - useEffect(() => { - schemaState.initialise(sessDispatch); - }, [schemaState.loadingState]); - - useEffect(() => { - let preReadyQueue = schemaState.preReadyQueue; - - if (!schemaState.isReady || !preReadyQueue) return; - - for (const payload of preReadyQueue) { - sessDispatch(payload); - } - - // Destroy the queue so that no one uses it. - schemaState.preReadyQueue = null; - }, [schemaState.isReady]); - - useEffect(() => { - // Validate the schema on the change of the data. - schemaState.validate(sessData); - }, [schemaState.isReady, sessData.__changeId]); - - useEffect(() => { - const items = sessData.__deferred__ || []; - - if (items.length == 0) return; - - sessDispatch({ - type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE, - }); - - items.forEach((item) => { - item.promise.then((resFunc) => { - sessDispatch({ - type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE, - path: item.action.path, - depChange: item.action.depChange, - listener: { - ...item.listener, - callback: resFunc, - }, - }); - }); - }); - }, [sessData.__deferred__?.length]); - - schemaState.reload = reload; - schemaState.reset = resetData; - - return { - schemaState, - dataDispatch: sessDispatchWithListener, - sessData, - reset: resetData, - }; -}; diff --git a/web/pgadmin/static/js/SchemaView/utils/createFieldControls.jsx b/web/pgadmin/static/js/SchemaView/utils/createFieldControls.jsx new file mode 100644 index 00000000000..18f2c98c063 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/utils/createFieldControls.jsx @@ -0,0 +1,205 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import _ from 'lodash'; + +import gettext from 'sources/gettext'; + +import { SCHEMA_STATE_ACTIONS } from '../SchemaState'; +import { isModeSupportedByField } from '../common'; +import { View, hasView } from '../registry'; +import { StaticMappedFormControl, MappedFormControl } from '../MappedControl'; + + +const DEFAULT_TAB = 'general'; + +export const createFieldControls = ({ + schema, schemaState, accessPath, viewHelperProps, dataDispatch +}) => { + + const { mode } = (viewHelperProps || {}); + const isPropertyMode = mode === 'properties'; + const groups = []; + const groupsById = {}; + let currentGroup = null; + + const createGroup = (id, label, visible, field, isFullTab) => { + const group = { + id: id, + label: label, + visible: visible, + field: field, + className: isFullTab ? ( + isPropertyMode ? 'Properties-noPadding' : 'FormView-fullSpace' + ) : '', + controls: [], + inlineGroups: {}, + isFullTab: isFullTab + }; + + groups.push(group); + groupsById[id] = group; + + return group; + }; + + // Create default group - 'General'. + createGroup(DEFAULT_TAB, gettext('General'), true); + + schema?.fields?.forEach((field) => { + if (!isModeSupportedByField(field, viewHelperProps)) return; + + let inlineGroup = null; + const inlineGroupId = field[inlineGroup]; + + if(field.type === 'group') { + + if (!field.id || (field.id in groups)) { + throw new Error('Group-id must be unique within a schema.'); + } + + const { visible } = schemaState.options(accessPath.concat(field.id)); + createGroup(field.id, field.label, visible, field); + + return; + } + + if (field.isFullTab) { + if (field.type === inlineGroup) + throw new Error('Inline group can not be full tab control'); + + const { visible } = schemaState.options(accessPath.concat(field.id)); + currentGroup = createGroup( + field.id, field.label, visible, field, true + ); + } else { + const { group } = field; + + currentGroup = groupsById[group || DEFAULT_TAB]; + + if (!currentGroup) { + const newGroup = createGroup(group, group, true); + currentGroup = newGroup; + } + + // Generate inline-view if necessary, or use existing one. + if (inlineGroupId) { + inlineGroup = currentGroup.inlineGroups[inlineGroupId]; + if (!inlineGroup) { + inlineGroup = currentGroup.inlineGroups[inlineGroupId] = { + control: View('InlineView'), + controlProps: { + viewHelperProps: viewHelperProps, + field: null, + }, + controls: [], + }; + currentGroup.controls.push(inlineGroup); + } + } + } + + if (field.type === inlineGroup) { + if (inlineGroupId) { + throw new Error('inline-group can not be created within inline-group'); + } + inlineGroup = currentGroup.inlineGroups[inlineGroupId]; + if (inlineGroup) { + throw new Error('inline-group must be unique-id within a tab group'); + } + inlineGroup = currentGroup.inlineGroups[inlineGroupId] = { + control: View('InlineView'), + controlProps: { + accessPath: schemaState.accessPath(accessPath, field.id), + viewHelperProps: viewHelperProps, + field: field, + }, + controls: [], + }; + currentGroup.controls.push(inlineGroup); + return; + } + + let control = null; + const controlProps = { + key: field.id, + accessPath: schemaState.accessPath(accessPath, field.id), + viewHelperProps: viewHelperProps, + dataDispatch: dataDispatch, + field: field, + }; + + switch (field.type) { + case 'nested-tab': + // We don't support nested-tab in 'properties' mode. + if (isPropertyMode) return; + + control = View('FormView'); + controlProps['isNested'] = true; + break; + case 'nested-fieldset': + control = View('FieldSetView'); + controlProps['controlClassName'] = + isPropertyMode ? 'Properties-controlRow' : 'FormView-controlRow'; + break; + case 'collection': + control = View('DataGridView'); + controlProps['containerClassName'] = + isPropertyMode ? 'Properties-controlRow' : 'FormView-controlRow'; + break; + default: + { + control = ( + hasView(field.type) ? View(field.type) : ( + field.id ? MappedFormControl : StaticMappedFormControl + ) + ); + + if (inlineGroup) { + controlProps['withContainer'] = false; + controlProps['controlGridBasis'] = 3; + } + + controlProps['className'] = field.isFullTab ? '' : ( + isPropertyMode ? 'Properties-controlRow' : 'FormView-controlRow' + ); + + if (field.id) { + controlProps['id'] = field.id; + controlProps['onChange'] = (changeValue) => { + // Get the changes on dependent fields as well. + dataDispatch?.({ + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + path: controlProps.accessPath, + value: changeValue, + }); + }; + } + } + break; + } + + // Use custom control over the standard one. + if (field.CustomControl) { + control = field.CustomControl; + } + + if (isPropertyMode) field.helpMessage = ''; + + // Its a form control. + if (_.isEqual(accessPath.concat(field.id), schemaState.errors?.name)) + currentGroup.hasError = true; + + (inlineGroup || currentGroup).controls.push({control, controlProps}); + }); + + return groups.filter( + (group) => (group.visible && group.controls.length) + ); +}; diff --git a/web/pgadmin/static/js/SchemaView/utils/index.js b/web/pgadmin/static/js/SchemaView/utils/index.js new file mode 100644 index 00000000000..b68395f1dfb --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/utils/index.js @@ -0,0 +1,17 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { createFieldControls } from './createFieldControls'; +import { listenDepChanges } from './listenDepChanges'; + + +export { + createFieldControls, + listenDepChanges, +}; diff --git a/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js b/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js new file mode 100644 index 00000000000..6214da30553 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js @@ -0,0 +1,57 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { useEffect } from 'react'; +import _ from 'lodash'; + +import { evalFunc } from 'sources/utils'; + + +export const listenDepChanges = (accessPath, field, visible, schemaState) => { + + useEffect(() => { + if (!visible || !schemaState || !field) return; + + if(field.depChange || field.deferredDepChange) { + schemaState.addDepListener( + accessPath, accessPath, + field.depChange, field.deferredDepChange + ); + } + + if (field.deps) { + const parentPath = [...accessPath]; + + // Remove the last element. + if (field.id && field.id === parentPath[parentPath.length - 1]) { + parentPath.pop(); + } + + (evalFunc(null, field.deps) || []).forEach((dep) => { + + // When dep is a string then prepend the complete accessPath, + // but - when dep is an array, then the intention is to provide + // the exact accesspath. + let source = _.isArray(dep) ? dep : parentPath.concat(dep); + + if(field.depChange || field.deferredDepChange) { + schemaState.addDepListener( + source, accessPath, field.depChange, field.deferredDepChange + ); + } + }); + } + + return () => { + // Cleanup the listeners when unmounting. + schemaState.removeDepListener(accessPath); + }; + }, []); + +}; diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index efd8aeffe75..0ba592aec89 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -80,6 +80,9 @@ const Root = styled('div')(({theme}) => ({ backgroundColor: theme.otherVars.borderColor, padding: theme.spacing(1), }, + '& .Form-plainstring': { + padding: theme.spacing(0.5), + } })); @@ -626,7 +629,6 @@ export function InputRadio({ helpid, value, onChange, controlProps, readonly, la inputProps={{ 'aria-label': value, 'aria-describedby': helpid }} style={{ padding: 0 }} disableRipple - {...props} /> } label={controlProps.label} @@ -1110,7 +1112,9 @@ export function PlainString({ controlProps, value }) { if (controlProps?.formatter) { finalValue = controlProps.formatter.fromRaw(finalValue); } - return {finalValue}; + return +
{finalValue}
+
; } PlainString.propTypes = { controlProps: PropTypes.object, diff --git a/web/pgadmin/static/js/components/PgReactTableStyled.jsx b/web/pgadmin/static/js/components/PgReactTableStyled.jsx index 5726faa5bb5..c1fe9f763ed 100644 --- a/web/pgadmin/static/js/components/PgReactTableStyled.jsx +++ b/web/pgadmin/static/js/components/PgReactTableStyled.jsx @@ -14,7 +14,6 @@ import PropTypes from 'prop-types'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded'; import EditRoundedIcon from '@mui/icons-material/EditRounded'; import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; import { PgIconButton } from './Buttons'; @@ -441,18 +440,6 @@ export function getCheckboxHeaderCell({title}) { return Cell; } -export function getReorderCell() { - const Cell = () => { - return
- -
; - }; - - Cell.displayName = 'ReorderCell'; - - return Cell; -} - export function getEditCell({isDisabled, title}) { const Cell = ({ row }) => { return } className='pgrt-cell-button' diff --git a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx index f7c22f74484..4a7d9231b06 100644 --- a/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx +++ b/web/pgadmin/static/js/components/ReactCodeMirror/components/Editor.jsx @@ -10,7 +10,10 @@ import React, { useEffect, useMemo, useRef } from 'react'; import ReactDOMServer from 'react-dom/server'; import PropTypes from 'prop-types'; -import { checkTrojanSource } from '../../../utils'; + +import { useIsMounted } from 'sources/custom_hooks'; + +import { checkTrojanSource } from 'sources/utils'; import usePreferences from '../../../../../preferences/static/js/store'; import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded'; import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; @@ -147,9 +150,12 @@ const defaultExtensions = [ ]; export default function Editor({ - currEditor, name, value, options, onCursorActivity, onChange, readonly, disabled, autocomplete = false, - breakpoint = false, onBreakPointChange, showActiveLine=false, - keepHistory = true, cid, helpid, labelledBy, customKeyMap, language='pgsql'}) { + currEditor, name, value, options, onCursorActivity, onChange, readonly, + disabled, autocomplete = false, breakpoint = false, onBreakPointChange, + showActiveLine=false, keepHistory = true, cid, helpid, labelledBy, + customKeyMap, language='pgsql' +}) { + const checkIsMounted = useIsMounted(); const editorContainerRef = useRef(); const editor = useRef(); @@ -166,6 +172,7 @@ export default function Editor({ const editableConfig = useRef(new Compartment()); useEffect(() => { + if (!checkIsMounted()) return; const finalOptions = { ...defaultOptions, ...options }; const finalExtns = [ (language == 'json') ? json() : sql({dialect: PgSQL}), @@ -248,6 +255,7 @@ export default function Editor({ }, []); useMemo(() => { + if (!checkIsMounted()) return; if(editor.current) { if(value != editor.current.getValue()) { if(!_.isEmpty(value)) { @@ -259,14 +267,19 @@ export default function Editor({ } }, [value]); - useEffect(()=>{ - const keys = keymap.of([customKeyMap??[], defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat()); + useEffect(() => { + if (!checkIsMounted()) return; + const keys = keymap.of([ + customKeyMap??[], defaultKeymap, closeBracketsKeymap, historyKeymap, + foldKeymap, completionKeymap + ].flat()); editor.current?.dispatch({ effects: shortcuts.current.reconfigure(keys) }); }, [customKeyMap]); useEffect(() => { + if (!checkIsMounted()) return; let pref = preferencesStore.getPreferencesForModule('sqleditor'); let newConfigExtn = []; @@ -361,6 +374,7 @@ export default function Editor({ }, [preferencesStore]); useMemo(() => { + if (!checkIsMounted()) return; if (editor.current) { if (value != editor.current.getValue()) { editor.current.dispatch({ @@ -371,6 +385,7 @@ export default function Editor({ }, [value]); useEffect(() => { + if (!checkIsMounted()) return; editor.current?.dispatch({ effects: editableConfig.current.reconfigure([ EditorView.editable.of(editable), @@ -379,7 +394,7 @@ export default function Editor({ }); }, [readonly, disabled, keepHistory]); - return useMemo(()=>( + return useMemo(() => (
), []); } diff --git a/web/pgadmin/static/js/components/SearchInputText.jsx b/web/pgadmin/static/js/components/SearchInputText.jsx new file mode 100644 index 00000000000..51cd06b3c63 --- /dev/null +++ b/web/pgadmin/static/js/components/SearchInputText.jsx @@ -0,0 +1,49 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import React from 'react'; +import PropTypes from 'prop-types'; + +import { InputText } from 'sources/components/FormComponents'; +import gettext from 'sources/gettext'; + + +export const SEARCH_INPUT_SIZE = { + FULL: 'full', + HALF: 'half', +}; + +export const SEARCH_INPUT_ALIGNMENT = { + LEFT: 'left', + RIGHT: 'right' +}; + +export const SearchInputText = ({ + searchText, onChange, placeholder, size, alignment +}) => { + const props = { + placeholder: placeholder || gettext('Search'), + style: { + width: size == SEARCH_INPUT_SIZE.FULL ? '100%' : '50%', + float: alignment == SEARCH_INPUT_ALIGNMENT.RIGHT ? 'right' : 'left', + }, + value: searchText, + onChange, + }; + + return ; +}; + +SearchInputText.propTypes = { + searchText: PropTypes.string.isRequired, + onChange: PropTypes.func, + placeholder: PropTypes.string, + size: PropTypes.oneOf(Object.values(SEARCH_INPUT_SIZE)), + alignment: PropTypes.oneOf(Object.values(SEARCH_INPUT_ALIGNMENT)), +}; diff --git a/web/pgadmin/static/js/helpers/DataGridViewWithHeaderForm.jsx b/web/pgadmin/static/js/helpers/DataGridViewWithHeaderForm.jsx deleted file mode 100644 index 95cfd429bfd..00000000000 --- a/web/pgadmin/static/js/helpers/DataGridViewWithHeaderForm.jsx +++ /dev/null @@ -1,103 +0,0 @@ -///////////////////////////////////////////////////////////// -// -// pgAdmin 4 - PostgreSQL Tools -// -// Copyright (C) 2013 - 2024, The pgAdmin Development Team -// This software is released under the PostgreSQL Licence -// -////////////////////////////////////////////////////////////// - -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { styled } from '@mui/material/styles'; -import { Box } from '@mui/material'; -import DataGridView, { DataGridHeader } from '../SchemaView/DataGridView'; -import SchemaView, { SCHEMA_STATE_ACTIONS } from '../SchemaView'; -import { DefaultButton } from '../components/Buttons'; -import { evalFunc } from '../utils'; -import PropTypes from 'prop-types'; -import CustomPropTypes from '../custom_prop_types'; -import _ from 'lodash'; - -const StyledBox = styled(Box)(({theme}) => ({ - '& .DataGridViewWithHeaderForm-border': { - ...theme.mixins.panelBorder, - borderBottom: 0, - '& .DataGridViewWithHeaderForm-body': { - padding: '0.25rem', - '& .DataGridViewWithHeaderForm-addBtn': { - marginLeft: 'auto', - } - }, - }, -})); - -export default function DataGridViewWithHeaderForm(props) { - let {containerClassName, headerSchema, headerVisible, ...otherProps} = props; - - const headerFormData = useRef({}); - const schemaRef = useRef(otherProps.schema); - const [addDisabled, setAddDisabled] = useState(true); - const [headerFormResetKey, setHeaderFormResetKey] = useState(0); - const onAddClick = useCallback(()=>{ - if(!otherProps.canAddRow) { - return; - } - - let newRow = headerSchema.getNewData(headerFormData.current); - otherProps.dataDispatch({ - type: SCHEMA_STATE_ACTIONS.ADD_ROW, - path: otherProps.accessPath, - value: newRow, - }); - setHeaderFormResetKey((preVal)=>preVal+1); - }, []); - - useEffect(()=>{ - headerSchema.top = schemaRef.current.top; - }, []); - - let state = schemaRef.current.top ? _.get(schemaRef.current.top.sessData, _.slice(otherProps.accessPath, 0, -1)) - : _.get(schemaRef.current.sessData); - - headerVisible = headerVisible && evalFunc(null, headerVisible, state); - return ( - - - {props.label && } - {headerVisible && - Promise.resolve({})} - schema={headerSchema} - viewHelperProps={props.viewHelperProps} - showFooter={false} - onDataChange={(isDataChanged, dataChanged)=>{ - headerFormData.current = dataChanged; - setAddDisabled(headerSchema.addDisabled(headerFormData.current)); - }} - hasSQL={false} - isTabView={false} - resetKey={headerFormResetKey} - /> - - Add - - } - - - - ); -} - -DataGridViewWithHeaderForm.propTypes = { - label: PropTypes.string, - value: PropTypes.array, - viewHelperProps: PropTypes.object, - formErr: PropTypes.object, - headerSchema: CustomPropTypes.schemaUI.isRequired, - headerVisible: PropTypes.func, - schema: CustomPropTypes.schemaUI, - accessPath: PropTypes.array.isRequired, - dataDispatch: PropTypes.func.isRequired, - containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), -}; diff --git a/web/pgadmin/static/js/helpers/withStandardTabInfo.jsx b/web/pgadmin/static/js/helpers/withStandardTabInfo.jsx index b22cfbf2f61..6521785a39c 100644 --- a/web/pgadmin/static/js/helpers/withStandardTabInfo.jsx +++ b/web/pgadmin/static/js/helpers/withStandardTabInfo.jsx @@ -23,7 +23,7 @@ export default function withStandardTabInfo(Component, tabId) { const [isActive, setIsActive] = React.useState(false); const layoutDocker = useContext(LayoutDockerContext); - useEffect(()=>{ + useEffect(() => { const i = pgAdmin.Browser.tree?.selected(); if(i) { setNodeInfo([true, i, pgAdmin.Browser.tree.itemData(i)]); @@ -38,22 +38,24 @@ export default function withStandardTabInfo(Component, tabId) { } }, 100); - const onUpdate = (item, data)=>{ - setNodeInfo([true, item, data]); + const onUpdate = () => { + // Only use the selected tree node item. + const item = pgAdmin.Browser.tree?.selected(); + setNodeInfo([ + true, item, item && pgAdmin.Browser.tree.itemData(item) + ]); }; let destroyTree = pgAdmin.Browser.Events.on('pgadmin-browser:tree:destroyed', onUpdate); let deregisterTree = pgAdmin.Browser.Events.on('pgadmin-browser:node:selected', onUpdate); let deregisterTreeUpdate = pgAdmin.Browser.Events.on('pgadmin-browser:tree:updated', onUpdate); let deregisterDbConnected = pgAdmin.Browser.Events.on('pgadmin:database:connected', onUpdate); - let deregisterServerConnected = pgAdmin.Browser.Events.on('pgadmin:server:connected', (_sid, item, data)=>{ - setNodeInfo([true, item, data]); - }); + let deregisterServerConnected = pgAdmin.Browser.Events.on('pgadmin:server:connected', onUpdate); let deregisterActive = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.ACTIVE, onTabActive); // if there is any dock changes to the tab and it appears to be active/inactive let deregisterChange = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.CHANGE, onTabActive); - return ()=>{ + return () => { onTabActive?.cancel(); destroyTree(); deregisterTree(); diff --git a/web/pgadmin/static/js/utils.js b/web/pgadmin/static/js/utils.js index 9c3dbfe3d34..edd3fdbc41f 100644 --- a/web/pgadmin/static/js/utils.js +++ b/web/pgadmin/static/js/utils.js @@ -677,3 +677,62 @@ export function getChartColor(index, theme='light', colorPalette=CHART_THEME_COL // loop back if out of index; return palette[index % palette.length]; } + +// Using this function instead of 'btoa' directly. +// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem +function stringToBase64(str) { + return btoa( + Array.from( + new TextEncoder().encode(str), + (byte) => String.fromCodePoint(byte), + ).join('') + ); +} + +/************************************ + * + * Memoization of a function. + * + * NOTE: Please don't use the function, when: + * - One of the parameter in the arguments could have a 'circular' dependency. + * NOTE: We use `JSON.stringify(...)` for all the arguments.`You could + * introduce 'Object.prototype.toJSON(...)' function for the object + * with circular dependency, which should return a JSON object without + * it. + * - It returns a Promise object (asynchronous functions). + * + * Consider to use 'https://github.com/sindresorhus/p-memoize' for an + * asychronous functions. + * + **/ +export const memoizeFn = fn => new Proxy(fn, { + cache: new Map(), + apply (target, thisArg, argsList) { + let cacheKey = stringToBase64(JSON.stringify(argsList)); + if(!this.cache.has(cacheKey)) { + this.cache.set(cacheKey, target.apply(thisArg, argsList)); + } + return this.cache.get(cacheKey); + } +}); + +export const memoizeTimeout = (fn, time) => new Proxy(fn, { + cache: new Map(), + apply (target, thisArg, argsList) { + const cacheKey = stringToBase64(JSON.stringify(argsList)); + const cached = this.cache.get(cacheKey); + const timeoutId = setTimeout(() => (this.cache.delete(cacheKey)), time); + + if (cached) { + clearInterval(cached.timeoutId); + cached.timeoutId = timeoutId; + + return cached.result; + } + + const result = target.apply(thisArg, argsList); + this.cache.set(cacheKey, {result, timeoutId}); + + return result; + } +}); diff --git a/web/pgadmin/tools/backup/static/js/backup.ui.js b/web/pgadmin/tools/backup/static/js/backup.ui.js index 6cd4569d421..7514064cefd 100644 --- a/web/pgadmin/tools/backup/static/js/backup.ui.js +++ b/web/pgadmin/tools/backup/static/js/backup.ui.js @@ -40,7 +40,7 @@ export class SectionSchema extends BaseUISchema { state.only_tablespaces || state.only_roles; }, - inlineNext: true, + inlineGroup: 'section', }, { id: 'data', label: gettext('Data'), @@ -53,6 +53,7 @@ export class SectionSchema extends BaseUISchema { state.only_tablespaces || state.only_roles; }, + inlineGroup: 'section', }, { id: 'post_data', label: gettext('Post-data'), @@ -65,6 +66,7 @@ export class SectionSchema extends BaseUISchema { state.only_tablespaces || state.only_roles; }, + inlineGroup: 'section', } ]; } @@ -105,7 +107,7 @@ export class TypeObjSchema extends BaseUISchema { state.only_tablespaces || state.only_roles; }, - inlineNext: true, + inlineGroup: 'type_of_objects', }, { id: 'only_schema', label: gettext('Only schemas'), @@ -121,7 +123,7 @@ export class TypeObjSchema extends BaseUISchema { state.only_tablespaces || state.only_roles; }, - inlineNext: true, + inlineGroup: 'type_of_objects', }, { id: 'only_tablespaces', label: gettext('Only tablespaces'), @@ -137,8 +139,8 @@ export class TypeObjSchema extends BaseUISchema { state.only_schema || state.only_roles; }, - visible: isVisibleForObjectBackup(obj?._top?.backupType), - inlineNext: true, + visible: isVisibleForObjectBackup(obj?.top?.backupType), + inlineGroup: 'type_of_objects', }, { id: 'only_roles', label: gettext('Only roles'), @@ -146,6 +148,7 @@ export class TypeObjSchema extends BaseUISchema { group: gettext('Type of objects'), deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_schema', 'only_tablespaces'], + inlineGroup: 'type_of_objects', disabled: function(state) { return state.pre_data || state.data || @@ -154,14 +157,15 @@ export class TypeObjSchema extends BaseUISchema { state.only_schema || state.only_tablespaces; }, - visible: isVisibleForObjectBackup(obj?._top?.backupType) + visible: isVisibleForObjectBackup(obj?.top?.backupType) }, { id: 'blobs', label: gettext('Blobs'), type: 'switch', group: gettext('Type of objects'), + inlineGroup: 'type_of_objects', visible: function(state) { - if (!isVisibleForServerBackup(obj?._top?.backupType)) { + if (!isVisibleForServerBackup(obj?.top?.backupType)) { state.blobs = false; return false; } @@ -200,43 +204,43 @@ export class SaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', }, { id: 'dns_no_role_passwords', label: gettext('Role passwords'), type: 'switch', disabled: false, group: gettext('Do not save'), - visible: isVisibleForObjectBackup(obj?._top?.backupType), - inlineNext: true, + visible: isVisibleForObjectBackup(obj?.top?.backupType), + inlineGroup: 'do_not_save', }, { id: 'dns_privilege', label: gettext('Privileges'), type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', }, { id: 'dns_tablespace', label: gettext('Tablespaces'), type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', }, { id: 'dns_unlogged_tbl_data', label: gettext('Unlogged table data'), type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', }, { id: 'dns_comments', label: gettext('Comments'), type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', min_version: 110000 }, { id: 'dns_publications', @@ -244,7 +248,7 @@ export class SaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', min_version: 110000 }, { id: 'dns_subscriptions', @@ -252,7 +256,7 @@ export class SaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', min_version: 110000 }, { id: 'dns_security_labels', @@ -260,7 +264,7 @@ export class SaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', min_version: 110000 }, { id: 'dns_toast_compression', @@ -268,7 +272,7 @@ export class SaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', min_version: 140000 }, { id: 'dns_table_access_method', @@ -276,7 +280,7 @@ export class SaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'do_not_save', min_version: 150000 }]; } @@ -322,13 +326,14 @@ export class DisabledOptionSchema extends BaseUISchema { disabled: function(state) { return !(state.only_data); }, - inlineNext: true, + inlineGroup: 'disable', }, { id: 'disable_quoting', label: gettext('$ quoting'), type: 'switch', disabled: false, group: gettext('Disable'), + inlineGroup: 'disable', }]; } } @@ -364,28 +369,29 @@ export class MiscellaneousSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Miscellaneous'), - inlineNext: true, + inlineGroup: 'miscellaneous', }, { id: 'dqoute', label: gettext('Force double quote on identifiers'), type: 'switch', disabled: false, group: gettext('Miscellaneous'), - inlineNext: true, + inlineGroup: 'miscellaneous', }, { id: 'use_set_session_auth', label: gettext('Use SET SESSION AUTHORIZATION'), type: 'switch', disabled: false, group: gettext('Miscellaneous'), - inlineNext: true, + inlineGroup: 'miscellaneous', }, { id: 'exclude_schema', label: gettext('Exclude schema'), type: 'select', disabled: false, group: gettext('Miscellaneous'), - visible: isVisibleForServerBackup(obj?._top?.backupType), + inlineGroup: 'miscellaneous', + visible: isVisibleForServerBackup(obj?.top?.backupType), controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' } }, { id: 'exclude_database', @@ -394,7 +400,7 @@ export class MiscellaneousSchema extends BaseUISchema { disabled: false, min_version: 160000, group: gettext('Miscellaneous'), - visible: isVisibleForObjectBackup(obj?._top?.backupType), + visible: isVisibleForObjectBackup(obj?.top?.backupType), controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' } }, { id: 'extra_float_digits', @@ -440,7 +446,7 @@ export class ExcludePatternsSchema extends BaseUISchema { type: 'select', disabled: false, group: gettext('Table Options'), - visible: isVisibleForServerBackup(obj?._top?.backupType), + visible: isVisibleForServerBackup(obj?.top?.backupType), controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' } }, { id: 'exclude_table_data', @@ -448,7 +454,7 @@ export class ExcludePatternsSchema extends BaseUISchema { type: 'select', disabled: false, group: gettext('Table Options'), - visible: isVisibleForServerBackup(obj?._top?.backupType), + visible: isVisibleForServerBackup(obj?.top?.backupType), controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' } }, { id: 'exclude_table_and_children', @@ -457,7 +463,7 @@ export class ExcludePatternsSchema extends BaseUISchema { disabled: false, group: gettext('Table Options'), min_version: 160000, - visible: isVisibleForServerBackup(obj?._top?.backupType), + visible: isVisibleForServerBackup(obj?.top?.backupType), controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' } }, { id: 'exclude_table_data_and_children', @@ -466,7 +472,7 @@ export class ExcludePatternsSchema extends BaseUISchema { disabled: false, group: gettext('Table Options'), min_version: 160000, - visible: isVisibleForServerBackup(obj?._top?.backupType), + visible: isVisibleForServerBackup(obj?.top?.backupType), controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' } }]; } @@ -638,7 +644,7 @@ export default class BackupSchema extends BaseUISchema { state.on_conflict_do_nothing = false; return true; }, - inlineNext: obj.backupType !== 'server', + inlineGroup: 'miscellaneous', }, { id: 'include_create_database', label: gettext('Include CREATE DATABASE statement'), @@ -646,7 +652,7 @@ export default class BackupSchema extends BaseUISchema { disabled: false, group: gettext('Query Options'), visible: isVisibleForServerBackup(obj.backupType), - inlineNext: true, + inlineGroup: 'miscellaneous', }, { id: 'include_drop_database', label: gettext('Include DROP DATABASE statement'), @@ -660,7 +666,7 @@ export default class BackupSchema extends BaseUISchema { } return false; }, - inlineNext: true, + inlineGroup: 'miscellaneous', }, { id: 'if_exists', label: gettext('Include IF EXISTS clause'), @@ -674,6 +680,7 @@ export default class BackupSchema extends BaseUISchema { state.if_exists = false; return true; }, + inlineGroup: 'miscellaneous', }, { id: 'use_column_inserts', label: gettext('Use Column INSERTS'), diff --git a/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx b/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx index b989662a131..8f306970e68 100644 --- a/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx +++ b/web/pgadmin/tools/debugger/static/js/components/DebuggerArgumentComponent.jsx @@ -818,9 +818,9 @@ export default function DebuggerArgumentComponent({ debuggerInfo, restartDebug, onDataChange={(isChanged, changedData) => { let isValid = false; let skipStep = false; - if ('_sessData' in debuggerArgsSchema.current) { + if ('sessData' in debuggerArgsSchema.current) { isValid = true; - debuggerArgsSchema.current._sessData.aregsCollection.forEach((data) => { + debuggerArgsSchema.current.sessData.aregsCollection.forEach((data) => { if (skipStep) { return; } diff --git a/web/pgadmin/tools/maintenance/static/js/maintenance.ui.js b/web/pgadmin/tools/maintenance/static/js/maintenance.ui.js index fea91b7dd98..79913fad4bf 100644 --- a/web/pgadmin/tools/maintenance/static/js/maintenance.ui.js +++ b/web/pgadmin/tools/maintenance/static/js/maintenance.ui.js @@ -37,7 +37,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('FULL'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -53,7 +53,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('FREEZE'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -69,7 +69,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('ANALYZE'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -85,7 +85,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op', 'vacuum_full'], type: 'switch', label: gettext('DISABLE PAGE SKIPPING'), - inlineNext: true, + inlineGroup: 'operations', disabled: function(state) { if (!obj.isApplicableForVacuum(state) || state.vacuum_full) { state.vacuum_disable_page_skipping = false; @@ -101,7 +101,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('SKIP LOCKED'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return state?.op ? (state.op == 'VACUUM' || state.op == 'ANALYZE') : false; }, @@ -118,7 +118,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op', 'vacuum_full'], type: 'switch', label: gettext('TRUNCATE'), - inlineNext: true, + inlineGroup: 'operations', disabled: function(state) { if (!obj.isApplicableForVacuum(state) || state.vacuum_full) { state.vacuum_truncate = false; @@ -135,7 +135,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('PROCESS TOAST'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -152,7 +152,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('PROCESS MAIN'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -169,7 +169,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('SKIP DATABASE STATS'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -186,7 +186,7 @@ export class VacuumSchema extends BaseUISchema { deps: ['op'], type: 'switch', label: gettext('ONLY DATABASE STATS'), - inlineNext: true, + inlineGroup: 'operations', visible: function(state) { return obj.isApplicableForVacuum(state); }, @@ -202,6 +202,7 @@ export class VacuumSchema extends BaseUISchema { id: 'vacuum_index_cleanup', deps: ['op', 'vacuum_full'], type: 'select', + inlineGroup: 'operations', label: gettext('INDEX CLEANUP'), controlProps: { allowClear: false, width: '100%' }, options: function () { @@ -277,7 +278,7 @@ export class VacuumSchema extends BaseUISchema { return obj.isApplicableForReindex(state); }, disabled: function(state) { - if (!obj.isApplicableForReindex(state) || obj?._top?.nodeInfo?.schema) { + if (!obj.isApplicableForReindex(state) || obj?.top?.nodeInfo?.schema) { state.reindex_system = false; return true; } diff --git a/web/pgadmin/tools/restore/static/js/restore.ui.js b/web/pgadmin/tools/restore/static/js/restore.ui.js index c8e030c5313..52dd5245246 100644 --- a/web/pgadmin/tools/restore/static/js/restore.ui.js +++ b/web/pgadmin/tools/restore/static/js/restore.ui.js @@ -42,7 +42,7 @@ export class RestoreSectionSchema extends BaseUISchema { label: gettext('Pre-data'), type: 'switch', group: gettext('Sections'), - inlineNext: true, + inlineGroup: 'sections', deps: ['only_data', 'only_schema'], disabled: function(state) { return obj.isDisabled(state); @@ -52,7 +52,7 @@ export class RestoreSectionSchema extends BaseUISchema { label: gettext('Data'), type: 'switch', group: gettext('Sections'), - inlineNext: true, + inlineGroup: 'sections', deps: ['only_data', 'only_schema'], disabled: function(state) { return obj.isDisabled(state); @@ -62,6 +62,7 @@ export class RestoreSectionSchema extends BaseUISchema { label: gettext('Post-data'), type: 'switch', group: gettext('Sections'), + inlineGroup: 'sections', deps: ['only_data', 'only_schema'], disabled: function(state) { return obj.isDisabled(state); @@ -97,7 +98,7 @@ export class RestoreTypeObjSchema extends BaseUISchema { label: gettext('Only data'), type: 'switch', group: gettext('Type of objects'), - inlineNext: true, + inlineGroup: 'types_of_data', deps: ['pre_data', 'data', 'post_data', 'only_schema'], disabled: function(state) { if(obj.selectedNodeType == 'table') { @@ -115,6 +116,7 @@ export class RestoreTypeObjSchema extends BaseUISchema { label: gettext('Only schema'), type: 'switch', group: gettext('Type of objects'), + inlineGroup: 'types_of_data', deps: ['pre_data', 'data', 'post_data', 'only_data'], disabled: function(state) { if(obj.selectedNodeType == 'index' || obj.selectedNodeType == 'function') { @@ -159,28 +161,28 @@ export class RestoreSaveOptSchema extends BaseUISchema { label: gettext('Owner'), type: 'switch', disabled: false, - inlineNext: true, + inlineGroup: 'save_options', group: gettext('Do not save'), }, { id: 'dns_privilege', label: gettext('Privileges'), type: 'switch', disabled: false, - inlineNext: true, + inlineGroup: 'save_options', group: gettext('Do not save'), }, { id: 'dns_tablespace', label: gettext('Tablespaces'), type: 'switch', disabled: false, - inlineNext: true, + inlineGroup: 'save_options', group: gettext('Do not save'), }, { id: 'dns_comments', label: gettext('Comments'), type: 'switch', disabled: false, - inlineNext: true, + inlineGroup: 'save_options', group: gettext('Do not save'), min_version: 110000 }, { @@ -189,7 +191,7 @@ export class RestoreSaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'save_options', min_version: 110000 }, { id: 'dns_subscriptions', @@ -197,7 +199,7 @@ export class RestoreSaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'save_options', min_version: 110000 }, { id: 'dns_security_labels', @@ -205,7 +207,7 @@ export class RestoreSaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'save_options', min_version: 110000 }, { id: 'dns_table_access_method', @@ -213,7 +215,7 @@ export class RestoreSaveOptSchema extends BaseUISchema { type: 'switch', disabled: false, group: gettext('Do not save'), - inlineNext: true, + inlineGroup: 'save_options', min_version: 150000 }]; } @@ -419,7 +421,7 @@ export default class RestoreSchema extends BaseUISchema { label: gettext('Clean before restore'), type: 'switch', group: gettext('Query Options'), - inlineNext: true, + inlineGroup: 'clean', disabled: function(state) { if(obj.selectedNodeType === 'function' || obj.selectedNodeType === 'trigger_function') { state.clean = true; @@ -431,6 +433,7 @@ export default class RestoreSchema extends BaseUISchema { label: gettext('Include IF EXISTS clause'), type: 'switch', group: gettext('Query Options'), + inlineGroup: 'clean', deps: ['clean'], disabled: function(state) { if (state.clean) { diff --git a/web/pgadmin/tools/sqleditor/static/js/components/dialogs/MacrosDialog.jsx b/web/pgadmin/tools/sqleditor/static/js/components/dialogs/MacrosDialog.jsx index 790d665f95a..8cd994faac5 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/dialogs/MacrosDialog.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/dialogs/MacrosDialog.jsx @@ -27,7 +27,9 @@ class MacrosCollection extends BaseUISchema { } /* Returns the new data row for the schema based on defaults and input */ - getNewData(current_macros, data={}) { + getNewData(data={}) { + const current_macros = this?.top?.sessData.macro; + let newRow = {}; this.fields.forEach((field)=>{ newRow[field.id] = this.defaults[field.id]; @@ -36,7 +38,8 @@ class MacrosCollection extends BaseUISchema { ...newRow, ...data, }; - if (current_macros){ + + if (current_macros) { // Extract an array of existing names from the 'macro' collection const existingNames = current_macros.map(macro => macro.name); const newName = getRandomName(existingNames); diff --git a/web/pgadmin/tools/sqleditor/static/js/show_view_data.js b/web/pgadmin/tools/sqleditor/static/js/show_view_data.js index 4e6c399c3c6..f5eb2dc8b0c 100644 --- a/web/pgadmin/tools/sqleditor/static/js/show_view_data.js +++ b/web/pgadmin/tools/sqleditor/static/js/show_view_data.js @@ -161,7 +161,7 @@ function showFilterDialog(pgBrowser, item, queryToolMod, transId, let helpUrl = url_for('help.static', {'filename': 'viewdata_filter.html'}); let okCallback = function() { - queryToolMod.launch(transId, gridUrl, false, queryToolTitle, {sql_filter: schema._sessData.filter_sql}); + queryToolMod.launch(transId, gridUrl, false, queryToolTitle, {sql_filter: schema.sessData.filter_sql}); }; pgBrowser.Events.trigger('pgadmin:utility:show', item, diff --git a/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx b/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx index 181703f62aa..08ff1724ad8 100644 --- a/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx +++ b/web/pgadmin/tools/user_management/static/js/UserManagementDialog.jsx @@ -166,8 +166,8 @@ class UserManagementCollection extends BaseUISchema { } if (state.auth_source != AUTH_METHODS['INTERNAL']) { - if (obj.isNew(state) && obj.top?._sessData?.userManagement) { - for (let user of obj.top._sessData.userManagement) { + if (obj.isNew(state) && obj.top?.sessData?.userManagement) { + for (let user of obj.top.sessData.userManagement) { if (user?.id && user.username.toLowerCase() == state.username.toLowerCase() && user.auth_source == state.auth_source) { @@ -193,8 +193,8 @@ class UserManagementCollection extends BaseUISchema { setError('email', null); } - if (obj.isNew(state) && obj.top?._sessData?.userManagement) { - for (let user of obj.top._sessData.userManagement) { + if (obj.isNew(state) && obj.top?.sessData?.userManagement) { + for (let user of obj.top.sessData.userManagement) { if (user?.id && user.email?.toLowerCase() == state.email?.toLowerCase()) { msg = gettext('Email address \'%s\' already exists', state.email); @@ -303,6 +303,7 @@ class UserManagementSchema extends BaseUISchema { }, { id: 'refreshBrowserTree', visible: false, type: 'switch', + mode: ['non_supported'], deps: ['userManagement'], depChange: ()=> { return { refreshBrowserTree: this.changeOwnership }; } diff --git a/web/regression/javascript/SchemaView/store.spec.js b/web/regression/javascript/SchemaView/store.spec.js new file mode 100644 index 00000000000..16af37a206b --- /dev/null +++ b/web/regression/javascript/SchemaView/store.spec.js @@ -0,0 +1,157 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { isValueEqual } from '../../../pgadmin/static/js/SchemaView/common'; +import { + createStore +} from '../../../pgadmin/static/js/SchemaView/SchemaState/store'; + +const initData = { + id: 1, + field1: 'field1val', + field2: 1, + fieldcoll: [ + {field3: 1, field4: 'field4val1', field5: 'field5val1'}, + {field3: 2, field4: 'field4val2', field5: 'field5val2'}, + ], + field3: 3, + field4: 'field4val', +}; + +describe('store', ()=>{ + describe('', () => { + + it('getState', () => { + const store = createStore(initData); + const data = store.getState(); + expect(isValueEqual(data, initData)).toBe(true); + }); + + it('get', () => { + const store = createStore(initData); + + const firstField3 = store.get(['fieldcoll', 0, 'field3']); + expect(firstField3 == 1).toBe(true); + + const firstFieldCollRow = store.get(['fieldcoll', '0']); + // Sending a copy of the data, and not itself. + expect(isValueEqual(firstFieldCollRow, initData.fieldcoll[0])).toBe(true); + }); + + it('setState', () => { + const store = createStore(initData); + const newData = {a: 1}; + + store.setState(newData); + + const newState = store.getState(); + expect(Object.is(newState, newData)).toBe(false); + expect(isValueEqual(newState, newData)).toBe(true); + }); + + it ('set', () => { + const store = createStore(initData); + const newData = {a: 1}; + + store.set(newData); + + let newState = store.getState(); + expect(Object.is(newState, newData)).toBe(false); + expect(isValueEqual(newState, newData)).toBe(true); + + store.set((prevState) => ({...prevState, initData})); + + newState = store.getState(); + expect(Object.is(newState, initData)).toBe(false); + expect(isValueEqual(newState, initData)).toBe(false); + + delete newState['a']; + + store.set(() => (newState)); + + newState = store.getState(); + expect(isValueEqual(newState, initData)).toBe(false); + }); + + it ('subscribe', () => { + const store = createStore(initData); + const listener = jest.fn(); + + const unsubscribe1 = store.subscribe(listener); + store.set((prevState) => (prevState)); + + expect(listener).not.toHaveBeenCalled(); + + store.set((prevState) => { + prevState.id = 2; + return prevState; + }); + + expect(listener).toHaveBeenCalled(); + + const listenForFirstField3 = jest.fn(); + const unsubscribe2 = store.subscribeForPath( + ['fieldcoll', '0', 'field3'], listenForFirstField3 + ); + const listenForSecondField3 = jest.fn(); + const unsubscribe3 = store.subscribeForPath( + ['fieldcoll', '1', 'field3'], listenForSecondField3 + ); + let changeTo = 10; + + store.set((prevState) => { + prevState.fieldcoll[0].field3 = changeTo; + return prevState; + }); + + expect(listenForFirstField3).toHaveBeenCalled(); + expect(listener).toHaveBeenCalledTimes(2); + expect(listenForSecondField3).not.toHaveBeenCalled(); + + store.set((prevState) => { + // There is no actual change from previous state. + prevState.fieldcoll[0].field3 = 10; + return prevState; + }); + + // Not expecting it be called. + expect(listenForFirstField3).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledTimes(2); + expect(listenForSecondField3).not.toHaveBeenCalled(); + + unsubscribe1(); + + store.set((prevState) => { + prevState.fieldcoll[0].field3 = 50; + return prevState; + }); + + // Don't expect this to be called again. + expect(listener).toHaveBeenCalledTimes(2); + // Expect this one to be called + expect(listenForFirstField3).toHaveBeenCalledTimes(2); + expect(listenForSecondField3).not.toHaveBeenCalled(); + + unsubscribe2(); + + store.set((prevState) => { + prevState.fieldcoll[0].field3 = 100; + return prevState; + }); + + // Don't expect any of them to be called. + expect(listener).toHaveBeenCalledTimes(2); + expect(listenForFirstField3).toHaveBeenCalledTimes(2); + expect(listenForSecondField3).not.toHaveBeenCalled(); + + unsubscribe3(); + }); + + }); +}); diff --git a/web/regression/javascript/schema_ui_files/aggregate.ui.spec.js b/web/regression/javascript/schema_ui_files/aggregate.ui.spec.js index 45de402b705..3da4324b8d8 100644 --- a/web/regression/javascript/schema_ui_files/aggregate.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/aggregate.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('AggregateSchema', ()=>{ - let schemaObj = new AggregateSchema(); + let createSchemaObj = () => new AggregateSchema(); let getInitData = ()=>Promise.resolve({}); @@ -25,15 +25,15 @@ describe('AggregateSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/cast.ui.spec.js b/web/regression/javascript/schema_ui_files/cast.ui.spec.js index dfc378f84e8..5c9ff1c01e7 100644 --- a/web/regression/javascript/schema_ui_files/cast.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/cast.ui.spec.js @@ -14,12 +14,13 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('CastSchema', ()=>{ - let schemaObj = new CastSchema( + let createSchemaObj = () => new CastSchema( { getTypeOptions: ()=>[], getFuncOptions: ()=>[], }, ); + const schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); @@ -29,15 +30,15 @@ describe('CastSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('srctyp depChange', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/catalog.ui.spec.js b/web/regression/javascript/schema_ui_files/catalog.ui.spec.js index 313e21febd6..87cfa858e23 100644 --- a/web/regression/javascript/schema_ui_files/catalog.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/catalog.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('CatalogSchema', ()=>{ - let catalogObj = new CatalogSchema( + let createCatalogObj = () => new CatalogSchema( { namespaceowner: '', } @@ -29,15 +29,15 @@ describe('CatalogSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(catalogObj); + await getCreateView(createCatalogObj()); }); it('edit', async ()=>{ - await getEditView(catalogObj, getInitData); + await getEditView(createCatalogObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(catalogObj, getInitData); + await getPropertiesView(createCatalogObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/catalog_object_column.ui.spec.js b/web/regression/javascript/schema_ui_files/catalog_object_column.ui.spec.js index 4744bdedfed..9efd384d549 100644 --- a/web/regression/javascript/schema_ui_files/catalog_object_column.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/catalog_object_column.ui.spec.js @@ -13,23 +13,19 @@ import {genericBeforeEach, getCreateView, getPropertiesView} from '../genericFun describe('CatalogObjectColumn', ()=>{ - let schemaObj = new CatalogObjectColumn(); + let createSchemaObj = () => new CatalogObjectColumn(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/check_constraint.ui.spec.js b/web/regression/javascript/schema_ui_files/check_constraint.ui.spec.js index 24dc14f11f3..102f53a001d 100644 --- a/web/regression/javascript/schema_ui_files/check_constraint.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/check_constraint.ui.spec.js @@ -35,7 +35,8 @@ function getFieldDepChange(schema, id) { describe('CheckConstraintSchema', ()=>{ - let schemaObj = new CheckConstraintSchema(); + let createSchemaObj = () => new CheckConstraintSchema(); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); @@ -47,15 +48,15 @@ describe('CheckConstraintSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('create collection', async ()=>{ diff --git a/web/regression/javascript/schema_ui_files/collation.ui.spec.js b/web/regression/javascript/schema_ui_files/collation.ui.spec.js index 06c72e1d8e7..9b9855e1ed4 100644 --- a/web/regression/javascript/schema_ui_files/collation.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/collation.ui.spec.js @@ -12,8 +12,7 @@ import CollationSchema from '../../../pgadmin/browser/server_groups/servers/data import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; describe('CollationsSchema', () => { - - let schemaObj = new CollationSchema( + const createSchemaObj = () => new CollationSchema( { rolesList: () => [], schemaList: () => [], @@ -24,24 +23,23 @@ describe('CollationsSchema', () => { schema: '' } ); + let schemaObj = createSchemaObj(); let getInitData = () => Promise.resolve({}); - - beforeEach(() => { genericBeforeEach(); }); it('create', () => { - getCreateView(schemaObj); + getCreateView(createSchemaObj()); }); it('edit', () => { - getEditView(schemaObj, getInitData); + getEditView(createSchemaObj(), getInitData); }); it('properties', () => { - getPropertiesView(schemaObj, getInitData); + getPropertiesView(createSchemaObj(), getInitData); }); it('validate', () => { diff --git a/web/regression/javascript/schema_ui_files/column.ui.spec.js b/web/regression/javascript/schema_ui_files/column.ui.spec.js index 95b36e84ef3..4f2ace8fbd5 100644 --- a/web/regression/javascript/schema_ui_files/column.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/column.ui.spec.js @@ -46,13 +46,13 @@ function getFieldDepChange(schema, id) { } describe('ColumnSchema', ()=>{ - - let schemaObj = new ColumnSchema( + const createSchemaObj = () => new ColumnSchema( ()=>new MockSchema(), {}, ()=>Promise.resolve([]), ()=>Promise.resolve([]), ); + let schemaObj = createSchemaObj(); let datatypes = [ {value: 'numeric', length: true, precision: true, min_val: 1, max_val: 140391}, {value: 'character varying', length: true, precision: false, min_val: 1, max_val: 140391}, @@ -64,15 +64,15 @@ describe('ColumnSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('create collection', async ()=>{ diff --git a/web/regression/javascript/schema_ui_files/compound_trigger.ui.spec.js b/web/regression/javascript/schema_ui_files/compound_trigger.ui.spec.js index 39403b9973b..2bcae257e61 100644 --- a/web/regression/javascript/schema_ui_files/compound_trigger.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/compound_trigger.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('CompoundTriggerSchema', ()=>{ - let schemaObj = new CompoundTriggerSchema( + const createSchemaObj = () => new CompoundTriggerSchema( { columns: [], }, @@ -23,26 +23,23 @@ describe('CompoundTriggerSchema', ()=>{ table: {} } ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/database.ui.spec.js b/web/regression/javascript/schema_ui_files/database.ui.spec.js index 1986228d2ae..de1f3581466 100644 --- a/web/regression/javascript/schema_ui_files/database.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/database.ui.spec.js @@ -12,6 +12,7 @@ import _ from 'lodash'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import DatabaseSchema from '../../../pgadmin/browser/server_groups/servers/databases/static/js/database.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; +import { initializeSchemaWithData } from './utils'; class MockSchema extends BaseUISchema { get baseFields() { @@ -21,7 +22,7 @@ class MockSchema extends BaseUISchema { describe('DatabaseSchema', ()=>{ - let schemaObj = new DatabaseSchema( + const createSchemaObj = () => new DatabaseSchema( ()=>new MockSchema(), ()=>new MockSchema(), { @@ -35,30 +36,29 @@ describe('DatabaseSchema', ()=>{ { datowner: 'postgres', } - ); + ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); - it('schema_res depChange', ()=>{ + it('schema_res depChange', () => { + initializeSchemaWithData(schemaObj, {}); let depChange = _.find(schemaObj.fields, (f)=>f.id=='schema_res').depChange; depChange({schema_res: 'abc'}); expect(schemaObj.informText).toBe('Please refresh the Schemas node to make changes to the schema restriction take effect.'); diff --git a/web/regression/javascript/schema_ui_files/domain.ui.spec.js b/web/regression/javascript/schema_ui_files/domain.ui.spec.js index fb236e58072..14cc207b8fc 100644 --- a/web/regression/javascript/schema_ui_files/domain.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/domain.ui.spec.js @@ -14,7 +14,7 @@ import {addNewDatagridRow, genericBeforeEach, getCreateView, getEditView, getPro describe('DomainSchema', ()=>{ - let schemaObj = new DomainSchema( + const createSchemaObj = () => new DomainSchema( { role: ()=>[], schema: ()=>[], @@ -31,23 +31,20 @@ describe('DomainSchema', ()=>{ let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/domain_constraint.ui.spec.js b/web/regression/javascript/schema_ui_files/domain_constraint.ui.spec.js index a992b29fa01..d986538dc6e 100644 --- a/web/regression/javascript/schema_ui_files/domain_constraint.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/domain_constraint.ui.spec.js @@ -13,27 +13,24 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('DomainConstraintSchema', ()=>{ - let schemaObj = new DomainConstraintSchema(); + let createSchemaObj = () => new DomainConstraintSchema(); let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/edbfunc.ui.spec.js b/web/regression/javascript/schema_ui_files/edbfunc.ui.spec.js index 7465b525def..0aeaae41370 100644 --- a/web/regression/javascript/schema_ui_files/edbfunc.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/edbfunc.ui.spec.js @@ -13,7 +13,8 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('EDBFuncSchema', ()=>{ - let edbFuncSchemaObj = new EDBFuncSchema( + + let edbFuncSchemaObj = () => new EDBFuncSchema( {}, { name: 'sysfunc' } @@ -21,23 +22,20 @@ describe('EDBFuncSchema', ()=>{ let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(edbFuncSchemaObj); + await getCreateView(edbFuncSchemaObj()); }); it('edit', async ()=>{ - await getEditView(edbFuncSchemaObj, getInitData); + await getEditView(edbFuncSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(edbFuncSchemaObj, getInitData); + await getPropertiesView(edbFuncSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/edbvar.ui.spec.js b/web/regression/javascript/schema_ui_files/edbvar.ui.spec.js index 7418ae832ee..d889b84f6e2 100644 --- a/web/regression/javascript/schema_ui_files/edbvar.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/edbvar.ui.spec.js @@ -13,27 +13,23 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('EDBVarSchema', ()=>{ - let edbVarSchemaObj = new EDBVarSchema(); + let edbVarSchemaObj = () => new EDBVarSchema(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(edbVarSchemaObj); + await getCreateView(edbVarSchemaObj()); }); it('edit', async ()=>{ - await getEditView(edbVarSchemaObj, getInitData); + await getEditView(edbVarSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(edbVarSchemaObj, getInitData); + await getPropertiesView(edbVarSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js b/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js index c837452b72f..9657c6fc27b 100644 --- a/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/event_trigger.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('EventTriggerSchema', ()=>{ - let schemaObj = new EventTriggerSchema( + const createSchemaObj = () => new EventTriggerSchema( { role: ()=>[], function_names: ()=>[], @@ -22,26 +22,24 @@ describe('EventTriggerSchema', ()=>{ eventowner: 'postgres' } ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/exclusion_constraint.ui.spec.js b/web/regression/javascript/schema_ui_files/exclusion_constraint.ui.spec.js index 6acc967fbe1..6df9630446b 100644 --- a/web/regression/javascript/schema_ui_files/exclusion_constraint.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/exclusion_constraint.ui.spec.js @@ -40,13 +40,14 @@ function getFieldDepChange(schema, id) { describe('ExclusionConstraintSchema', ()=>{ - let schemaObj; + const createSchemaObject = () => getNodeExclusionConstraintSchema( + {}, {}, {Nodes: {table: {}}} + ); let getInitData = ()=>Promise.resolve({}); beforeAll(()=>{ jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue(Promise.resolve([])); jest.spyOn(nodeAjax, 'getNodeListByName').mockReturnValue(Promise.resolve([])); - schemaObj = getNodeExclusionConstraintSchema({}, {}, {Nodes: {table: {}}}); }); beforeEach(()=>{ @@ -54,18 +55,19 @@ describe('ExclusionConstraintSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('create collection', async ()=>{ + let schemaObj = createSchemaObject(); let schemaCollObj = new SchemaInColl(schemaObj); const {ctrl, user} = await getCreateView(schemaCollObj); /* Make sure you hit every corner */ @@ -73,6 +75,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); it('changeColumnOptions', ()=>{ + let schemaObj = createSchemaObject(); jest.spyOn(schemaObj.exHeaderSchema, 'changeColumnOptions'); let columns = [{label: 'label', value: 'value'}]; schemaObj.changeColumnOptions(columns); @@ -80,6 +83,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); describe('ExclusionColHeaderSchema', ()=>{ + let schemaObj = createSchemaObject(); it('getNewData', ()=>{ schemaObj.exHeaderSchema.columnOptions = [ {label: 'id', value: 'id', datatype: 'numeric'}, @@ -111,6 +115,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); describe('ExclusionColumnSchema', ()=>{ + let schemaObj = createSchemaObject(); it('isEditable', ()=>{ schemaObj.exColumnSchema.isNewExCons = false; expect(schemaObj.exColumnSchema.isEditable()).toBe(false); @@ -125,6 +130,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); it('depChange', ()=>{ + let schemaObj = createSchemaObject(); let state = {columns: [{column: 'id'}]}; schemaObj.top = new TableSchema({}, null); @@ -169,6 +175,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); it('columns formatter', ()=>{ + let schemaObj = createSchemaObject(); let formatter = _.find(schemaObj.fields, (f)=>f.id=='columns').cell().controlProps.formatter; expect(formatter.fromRaw([{ column: 'lid', @@ -180,6 +187,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); describe('amname change', ()=>{ + let schemaObj = createSchemaObject(); let confirmSpy; let deferredDepChange; let operClassOptions = [ @@ -243,6 +251,7 @@ describe('ExclusionConstraintSchema', ()=>{ }); it('validate', ()=>{ + let schemaObj = createSchemaObject(); let state = {}; let setError = jest.fn(); @@ -255,4 +264,3 @@ describe('ExclusionConstraintSchema', ()=>{ expect(schemaObj.validate(state, setError)).toBe(false); }); }); - diff --git a/web/regression/javascript/schema_ui_files/extension.ui.spec.js b/web/regression/javascript/schema_ui_files/extension.ui.spec.js index d9e062186f3..a44f136f79c 100644 --- a/web/regression/javascript/schema_ui_files/extension.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/extension.ui.spec.js @@ -13,32 +13,31 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('ExtensionSchema', ()=>{ - let schemaObj = new ExtensionsSchema( + const createSchemaObj = () => new ExtensionsSchema( { extensionsList: ()=>[], schemaList: ()=>[], } ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/foreign_data_wrapper.ui.spec.js b/web/regression/javascript/schema_ui_files/foreign_data_wrapper.ui.spec.js index 4c330a0a6f1..0570f77759f 100644 --- a/web/regression/javascript/schema_ui_files/foreign_data_wrapper.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/foreign_data_wrapper.ui.spec.js @@ -20,7 +20,7 @@ class MockSchema extends BaseUISchema { describe('ForeignDataWrapperSchema', ()=>{ - let schemaObj = new ForeignDataWrapperSchema( + const createSchemaObj = () => new ForeignDataWrapperSchema( ()=>new MockSchema(), { role: ()=>[], @@ -34,23 +34,20 @@ describe('ForeignDataWrapperSchema', ()=>{ let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/foreign_key.ui.spec.js b/web/regression/javascript/schema_ui_files/foreign_key.ui.spec.js index 25d1a4ae3d8..908c2d107c0 100644 --- a/web/regression/javascript/schema_ui_files/foreign_key.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/foreign_key.ui.spec.js @@ -48,8 +48,6 @@ describe('ForeignKeySchema', ()=>{ schemaObj = getNodeForeignKeySchema({}, {}, {Nodes: {table: {}}}); }); - - beforeEach(()=>{ genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/foreign_server.ui.spec.js b/web/regression/javascript/schema_ui_files/foreign_server.ui.spec.js index 5bce4de9fca..0d07db5c118 100644 --- a/web/regression/javascript/schema_ui_files/foreign_server.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/foreign_server.ui.spec.js @@ -20,7 +20,7 @@ class MockSchema extends BaseUISchema { describe('ForeignServerSchema', ()=>{ - let schemaObj = new ForeignServerSchema( + const createSchemaObj = () => new ForeignServerSchema( ()=>new MockSchema(), { role: ()=>[], @@ -31,24 +31,20 @@ describe('ForeignServerSchema', ()=>{ ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/foreign_table.ui.spec.js b/web/regression/javascript/schema_ui_files/foreign_table.ui.spec.js index a271497a428..b8de33f3498 100644 --- a/web/regression/javascript/schema_ui_files/foreign_table.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/foreign_table.ui.spec.js @@ -19,8 +19,7 @@ class MockSchema extends BaseUISchema { } describe('ForeignTableSchema', ()=>{ - - let schemaObj = new ForeignTableSchema( + const createSchemaObj = () => new ForeignTableSchema( ()=>new MockSchema(), ()=>new MockSchema(), ()=>new MockSchema(), @@ -38,6 +37,7 @@ describe('ForeignTableSchema', ()=>{ } } ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); beforeEach(()=>{ @@ -45,15 +45,15 @@ describe('ForeignTableSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/fts_configuration.ui.spec.js b/web/regression/javascript/schema_ui_files/fts_configuration.ui.spec.js index af65849ddb5..05e926c071b 100644 --- a/web/regression/javascript/schema_ui_files/fts_configuration.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/fts_configuration.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('FTSConfigurationSchema', ()=>{ - let schemaObj = new FTSConfigurationSchema( + const createSchemaObj = () => new FTSConfigurationSchema( { role: ()=>[], schema: ()=>[], @@ -27,26 +27,23 @@ describe('FTSConfigurationSchema', ()=>{ schema: 'public', } ); - let getInitData = ()=>Promise.resolve({}); - - - - + let schemaObj = createSchemaObj(); + let getInitData = () => Promise.resolve({}); beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/fts_dictionary.ui.spec.js b/web/regression/javascript/schema_ui_files/fts_dictionary.ui.spec.js index cfafae3d9ea..3d34ba33b12 100644 --- a/web/regression/javascript/schema_ui_files/fts_dictionary.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/fts_dictionary.ui.spec.js @@ -12,8 +12,7 @@ import FTSDictionarySchema from '../../../pgadmin/browser/server_groups/servers/ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; describe('FTSDictionarySchema', ()=>{ - - let schemaObj = new FTSDictionarySchema( + const createSchemaObj = () => new FTSDictionarySchema( { role: ()=>[], schema: ()=>[], @@ -28,23 +27,20 @@ describe('FTSDictionarySchema', ()=>{ let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/fts_parser.ui.spec.js b/web/regression/javascript/schema_ui_files/fts_parser.ui.spec.js index f8ff68d7ef6..e8331849e65 100644 --- a/web/regression/javascript/schema_ui_files/fts_parser.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/fts_parser.ui.spec.js @@ -13,39 +13,35 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('FTSParserSchema', ()=>{ - let schemaObj = new FTSParserSchema( + const createSchemaObj = () => new FTSParserSchema( { - prsstartList: ()=> [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], - prstokenList: ()=> [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], - prsendList: ()=> [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], - prslextypeList: ()=> [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], - prsheadlineList: ()=> [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], - schemaList: ()=> [], + prsstartList: () => [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], + prstokenList: () => [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], + prsendList: () => [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], + prslextypeList: () => [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], + prsheadlineList: () => [{ label: '', value: ''}, { label: 'lb1', value: 'val1'}], + schemaList: () => [], }, { schema: 123 } ); - let getInitData = ()=>Promise.resolve({}); - - - - + let getInitData = () => Promise.resolve({}); beforeEach(()=>{ genericBeforeEach(); }); - it('create', async ()=>{ - await getCreateView(schemaObj); + it('create', async () => { + await getCreateView(createSchemaObj()); }); - it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + it('edit', async () => { + await getEditView(createSchemaObj(), getInitData); }); - it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + it('properties', async () => { + await getPropertiesView(createSchemaObj(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/functions.ui.spec.js b/web/regression/javascript/schema_ui_files/functions.ui.spec.js index 8cd1da7b603..46a9c0524fe 100644 --- a/web/regression/javascript/schema_ui_files/functions.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/functions.ui.spec.js @@ -21,7 +21,7 @@ class MockSchema extends BaseUISchema { describe('FunctionSchema', ()=>{ //Procedure schema - let procedureSchemaObj = new FunctionSchema( + const procedureSchemaObj = new FunctionSchema( ()=>new MockSchema(), ()=>new MockSchema(), { @@ -61,7 +61,7 @@ describe('FunctionSchema', ()=>{ ); - let schemaObj = new FunctionSchema( + const createSchemaObj = () => new FunctionSchema( () => new MockSchema(), () => new MockSchema(), { @@ -105,7 +105,8 @@ describe('FunctionSchema', ()=>{ funcowner: 'postgres', pronamespace: 'public', } - ); + ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); @@ -117,19 +118,19 @@ describe('FunctionSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); - it('create', async ()=>{ + it('create procedure', async ()=>{ await getCreateView(procedureSchemaObj); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('proiswindow visible', async ()=>{ diff --git a/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js b/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js index d5656fdc4da..cdf48e20697 100644 --- a/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/import_export_servers.ui.spec.js @@ -13,19 +13,20 @@ import {genericBeforeEach, getCreateView} from '../genericFunctions'; describe('ImportExportServers', () => { - let schemaObj = new ImportExportSelectionSchema(); beforeEach(() => { genericBeforeEach(); }); it('import', async () => { + const schemaObj = new ImportExportSelectionSchema(); await getCreateView(schemaObj); }); it('export', async () => { - schemaObj = new ImportExportSelectionSchema( - {imp_exp: 'e', filename: 'test.json'}); + const schemaObj = new ImportExportSelectionSchema({ + imp_exp: 'e', filename: 'test.json' + }); await getCreateView(schemaObj); }); diff --git a/web/regression/javascript/schema_ui_files/index.ui.spec.js b/web/regression/javascript/schema_ui_files/index.ui.spec.js index 27c1ab9bf8e..246dd7c91f4 100644 --- a/web/regression/javascript/schema_ui_files/index.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/index.ui.spec.js @@ -30,23 +30,23 @@ class SchemaInColl extends BaseUISchema { } function getFieldDepChange(schema, id) { - return _.find(schema.fields, (f)=>f.id==id)?.depChange; + return _.find(schema.fields, (f) => f.id==id)?.depChange; } -describe('IndexSchema', ()=>{ +describe('IndexSchema', () => { let indexSchemaObj; - let getInitData = ()=>Promise.resolve({}); + let getInitData = () => Promise.resolve({}); - beforeAll(()=>{ + beforeAll(() => { jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue(Promise.resolve([])); jest.spyOn(nodeAjax, 'getNodeListByName').mockReturnValue(Promise.resolve([])); indexSchemaObj = new IndexSchema( { - tablespaceList: ()=>[], - amnameList : ()=>[{label:'abc', value:'abc'}], - columnList: ()=>[{label:'abc', value:'abc'}], - collationList: ()=>[{label:'abc', value:'abc'}], - opClassList: ()=>[{label:'abc', value:'abc'}] + tablespaceList: () => [], + amnameList : () => [{label:'abc', value:'abc'}], + columnList: () => [{label:'abc', value:'abc'}], + collationList: () => [{label:'abc', value:'abc'}], + opClassList: () => [{label:'abc', value:'abc'}] }, { node_info: {'server': { 'version': 110000} } @@ -57,23 +57,23 @@ describe('IndexSchema', ()=>{ ); }); - beforeEach(()=>{ + beforeEach(() => { genericBeforeEach(); }); - it('create', async ()=>{ + it('create', async () => { await getCreateView(indexSchemaObj); }); - it('edit', async ()=>{ + it('edit', async () => { await getEditView(indexSchemaObj, getInitData); }); - it('properties', async ()=>{ + it('properties', async () => { await getPropertiesView(indexSchemaObj, getInitData); }); - it('create collection', async ()=>{ + it('create collection', async () => { let schemaCollObj = new SchemaInColl(indexSchemaObj); let {ctrl, user} = await getCreateView(schemaCollObj); /* Make sure you hit every corner */ @@ -82,15 +82,15 @@ describe('IndexSchema', ()=>{ await addNewDatagridRow(user, ctrl); }); - it('changeColumnOptions', ()=>{ + it('changeColumnOptions', () => { jest.spyOn(indexSchemaObj.indexHeaderSchema, 'changeColumnOptions'); let columns = [{label: 'label', value: 'value'}]; indexSchemaObj.changeColumnOptions(columns); expect(indexSchemaObj.indexHeaderSchema.changeColumnOptions).toHaveBeenCalledWith(columns); }); - describe('IndexColHeaderSchema', ()=>{ - it('getNewData', ()=>{ + describe('IndexColHeaderSchema', () => { + it('getNewData', () => { indexSchemaObj.indexHeaderSchema.columnOptions = [ {label: 'id', value: 'id'}, {label: 'name', value: 'name'} @@ -118,18 +118,18 @@ describe('IndexSchema', ()=>{ }); }); - describe('IndexColumnSchema', ()=>{ - it('column schema colname editable', ()=>{ - indexSchemaObj.indexColumnSchema._top = { - _sessData: { amname: 'btree' } + describe('IndexColumnSchema', () => { + it('column schema colname editable', () => { + indexSchemaObj.indexColumnSchema.top = { + sessData: { amname: 'btree' } }; - let cell = _.find(indexSchemaObj.indexColumnSchema.fields, (f)=>f.id=='op_class').cell; + let cell = _.find(indexSchemaObj.indexColumnSchema.fields, (f) => f.id=='op_class').cell; cell(); }); - it('column schema sort_order depChange', ()=>{ + it('column schema sort_order depChange', () => { let topState = { amname: 'btree' }; - let depChange = _.find(indexSchemaObj.indexColumnSchema.fields, (f)=>f.id=='sort_order').depChange; + let depChange = _.find(indexSchemaObj.indexColumnSchema.fields, (f) => f.id=='sort_order').depChange; let state = { sort_order: true }; depChange(state, {}, topState, { oldState: { sort_order: false } }); @@ -140,13 +140,13 @@ describe('IndexSchema', ()=>{ expect(state.is_sort_nulls_applicable).toBe(false); }); - it('column schema sort_order editable', ()=>{ - indexSchemaObj.indexColumnSchema._top = { - _sessData: { amname: 'btree' } + it('column schema sort_order editable', () => { + indexSchemaObj.indexColumnSchema.top = { + sessData: { amname: 'btree' } }; let state = {}; jest.spyOn(indexSchemaObj.indexColumnSchema, 'inSchemaWithModelCheck').mockReturnValue(true); - let editable = _.find(indexSchemaObj.indexColumnSchema.fields, (f)=>f.id=='sort_order').editable; + let editable = _.find(indexSchemaObj.indexColumnSchema.fields, (f) => f.id=='sort_order').editable; let status = editable(state); expect(status).toBe(false); @@ -154,18 +154,18 @@ describe('IndexSchema', ()=>{ status = editable(state); expect(status).toBe(true); - indexSchemaObj.indexColumnSchema._top._sessData.amname = 'abc'; + indexSchemaObj.indexColumnSchema.top.sessData.amname = 'abc'; status = editable(state); expect(status).toBe(false); }); - it('column schema nulls editable', ()=>{ - indexSchemaObj.indexColumnSchema._top = { - _sessData: { amname: 'btree' } + it('column schema nulls editable', () => { + indexSchemaObj.indexColumnSchema.top = { + sessData: { amname: 'btree' } }; let state = {}; jest.spyOn(indexSchemaObj.indexColumnSchema, 'inSchemaWithModelCheck').mockReturnValue(true); - let editable = _.find(indexSchemaObj.indexColumnSchema.fields, (f)=>f.id=='nulls').editable; + let editable = _.find(indexSchemaObj.indexColumnSchema.fields, (f) => f.id=='nulls').editable; let status = editable(state); expect(status).toBe(false); @@ -173,14 +173,14 @@ describe('IndexSchema', ()=>{ status = editable(state); expect(status).toBe(true); - indexSchemaObj.indexColumnSchema._top._sessData.amname = 'abc'; + indexSchemaObj.indexColumnSchema.top.sessData.amname = 'abc'; status = editable(state); expect(status).toBe(false); }); - it('column schema setOpClassTypes', ()=>{ - indexSchemaObj.indexColumnSchema._top = { - _sessData: { amname: 'btree' } + it('column schema setOpClassTypes', () => { + indexSchemaObj.indexColumnSchema.top = { + sessData: { amname: 'btree' } }; let options = []; indexSchemaObj.indexColumnSchema.op_class_types = []; @@ -195,15 +195,15 @@ describe('IndexSchema', ()=>{ }); - it('depChange', ()=>{ + it('depChange', () => { let state = {}; expect(getFieldDepChange(indexSchemaObj, 'description')(state)).toEqual({ comment: '', }); }); - it('columns formatter', ()=>{ - let formatter = _.find(indexSchemaObj.fields, (f)=>f.id=='columns').cell().controlProps.formatter; + it('columns formatter', () => { + let formatter = _.find(indexSchemaObj.fields, (f) => f.id=='columns').cell().controlProps.formatter; expect(formatter.fromRaw([{ colname: 'lid', },{ @@ -213,7 +213,7 @@ describe('IndexSchema', ()=>{ expect(formatter.fromRaw([])).toBe(''); }); - it('validate', ()=>{ + it('validate', () => { let state = { columns: [] }; let setError = jest.fn(); diff --git a/web/regression/javascript/schema_ui_files/language.ui.spec.js b/web/regression/javascript/schema_ui_files/language.ui.spec.js index d28aa27113d..7b1957540fb 100644 --- a/web/regression/javascript/schema_ui_files/language.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/language.ui.spec.js @@ -20,7 +20,7 @@ class MockSchema extends BaseUISchema { describe('LanguageSchema', ()=>{ - let schemaObj = new LanguageSchema( + const createSchemaObj = () => new LanguageSchema( ()=>new MockSchema(), { lan_functions: ()=>[], @@ -35,26 +35,23 @@ describe('LanguageSchema', ()=>{ }, }, ); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/membership.ui.spec.js b/web/regression/javascript/schema_ui_files/membership.ui.spec.js index 18c409f008c..79bf42cb890 100644 --- a/web/regression/javascript/schema_ui_files/membership.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/membership.ui.spec.js @@ -30,29 +30,24 @@ class SchemaInColl extends BaseUISchema { } describe('MembershipSchema', ()=>{ - - let schemaObj = new MembershipSchema( - ()=>[]); + const createSchemaObj = () => new MembershipSchema(()=>[]); + let schemaObj = createSchemaObj(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('MembershipMemberSchema', async ()=>{ diff --git a/web/regression/javascript/schema_ui_files/mview.ui.spec.js b/web/regression/javascript/schema_ui_files/mview.ui.spec.js index 0133038ba9b..90cf0d53ee4 100644 --- a/web/regression/javascript/schema_ui_files/mview.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/mview.ui.spec.js @@ -11,6 +11,7 @@ import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import MViewSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; +import { initializeSchemaWithData } from './utils'; class MockSchema extends BaseUISchema { get baseFields() { @@ -20,7 +21,7 @@ class MockSchema extends BaseUISchema { describe('MaterializedViewSchema', ()=>{ - let schemaObj = new MViewSchema( + const createSchemaObject = () => new MViewSchema( ()=>new MockSchema(), ()=>new MockSchema(), { @@ -32,7 +33,8 @@ describe('MaterializedViewSchema', ()=>{ owner: 'postgres', schema: 'public' } - ); + ); + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); beforeEach(()=>{ @@ -40,18 +42,19 @@ describe('MaterializedViewSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('validate', ()=>{ + initializeSchemaWithData(schemaObj, {}); let state = {}; let setError = jest.fn(); diff --git a/web/regression/javascript/schema_ui_files/operator.ui.spec.js b/web/regression/javascript/schema_ui_files/operator.ui.spec.js index 9f88293c63d..acc4d5cb3bd 100644 --- a/web/regression/javascript/schema_ui_files/operator.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/operator.ui.spec.js @@ -13,27 +13,23 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('OperatorSchema', ()=>{ - let schemaObj = new OperatorSchema(); + const createSchemaObject = () => new OperatorSchema(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/packages.ui.spec.js b/web/regression/javascript/schema_ui_files/packages.ui.spec.js index ea3e4487181..659d9ba8d30 100644 --- a/web/regression/javascript/schema_ui_files/packages.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/packages.ui.spec.js @@ -15,33 +15,32 @@ import { initializeSchemaWithData } from './utils'; describe('PackageSchema', ()=>{ - let packageSchemaObj = new PackageSchema( - (privileges)=>getNodePrivilegeRoleSchema({}, {server: {user: {name: 'postgres'}}}, {}, privileges), + const createSchemaObject = () => new PackageSchema( + (privileges) => getNodePrivilegeRoleSchema( + {}, {server: {user: {name: 'postgres'}}}, {}, privileges + ), { schemas:() => [], node_info: {'schema': []} }, ); + let packageSchemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(packageSchemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(packageSchemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(packageSchemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('pkgheadsrc depChange', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/partition.ui.spec.js b/web/regression/javascript/schema_ui_files/partition.ui.spec.js index 7a9988345e9..a6382b89105 100644 --- a/web/regression/javascript/schema_ui_files/partition.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/partition.ui.spec.js @@ -15,47 +15,46 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('PartitionTableSchema', ()=>{ - let schemaObj; + const createSchemaObject = () => getNodePartitionTableSchema({ + server: { + _id: 1, + }, + schema: { + _label: 'public', + } + }, {}, { + Nodes: {table: {}}, + serverInfo: { + 1: { + user: { + name: 'Postgres', + } + } + } + }); + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); beforeAll(()=>{ jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue(Promise.resolve([])); jest.spyOn(nodeAjax, 'getNodeListByName').mockReturnValue(Promise.resolve([])); - schemaObj = getNodePartitionTableSchema({ - server: { - _id: 1, - }, - schema: { - _label: 'public', - } - }, {}, { - Nodes: {table: {}}, - serverInfo: { - 1: { - user: { - name: 'Postgres', - } - } - } - }); }); - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('depChange', ()=>{ @@ -96,4 +95,3 @@ describe('PartitionTableSchema', ()=>{ expect(schemaObj.validate(state, setError)).toBe(false); }); }); - diff --git a/web/regression/javascript/schema_ui_files/partition.utils.ui.spec.js b/web/regression/javascript/schema_ui_files/partition.utils.ui.spec.js index eca02bd6aa2..4f7d1280241 100644 --- a/web/regression/javascript/schema_ui_files/partition.utils.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/partition.utils.ui.spec.js @@ -15,6 +15,7 @@ import { PartitionKeysSchema, PartitionsSchema } from '../../../pgadmin/browser/ import {addNewDatagridRow, genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; import { initializeSchemaWithData } from './utils'; + function getFieldDepChange(schema, id) { return _.find(schema.fields, (f)=>f.id==id)?.depChange; } @@ -41,24 +42,24 @@ class SchemaInColl extends BaseUISchema { describe('PartitionKeysSchema', ()=>{ - let schemaObj; + const createSchemaObject = () => { + let partitionObj = new PartitionKeysSchema(); + return new SchemaInColl(partitionObj); + }; + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); beforeAll(()=>{ jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue(Promise.resolve([])); jest.spyOn(nodeAjax, 'getNodeListByName').mockReturnValue(Promise.resolve([])); - let partitionObj = new PartitionKeysSchema(); - schemaObj = new SchemaInColl(partitionObj); }); - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - const {ctrl, user} = await getCreateView(schemaObj); + const {ctrl, user} = await getCreateView(createSchemaObject()); /* Make sure you hit every corner */ @@ -67,11 +68,11 @@ describe('PartitionKeysSchema', ()=>{ }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('depChange', ()=>{ @@ -104,14 +105,17 @@ describe('PartitionKeysSchema', ()=>{ describe('PartitionsSchema', ()=>{ - let schemaObj; + const createSchemaObject = () => { + let schemaObj = new PartitionsSchema(); + schemaObj.top = schemaObj; + return schemaObj; + }; + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); beforeAll(()=>{ jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue(Promise.resolve([])); jest.spyOn(nodeAjax, 'getNodeListByName').mockReturnValue(Promise.resolve([])); - schemaObj = new PartitionsSchema(); - schemaObj.top = schemaObj; }); @@ -121,15 +125,15 @@ describe('PartitionsSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('create collection', async ()=>{ diff --git a/web/regression/javascript/schema_ui_files/pga_job.ui.spec.js b/web/regression/javascript/schema_ui_files/pga_job.ui.spec.js index 93a58a1e20d..4993339cf33 100644 --- a/web/regression/javascript/schema_ui_files/pga_job.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/pga_job.ui.spec.js @@ -20,32 +20,28 @@ class MockSchema extends BaseUISchema { describe('PgaJobSchema', ()=>{ - let schemaObj = new PgaJobSchema( + const createSchemaObject = () => new PgaJobSchema( { jobjclid:()=>[], }, ()=>new MockSchema(), - ); + ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/pga_jobstep.ui.spec.js b/web/regression/javascript/schema_ui_files/pga_jobstep.ui.spec.js index 124d0f2095a..b8b3af2a18d 100644 --- a/web/regression/javascript/schema_ui_files/pga_jobstep.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/pga_jobstep.ui.spec.js @@ -13,22 +13,13 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('PgaJobStepSchema', ()=>{ - let schemaObj = new PgaJobStepSchema( - { - databases: ()=>[], - }, - [], - { - jstdbname: 'postgres', - } - ); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ + schemaObj = new PgaJobStepSchema( + { databases: ()=>[] }, [], { jstdbname: 'postgres' } + ); genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/pga_schedule.ui.spec.js b/web/regression/javascript/schema_ui_files/pga_schedule.ui.spec.js index 5a3fe336d72..a9c365d4ad9 100644 --- a/web/regression/javascript/schema_ui_files/pga_schedule.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/pga_schedule.ui.spec.js @@ -10,34 +10,38 @@ import PgaJobScheduleSchema, { ExceptionsSchema } from '../../../pgadmin/browser/server_groups/servers/pgagent/schedules/static/js/pga_schedule.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; +import { initializeSchemaWithData } from './utils'; describe('PgaJobScheduleSchema', ()=>{ - let schemaObj = new PgaJobScheduleSchema([], { + const createSchemaObject = () => new PgaJobScheduleSchema([], { jscweekdays:[true,true,true,true,false,false,true], - jscexceptions:[{'jexid':81,'jexdate':'2021-08-05','jextime':'12:55:00'},{'jexid':83,'jexdate':'2021-08-17','jextime':'20:00:00'}], + jscexceptions:[ + {'jexid':81,'jexdate':'2021-08-05','jextime':'12:55:00'}, + {'jexid':83,'jexdate':'2021-08-17','jextime':'20:00:00'} + ], }); - let getInitData = ()=>Promise.resolve({}); - - + let getInitData = () => Promise.resolve({}); beforeEach(()=>{ genericBeforeEach(); }); - it('create', async ()=>{ - await getCreateView(schemaObj); + it('create', async () => { + await getCreateView(createSchemaObject()); }); - it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + it('edit', async () => { + await getEditView(createSchemaObject(), getInitData); }); - it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + it('properties', async () => { + await getPropertiesView(createSchemaObject(), getInitData); }); - it('validate', ()=>{ + it('validate', () => { + let schemaObj = createSchemaObject(); + initializeSchemaWithData(schemaObj, {}); let state = {}; let setError = jest.fn(); @@ -59,28 +63,30 @@ describe('PgaJobScheduleSchema', ()=>{ }); }); -describe('ExceptionsSchema', ()=>{ +describe('ExceptionsSchema', () => { - let schemaObj = new ExceptionsSchema(); + const createSchemaObject = () => new ExceptionsSchema(); let getInitData = ()=>Promise.resolve({}); beforeEach(()=>{ genericBeforeEach(); }); - it('create', async ()=>{ - await getCreateView(schemaObj); + it('create', async () => { + await getCreateView(createSchemaObject()); }); - it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + it('edit', async () => { + await getEditView(createSchemaObject(), getInitData); }); - it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + it('properties', async () => { + await getPropertiesView(createSchemaObject(), getInitData); }); - it('validate', ()=>{ + it('validate', () => { + let schemaObj = createSchemaObject(); + initializeSchemaWithData(schemaObj, {}); let state = {}; let setError = jest.fn(); @@ -95,4 +101,3 @@ describe('ExceptionsSchema', ()=>{ expect(setError).toHaveBeenCalledWith('jscdate', null); }); }); - diff --git a/web/regression/javascript/schema_ui_files/primary_key.ui.spec.js b/web/regression/javascript/schema_ui_files/primary_key.ui.spec.js index 35294fe5dc5..ffdc0146d6f 100644 --- a/web/regression/javascript/schema_ui_files/primary_key.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/primary_key.ui.spec.js @@ -47,8 +47,6 @@ describe('PrimaryKeySchema', ()=>{ }, {}); }); - - beforeEach(()=>{ genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/privilege.ui.spec.js b/web/regression/javascript/schema_ui_files/privilege.ui.spec.js index 37517aff9ae..903399a40be 100644 --- a/web/regression/javascript/schema_ui_files/privilege.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/privilege.ui.spec.js @@ -15,20 +15,17 @@ import {addNewDatagridRow, genericBeforeEach, getCreateView, getEditView, getPro describe('PrivilegeSchema', ()=>{ - let schemaObj = new PrivilegeRoleSchema( - ()=>[], - ()=>[], - null, - {server: {user: {name: 'postgres'}}}, - ['X'] - ); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ + schemaObj = new PrivilegeRoleSchema( + ()=>[], + ()=>[], + null, + {server: {user: {name: 'postgres'}}}, + ['X'] + ); genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/publication.ui.spec.js b/web/regression/javascript/schema_ui_files/publication.ui.spec.js index d989c6ccb4f..620ccee2e61 100644 --- a/web/regression/javascript/schema_ui_files/publication.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/publication.ui.spec.js @@ -13,30 +13,28 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('PublicationSchema', ()=>{ - let schemaObj = new PublicationSchema( - { - allTables: ()=>[], - allSchemas:()=>[], - getColumns: ()=>[], - role: ()=>[], - }, - { - node_info: { - connected: true, - user: {id: 10, name: 'postgres', is_superuser: true, can_create_role: true, can_create_db: true}, - user_id: 1, - username: 'postgres', - version: 130005, - }, - }, - ); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ + schemaObj = new PublicationSchema( + { + allTables: ()=>[], + allSchemas:()=>[], + getColumns: ()=>[], + role: ()=>[], + }, + { + node_info: { + connected: true, + user: {id: 10, name: 'postgres', is_superuser: true, can_create_role: true, can_create_db: true}, + user_id: 1, + username: 'postgres', + version: 130005, + }, + }, + ); + genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/resource_group.ui.spec.js b/web/regression/javascript/schema_ui_files/resource_group.ui.spec.js index 560e4ae54c2..929b1e37602 100644 --- a/web/regression/javascript/schema_ui_files/resource_group.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/resource_group.ui.spec.js @@ -13,14 +13,12 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('ResourceGroupSchema', ()=>{ - let schemaObj = new ResourceGroupSchema(); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ + beforeEach(() => { + schemaObj = new ResourceGroupSchema(); genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/restore.ui.spec.js b/web/regression/javascript/schema_ui_files/restore.ui.spec.js index 51a1a57846a..96065ab814a 100644 --- a/web/regression/javascript/schema_ui_files/restore.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/restore.ui.spec.js @@ -14,10 +14,7 @@ import {getCreateView} from '../genericFunctions'; describe('RestoreSchema', ()=>{ - - - - let restoreSchemaObj = new RestoreSchema( + const createSchemaObj = () => new RestoreSchema( ()=>getRestoreSectionSchema({selectedNodeType: 'table'}), ()=>getRestoreTypeObjSchema({selectedNodeType: 'table'}), ()=>getRestoreSaveOptSchema({nodeInfo: {server: {version: 11000}}}), @@ -32,10 +29,11 @@ describe('RestoreSchema', ()=>{ ); it('restore dialog', async ()=>{ - await getCreateView(restoreSchemaObj); + await getCreateView(createSchemaObj()); }); it('restore validate', () => { + let restoreSchemaObj = createSchemaObj(); let state = { file: undefined }; //validating for empty file let setError = jest.fn(); diff --git a/web/regression/javascript/schema_ui_files/role.ui.spec.js b/web/regression/javascript/schema_ui_files/role.ui.spec.js index 9bd13d7a253..7bd8747cc41 100644 --- a/web/regression/javascript/schema_ui_files/role.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/role.ui.spec.js @@ -20,7 +20,7 @@ class MockSchema extends BaseUISchema { describe('RoleSchema', ()=>{ - let schemaObj = new RoleSchema( + const createSchemaObject = () => new RoleSchema( ()=>new MockSchema(), ()=>new MockSchema(), { @@ -30,24 +30,20 @@ describe('RoleSchema', ()=>{ ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/row_security_policy.ui.spec.js b/web/regression/javascript/schema_ui_files/row_security_policy.ui.spec.js index 0c32e70bcab..79dee87c730 100644 --- a/web/regression/javascript/schema_ui_files/row_security_policy.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/row_security_policy.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('RowSecurityPolicySchema', ()=>{ - let schemaObj = new RowSecurityPolicySchema( + const createSchemaObject = () => new RowSecurityPolicySchema( { role: ()=>[], nodeInfo: {server: {version: 90400}}, @@ -21,24 +21,20 @@ describe('RowSecurityPolicySchema', ()=>{ ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/rule.ui.spec.js b/web/regression/javascript/schema_ui_files/rule.ui.spec.js index d96fbea2e1c..59ede27060e 100644 --- a/web/regression/javascript/schema_ui_files/rule.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/rule.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('RuleSchema', ()=>{ - let schemaObj = new RuleSchema( + const createSchemaObject = () => new RuleSchema( { nodeInfo: {schema: {label: 'public'}, server: {version: 90400}}, nodeData: {label: 'Test'} @@ -21,24 +21,20 @@ describe('RuleSchema', ()=>{ ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/schema.ui.spec.js b/web/regression/javascript/schema_ui_files/schema.ui.spec.js index 812db9d945b..0b18eb5fd16 100644 --- a/web/regression/javascript/schema_ui_files/schema.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/schema.ui.spec.js @@ -14,25 +14,23 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('PGSchema', ()=>{ - let schemaObj = new PGSchema( + const createSchemaObject = () => new PGSchema( ()=>getNodePrivilegeRoleSchema({}, {server: {user: {name: 'postgres'}}}, {}), { roles:() => [], namespaceowner: '', } - ); + ); + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('schema validate', () => { @@ -48,11 +46,11 @@ describe('PGSchema', ()=>{ }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/sequence.ui.spec.js b/web/regression/javascript/schema_ui_files/sequence.ui.spec.js index a3b0eb67103..1d3364520a4 100644 --- a/web/regression/javascript/schema_ui_files/sequence.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/sequence.ui.spec.js @@ -20,7 +20,7 @@ class MockSchema extends BaseUISchema { describe('SequenceSchema', ()=>{ - let schemaObj = new SequenceSchema( + const createSchemaObject = () => new SequenceSchema( ()=>new MockSchema(), { role: ()=>[], @@ -30,26 +30,23 @@ describe('SequenceSchema', ()=>{ schema: 'public', } ); - let getInitData = ()=>Promise.resolve({}); - - - - + let schemaObj = createSchemaObject(); + let getInitData = () => Promise.resolve({}); beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/server.ui.spec.js b/web/regression/javascript/schema_ui_files/server.ui.spec.js index 47a5d1727b1..e9ae2451d69 100644 --- a/web/regression/javascript/schema_ui_files/server.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/server.ui.spec.js @@ -14,11 +14,12 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('ServerSchema', ()=>{ - let schemaObj = new ServerSchema([{ + const createSchemaObject = () => new ServerSchema([{ label: 'Servers', value: 1, }], 0, { user_id: 'jasmine', }); + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); beforeEach(()=>{ @@ -27,15 +28,15 @@ describe('ServerSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('validate', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/server_group.ui.spec.js b/web/regression/javascript/schema_ui_files/server_group.ui.spec.js index 17357e29509..3dbf59b94ff 100644 --- a/web/regression/javascript/schema_ui_files/server_group.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/server_group.ui.spec.js @@ -13,26 +13,22 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('ServerGroupSchema', ()=>{ - let schemaObj = new ServerGroupSchema(); + const createSchemaObject = () => new ServerGroupSchema(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/subscription.ui.spec.js b/web/regression/javascript/schema_ui_files/subscription.ui.spec.js index 9de21e98f5b..51f51cb3f61 100644 --- a/web/regression/javascript/schema_ui_files/subscription.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/subscription.ui.spec.js @@ -11,63 +11,60 @@ import SubscriptionSchema from '../../../pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; -describe('SubscriptionSchema', ()=>{ - - let schemaObj = new SubscriptionSchema( - { - getPublication: ()=>[], - role: ()=>[], - }, - { - node_info: { - connected: true, - user: {id: 10, name: 'postgres', is_superuser: true, can_create_role: true, can_create_db: true}, - user_id: 1, - username: 'postgres', - version: 130005, - server: {host: '127.0.0.1', port: 5432}, - }, - }, - { - subowner : 'postgres' - } - ); - let getInitData = ()=>Promise.resolve({}); - - - +describe('SubscriptionSchema', () => { + let schemaObj; + let getInitData = ()=>Promise.resolve({}); - beforeEach(()=>{ + beforeEach(() => { + schemaObj = new SubscriptionSchema( + { + getPublication: ()=>[], + role: ()=>[], + }, + { + node_info: { + connected: true, + user: {id: 10, name: 'postgres', is_superuser: true, can_create_role: true, can_create_db: true}, + user_id: 1, + username: 'postgres', + version: 130005, + server: {host: '127.0.0.1', port: 5432}, + }, + }, + { + subowner : 'postgres' + } + ); genericBeforeEach(); }); - it('create', async ()=>{ + it('create', async () => { await getCreateView(schemaObj); }); - it('edit', async ()=>{ + it('edit', async () => { await getEditView(schemaObj, getInitData); }); - it('properties', async ()=>{ + it('properties', async () => { await getPropertiesView(schemaObj, getInitData); }); - it('copy_data_after_refresh readonly', ()=>{ + it('copy_data_after_refresh readonly', () => { let isReadonly = _.find(schemaObj.fields, (f)=>f.id=='copy_data_after_refresh').readonly; let status = isReadonly({host: '127.0.0.1', port : 5432}); expect(status).toBe(true); }); - it('copy_data_after_refresh readonly', ()=>{ + it('copy_data_after_refresh readonly', () => { let isReadonly = _.find(schemaObj.fields, (f)=>f.id=='copy_data_after_refresh').readonly; let status = isReadonly({refresh_pub : true}); expect(status).toBe(false); }); - it('validate', ()=>{ + it('validate', () => { let state = {}; let setError = jest.fn(); @@ -88,4 +85,3 @@ describe('SubscriptionSchema', ()=>{ expect(setError).toHaveBeenCalledWith('pub', 'Publication must be specified.'); }); }); - diff --git a/web/regression/javascript/schema_ui_files/synonym.ui.spec.js b/web/regression/javascript/schema_ui_files/synonym.ui.spec.js index 8d63a1317f2..d188e620fc6 100644 --- a/web/regression/javascript/schema_ui_files/synonym.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/synonym.ui.spec.js @@ -11,45 +11,42 @@ import SynonymSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/synonyms/static/js/synonym.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; -describe('SynonymSchema', ()=>{ +describe('SynonymSchema', () => { - let schemaObj = new SynonymSchema( - { - role: ()=>[], - schema: ()=>[], - synobjschema: ()=>[], - getTargetObjectOptions: ()=>[], - }, - [], - { - owner: 'postgres', - schema: 'public', - synobjschema: 'public', - } - ); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - - beforeEach(()=>{ + beforeEach(() => { + schemaObj = new SynonymSchema( + { + role: ()=>[], + schema: ()=>[], + synobjschema: ()=>[], + getTargetObjectOptions: ()=>[], + }, + [], + { + owner: 'postgres', + schema: 'public', + synobjschema: 'public', + } + ); genericBeforeEach(); }); - it('create', async ()=>{ + it('create', async () => { await getCreateView(schemaObj); }); - it('edit', async ()=>{ + it('edit', async () => { await getEditView(schemaObj, getInitData); }); - it('properties', async ()=>{ + it('properties', async () => { await getPropertiesView(schemaObj, getInitData); }); - it('validate', ()=>{ + it('validate', () => { let state = {}; let setError = jest.fn(); diff --git a/web/regression/javascript/schema_ui_files/table.ui.spec.js b/web/regression/javascript/schema_ui_files/table.ui.spec.js index 60b28a180ac..7a755c7b541 100644 --- a/web/regression/javascript/schema_ui_files/table.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/table.ui.spec.js @@ -19,52 +19,50 @@ function getFieldDepChange(schema, id) { return _.find(schema.fields, (f)=>f.id==id)?.depChange; } -describe('TableSchema', ()=>{ - - let schemaObj; +describe('TableSchema', () => { + + const createTableSchemaObject = () => getNodeTableSchema({ + server: { + _id: 1, + }, + schema: { + _label: 'public', + } + }, {}, { + Nodes: {table: {}}, + serverInfo: { + 1: { + user: { + name: 'Postgres', + } + } + } + }); + let schemaObj = createTableSchemaObject(); let getInitData = ()=>Promise.resolve({}); - beforeAll(()=>{ + beforeAll(() => { jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue(Promise.resolve([])); jest.spyOn(nodeAjax, 'getNodeListByName').mockReturnValue(Promise.resolve([])); - schemaObj = getNodeTableSchema({ - server: { - _id: 1, - }, - schema: { - _label: 'public', - } - }, {}, { - Nodes: {table: {}}, - serverInfo: { - 1: { - user: { - name: 'Postgres', - } - } - } - }); }); - - - beforeEach(()=>{ + beforeEach(() => { genericBeforeEach(); }); - it('create', async ()=>{ - await getCreateView(schemaObj); + it('create', async () => { + await getCreateView(createTableSchemaObject()); }); - it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + it('edit', async () => { + await getEditView(createTableSchemaObject(), getInitData); }); - it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + it('properties', async () => { + await getPropertiesView(createTableSchemaObject(), getInitData); }); - it('getTableOid', ()=>{ + it('getTableOid', () => { schemaObj.inheritedTableList = [ {label: 'tab1', tid: 140391}, {label: 'tab2', tid: 180191} @@ -72,12 +70,12 @@ describe('TableSchema', ()=>{ expect(schemaObj.getTableOid('tab2')).toBe(180191); }); - it('canEditDeleteRowColumns', ()=>{ + it('canEditDeleteRowColumns', () => { expect(schemaObj.canEditDeleteRowColumns({inheritedfrom: 1234})).toBe(false); expect(schemaObj.canEditDeleteRowColumns({inheritedfrom: null})).toBe(true); }); - it('LikeSchema typname change', ()=>{ + it('LikeSchema typname change', () => { let likeSchemaObj = new LikeSchema([]); /* Dummy */ likeSchemaObj.top = new LikeSchema([]); @@ -96,13 +94,13 @@ describe('TableSchema', ()=>{ }); }); - describe('typname change', ()=>{ + describe('typname change', () => { let confirmSpy; let deferredDepChange; let oftypeColumns = [ {name: 'id'} ]; - beforeEach(()=>{ + beforeEach(() => { jest.spyOn(schemaObj, 'changeColumnOptions'); jest.spyOn(schemaObj, 'getTableOid').mockReturnValue(140391); confirmSpy = jest.spyOn(pgAdmin.Browser.notifier, 'confirm'); @@ -184,7 +182,7 @@ describe('TableSchema', ()=>{ }); }); - describe('coll_inherits change', ()=>{ + describe('coll_inherits change', () => { let deferredDepChange; let inheritCol = {name: 'id'}; let onRemoveAction = (depChange, state, done)=> { @@ -197,7 +195,7 @@ describe('TableSchema', ()=>{ done(); }; - beforeEach(()=>{ + beforeEach(() => { jest.spyOn(schemaObj, 'changeColumnOptions'); jest.spyOn(schemaObj, 'getTableOid').mockReturnValue(140391); jest.spyOn(schemaObj, 'getColumns').mockReturnValue(Promise.resolve([inheritCol])); @@ -271,7 +269,7 @@ describe('TableSchema', ()=>{ }); }); - it('depChange', ()=>{ + it('depChange', () => { jest.spyOn(schemaObj, 'getTableOid').mockReturnValue(140391); let state = {}; @@ -300,7 +298,7 @@ describe('TableSchema', ()=>{ }); }); - it('validate', ()=>{ + it('validate', () => { let state = {is_partitioned: true}; let setError = jest.fn(); diff --git a/web/regression/javascript/schema_ui_files/tablespace.ui.spec.js b/web/regression/javascript/schema_ui_files/tablespace.ui.spec.js index bb3c46a047a..d486854c28d 100644 --- a/web/regression/javascript/schema_ui_files/tablespace.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/tablespace.ui.spec.js @@ -20,23 +20,20 @@ class MockSchema extends BaseUISchema { describe('TablespaceSchema', ()=>{ - let schemaObj = new TablespaceSchema( - ()=>new MockSchema(), - ()=>new MockSchema(), - { - role: ()=>[], - }, - { - spcuser: 'postgres' - } - ); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ + schemaObj = new TablespaceSchema( + ()=>new MockSchema(), + ()=>new MockSchema(), + { + role: ()=>[], + }, + { + spcuser: 'postgres' + } + ); genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/trigger.ui.spec.js b/web/regression/javascript/schema_ui_files/trigger.ui.spec.js index a792b187513..a38cccbaa86 100644 --- a/web/regression/javascript/schema_ui_files/trigger.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/trigger.ui.spec.js @@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from ' describe('TriggerSchema', ()=>{ - let schemaObj = new TriggerSchema( + let createSchemaObj = () => new TriggerSchema( { triggerFunction: [], columns: [], @@ -26,27 +26,24 @@ describe('TriggerSchema', ()=>{ ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObj()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObj(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObj(), getInitData); }); it('validate', ()=>{ + const schemaObj = createSchemaObj(); let state = {}; let setError = jest.fn(); @@ -148,7 +145,7 @@ describe('TriggerSchema', ()=>{ describe('TriggerEventsSchema', ()=>{ - let schemaObj = new EventSchema( + let createEventSchemaObj = () => new EventSchema( { nodeInfo: { server: {user: {name:'postgres', id:0}, server_type: 'pg', version: 90400}, @@ -167,18 +164,19 @@ describe('TriggerEventsSchema', ()=>{ }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createEventSchemaObj()); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createEventSchemaObj(), getInitData); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createEventSchemaObj(), getInitData); }); it('validate', ()=>{ + const schemaObj = createEventSchemaObj(); let state = {}; let setError = jest.fn(); diff --git a/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js b/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js index 3aa353b8a9e..9fc4a24aa5b 100644 --- a/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/trigger_function.ui.spec.js @@ -20,7 +20,7 @@ class MockSchema extends BaseUISchema { describe('TriggerFunctionSchema', ()=>{ - let schemaObj = new TriggerFunctionSchema( + const createSchemaObject = () => new TriggerFunctionSchema( ()=>new MockSchema(), ()=>new MockSchema(), { @@ -33,32 +33,29 @@ describe('TriggerFunctionSchema', ()=>{ funcowner: 'postgres', pronamespace: 'public' } - ); + ); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('validate', ()=>{ let state = {}; let setError = jest.fn(); + let schemaObj = createSchemaObject(); state.prosrc = null; schemaObj.validate(state, setError); diff --git a/web/regression/javascript/schema_ui_files/type.ui.spec.js b/web/regression/javascript/schema_ui_files/type.ui.spec.js index bcbd6d0f792..c9b0dd37576 100644 --- a/web/regression/javascript/schema_ui_files/type.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/type.ui.spec.js @@ -13,6 +13,68 @@ import { getNodePrivilegeRoleSchema } from '../../../pgadmin/browser/server_grou import TypeSchema, { EnumerationSchema, getCompositeSchema, getExternalSchema, getRangeSchema, getDataTypeSchema } from '../../../pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; +const types = [{ + label: '', value: '' +}, { + label: 'lb1', value: 'numeric[]', length: true, + min_val: 10, max_val: 100, precision: true, is_collatable: true, +}]; + +const createCompositeSchemaObject = () => { + let compositeCollObj = getCompositeSchema( + {}, {server: {user: {name: 'postgres'}}}, {} + ); + let collations = [ + { label: '', value: ''}, { label: 'lb1', value: 'numeric[]'} + ]; + + jest.spyOn(compositeCollObj.fieldOptions, 'types').mockReturnValue(types); + jest.spyOn(compositeCollObj.fieldOptions, 'collations') + .mockReturnValue(collations); + + return compositeCollObj; +}; + +const createExternalSchemaObject = () => { + + let externalCollObj = getExternalSchema({}, {server: {user: {name: 'postgres'}}}, {}); + + jest.spyOn(externalCollObj.fieldOptions, 'externalFunctionsList') + .mockReturnValue([ + { label: '', value: ''}, + { label: 'lb1', cbtype: 'typmodin', value: 'val1'}, + { label: 'lb2', cbtype: 'all', value: 'val2'} + ]); + jest.spyOn(externalCollObj.fieldOptions, 'types') + .mockReturnValue([{ label: '', value: ''}]); + + return externalCollObj; +}; + +const createRangeSchemaObject = () => { + let rangeCollObj = getRangeSchema({}, {server: {user: {name: 'postgres'}}}, {}); + + jest.spyOn(rangeCollObj.fieldOptions, 'getSubOpClass').mockReturnValue([ + { label: '', value: ''}, { label: 'lb1', value: 'val1'} + ]); + jest.spyOn(rangeCollObj.fieldOptions, 'getCanonicalFunctions') + .mockReturnValue([ + { label: '', value: ''}, { label: 'lb1', value: 'val1'} + ]); + jest.spyOn(rangeCollObj.fieldOptions, 'getSubDiffFunctions') + .mockReturnValue([ + { label: '', value: ''}, { label: 'lb1', value: 'val1'} + ]); + jest.spyOn(rangeCollObj.fieldOptions, 'typnameList').mockReturnValue([ + { label: '', value: ''}, { label: 'lb1', value: 'val1'} + ]); + jest.spyOn(rangeCollObj.fieldOptions, 'collationsList').mockReturnValue([ + { label: '', value: ''}, { label: 'lb1', value: 'val1'} + ]); + + return rangeCollObj; +}; + describe('TypeSchema', ()=>{ let getInitData = ()=>Promise.resolve({}); @@ -21,15 +83,10 @@ describe('TypeSchema', ()=>{ }); describe('composite schema describe', () => { + let compositeCollObj = createCompositeSchemaObject(); - let compositeCollObj = getCompositeSchema({}, {server: {user: {name: 'postgres'}}}, {}); - let types = [{ label: '', value: ''}, { label: 'lb1', value: 'numeric[]', length: true, min_val: 10, max_val: 100, precision: true, is_collatable: true}]; - let collations = [{ label: '', value: ''}, { label: 'lb1', value: 'numeric[]'}]; - - it('composite collection', async ()=>{ + it('composite collection', async () => { jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue([]); - jest.spyOn(compositeCollObj.fieldOptions, 'types').mockReturnValue(types); - jest.spyOn(compositeCollObj.fieldOptions, 'collations').mockReturnValue(collations); await getCreateView(compositeCollObj); await getEditView(compositeCollObj, getInitData); }); @@ -91,13 +148,10 @@ describe('TypeSchema', ()=>{ describe('external schema describe', () => { - let externalCollObj = getExternalSchema({}, {server: {user: {name: 'postgres'}}}, {}); + let externalCollObj = createExternalSchemaObject(); it('external collection', async ()=>{ - jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue([]); - jest.spyOn(externalCollObj.fieldOptions, 'externalFunctionsList').mockReturnValue([{ label: '', value: ''}, { label: 'lb1', cbtype: 'typmodin', value: 'val1'}, { label: 'lb2', cbtype: 'all', value: 'val2'}]); - jest.spyOn(externalCollObj.fieldOptions, 'types').mockReturnValue([{ label: '', value: ''}]); await getCreateView(externalCollObj); await getEditView(externalCollObj, getInitData); @@ -118,16 +172,11 @@ describe('TypeSchema', ()=>{ describe('range schema describe', () => { - let rangeCollObj = getRangeSchema({}, {server: {user: {name: 'postgres'}}}, {}); + let rangeCollObj = createRangeSchemaObject(); - it('range collection', async ()=>{ + it('range collection', async () => { jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue([]); - jest.spyOn(rangeCollObj.fieldOptions, 'getSubOpClass').mockReturnValue([{ label: '', value: ''}, { label: 'lb1', value: 'val1'}]); - jest.spyOn(rangeCollObj.fieldOptions, 'getCanonicalFunctions').mockReturnValue([{ label: '', value: ''}, { label: 'lb1', value: 'val1'}]); - jest.spyOn(rangeCollObj.fieldOptions, 'getSubDiffFunctions').mockReturnValue([{ label: '', value: ''}, { label: 'lb1', value: 'val1'}]); - jest.spyOn(rangeCollObj.fieldOptions, 'typnameList').mockReturnValue([{ label: '', value: ''}, { label: 'lb1', value: 'val1'}]); - jest.spyOn(rangeCollObj.fieldOptions, 'collationsList').mockReturnValue([{ label: '', value: ''}, { label: 'lb1', value: 'val1'}]); await getCreateView(rangeCollObj); await getEditView(rangeCollObj, getInitData); @@ -145,7 +194,13 @@ describe('TypeSchema', ()=>{ describe('data type schema describe', () => { let dataTypeObj = getDataTypeSchema({}, {server: {user: {name: 'postgres'}}}, {}); - let types = [{ label: '', value: ''}, { label: 'lb1', value: 'numeric', length: true, min_val: 10, max_val: 100, precision: true}]; + const types = [ + { label: '', value: ''}, + { + label: 'lb1', value: 'numeric', length: true, + min_val: 10, max_val: 100, precision: true, + } + ]; it('data type collection', async ()=>{ @@ -183,34 +238,40 @@ describe('TypeSchema', ()=>{ }); }); - let typeSchemaObj = new TypeSchema( - (privileges)=>getNodePrivilegeRoleSchema({}, {server: {user: {name: 'postgres'}}}, {}, privileges), - ()=>getCompositeSchema({}, {server: {user: {name: 'postgres'}}}, {}), - ()=>getRangeSchema({}, {server: {user: {name: 'postgres'}}}, {}), - ()=>getExternalSchema({}, {server: {user: {name: 'postgres'}}}, {}), - ()=>getDataTypeSchema({}, {server: {user: {name: 'postgres'}}}, {}), - { - roles: ()=>[], - schemas: ()=>[{ label: 'pg_demo', value: 'pg_demo'}], - server_info: [], - node_info: {'schema': []} - }, - { - typowner: 'postgres', - schema: 'public', - typtype: 'c' - } - ); + const createTypeSchemaObject = () => { + jest.spyOn(nodeAjax, 'getNodeAjaxOptions').mockReturnValue([]); + + return new TypeSchema( + (privileges)=>getNodePrivilegeRoleSchema( + {}, {server: {user: {name: 'postgres'}}}, {}, privileges + ), + ()=>createCompositeSchemaObject(), + ()=>createRangeSchemaObject(), + ()=>createExternalSchemaObject(), + ()=>getDataTypeSchema({}, {server: {user: {name: 'postgres'}}}, {}), + { + roles: ()=>[], + schemas: ()=>[{ label: 'pg_demo', value: 'pg_demo'}], + server_info: [], + node_info: {'schema': []} + }, + { + typowner: 'postgres', + schema: 'public', + typtype: 'c' + } + ); + }; it('create', async ()=>{ - await getCreateView(typeSchemaObj); + await getCreateView(createTypeSchemaObject()); }); it('edit', async ()=>{ - await getEditView(typeSchemaObj, getInitData); + await getEditView(createTypeSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(typeSchemaObj, getInitData); + await getPropertiesView(createTypeSchemaObject(), getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/unique_constraint.ui.spec.js b/web/regression/javascript/schema_ui_files/unique_constraint.ui.spec.js index 6658ae67a0a..9b85d240448 100644 --- a/web/regression/javascript/schema_ui_files/unique_constraint.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/unique_constraint.ui.spec.js @@ -47,8 +47,6 @@ describe('UniqueConstraintSchema', ()=>{ }, {}); }); - - beforeEach(()=>{ genericBeforeEach(); }); diff --git a/web/regression/javascript/schema_ui_files/user_mapping.ui.spec.js b/web/regression/javascript/schema_ui_files/user_mapping.ui.spec.js index aaeba2db22e..515d4a56337 100644 --- a/web/regression/javascript/schema_ui_files/user_mapping.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/user_mapping.ui.spec.js @@ -18,36 +18,33 @@ class MockSchema extends BaseUISchema { } } -describe('UserMappingSchema', ()=>{ - - let schemaObj = new UserMappingSchema( - ()=>new MockSchema(), - { - role: ()=>[], - }, - { - name: 'postgres' - } - ); - let getInitData = ()=>Promise.resolve({}); - - - +describe('UserMappingSchema', () => { + let schemaObj; + let getInitData = ()=>Promise.resolve({}); - beforeEach(()=>{ + beforeEach(() => { + schemaObj = new UserMappingSchema( + ()=>new MockSchema(), + { + role: ()=>[], + }, + { + name: 'postgres' + } + ); genericBeforeEach(); }); - it('create', async ()=>{ + it('create', async () => { await getCreateView(schemaObj); }); - it('edit', async ()=>{ + it('edit', async () => { await getEditView(schemaObj, getInitData); }); - it('properties', async ()=>{ + it('properties', async () => { await getPropertiesView(schemaObj, getInitData); }); }); diff --git a/web/regression/javascript/schema_ui_files/utils.js b/web/regression/javascript/schema_ui_files/utils.js index 7099f1b6a48..034cb6e9218 100644 --- a/web/regression/javascript/schema_ui_files/utils.js +++ b/web/regression/javascript/schema_ui_files/utils.js @@ -7,7 +7,9 @@ // ////////////////////////////////////////////////////////////// -import { SchemaState } from '../../../pgadmin/static/js/SchemaView/useSchemaState'; +import { + SchemaState, +} from '../../../pgadmin/static/js/SchemaView/SchemaState'; export function initializeSchemaWithData(schema, data) { const state = schema.state = new SchemaState( diff --git a/web/regression/javascript/schema_ui_files/variable.ui.spec.js b/web/regression/javascript/schema_ui_files/variable.ui.spec.js index f50f636fbdd..bf347885df4 100644 --- a/web/regression/javascript/schema_ui_files/variable.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/variable.ui.spec.js @@ -34,32 +34,29 @@ class MockSchema extends BaseUISchema { describe('VariableSchema', ()=>{ - let schemaObj = new VariableSchema( + const createSchemaObject = () => new VariableSchema( ()=>[], ()=>[], ()=>[], null ); + let schemaObj = createSchemaObject(); let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ genericBeforeEach(); }); it('create', async ()=>{ - await getCreateView(schemaObj); + await getCreateView(createSchemaObject()); }); it('edit', async ()=>{ - await getEditView(schemaObj, getInitData); + await getEditView(createSchemaObject(), getInitData); }); it('properties', async ()=>{ - await getPropertiesView(schemaObj, getInitData); + await getPropertiesView(createSchemaObject(), getInitData); }); it('getValueFieldProps', ()=>{ diff --git a/web/regression/javascript/schema_ui_files/view.ui.spec.js b/web/regression/javascript/schema_ui_files/view.ui.spec.js index ef2aef34602..063e259892d 100644 --- a/web/regression/javascript/schema_ui_files/view.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/view.ui.spec.js @@ -11,6 +11,7 @@ import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import ViewSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui'; import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions'; +import { initializeSchemaWithData } from './utils'; class MockSchema extends BaseUISchema { get baseFields() { @@ -20,25 +21,22 @@ class MockSchema extends BaseUISchema { describe('ViewSchema', ()=>{ - let schemaObj = new ViewSchema( - ()=>new MockSchema(), - {server: {server_type: 'pg'}}, - { - role: ()=>[], - schema: ()=>[], - }, - { - owner: 'postgres', - schema: 'public' - } - ); + let schemaObj; let getInitData = ()=>Promise.resolve({}); - - - - beforeEach(()=>{ + schemaObj = new ViewSchema( + ()=>new MockSchema(), + {server: {server_type: 'pg'}}, + { + role: ()=>[], + schema: ()=>[], + }, + { + owner: 'postgres', + schema: 'public' + } + ); genericBeforeEach(); }); @@ -57,6 +55,7 @@ describe('ViewSchema', ()=>{ it('validate', ()=>{ let state = {}; let setError = jest.fn(); + initializeSchemaWithData(schemaObj, {}); state.definition = null; schemaObj.validate(state, setError); @@ -77,4 +76,3 @@ describe('ViewSchema', ()=>{ }); }); - diff --git a/web/yarn.lock b/web/yarn.lock index b9cb73d8bb7..4c2ca11d54e 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -88,15 +88,15 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.4, @babel/generator@npm:^7.7.2": - version: 7.25.5 - resolution: "@babel/generator@npm:7.25.5" +"@babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.6, @babel/generator@npm:^7.7.2": + version: 7.25.6 + resolution: "@babel/generator@npm:7.25.6" dependencies: - "@babel/types": ^7.25.4 + "@babel/types": ^7.25.6 "@jridgewell/gen-mapping": ^0.3.5 "@jridgewell/trace-mapping": ^0.3.25 jsesc: ^2.5.1 - checksum: d7713f02536a8144eca810e9b13ae854b05fec462348eaf52e7b50df2c0a312bc43bfff0e8e10d6dd982e8986d61175ac8e67d7358a8b4dad9db4d6733bf0c9c + checksum: b55975cd664f5602304d868bb34f4ee3bed6f5c7ce8132cd92ff27a46a53a119def28a182d91992e86f75db904f63094a81247703c4dc96e4db0c03fd04bcd68 languageName: node linkType: hard @@ -306,12 +306,12 @@ __metadata: linkType: hard "@babel/helpers@npm:^7.25.0": - version: 7.25.0 - resolution: "@babel/helpers@npm:7.25.0" + version: 7.25.6 + resolution: "@babel/helpers@npm:7.25.6" dependencies: "@babel/template": ^7.25.0 - "@babel/types": ^7.25.0 - checksum: 739e3704ff41a30f5eaac469b553f4d3ab02be6ced083f5925851532dfbd9efc5c347728e77b754ed0b262a4e5e384e60932a62c192d338db7e4b7f3adf9f4a7 + "@babel/types": ^7.25.6 + checksum: 5a548999db82049a5f7ac6de57576b4ed0d386ce07d058151698836ed411eae6230db12535487caeebb68a2ffc964491e8aead62364a5132ab0ae20e8b68e19f languageName: node linkType: hard @@ -327,14 +327,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.4": - version: 7.25.4 - resolution: "@babel/parser@npm:7.25.4" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.6": + version: 7.25.6 + resolution: "@babel/parser@npm:7.25.6" dependencies: - "@babel/types": ^7.25.4 + "@babel/types": ^7.25.6 bin: parser: ./bin/babel-parser.js - checksum: fe4f083d4ad34f019dd7fad672cd007003004fb0a3df9b7315a5da9a5e8e56c1fed95acab6862e7d76cfccb2e8e364bcc307e9117718e6bb6dfb2e87ad065abf + checksum: 85b237ded09ee43cc984493c35f3b1ff8a83e8dbbb8026b8132e692db6567acc5a1659ec928e4baa25499ddd840d7dae9dee3062be7108fe23ec5f94a8066b1e languageName: node linkType: hard @@ -500,24 +500,24 @@ __metadata: linkType: hard "@babel/plugin-syntax-import-assertions@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.24.7" + version: 7.25.6 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.25.6" dependencies: - "@babel/helper-plugin-utils": ^7.24.7 + "@babel/helper-plugin-utils": ^7.24.8 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: c4d67be4eb1d4637e361477dbe01f5b392b037d17c1f861cfa0faa120030e137aab90a9237931b8040fd31d1e5d159e11866fa1165f78beef7a3be876a391a17 + checksum: b3b251ace9f184c2d6369cde686ff01581050cb0796f2ff00ff4021f31cf86270b347df09579f2c0996e999e37e1dddafacec42ed1ef6aae21a265aff947e792 languageName: node linkType: hard "@babel/plugin-syntax-import-attributes@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.24.7" + version: 7.25.6 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.25.6" dependencies: - "@babel/helper-plugin-utils": ^7.24.7 + "@babel/helper-plugin-utils": ^7.24.8 peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 590dbb5d1a15264f74670b427b8d18527672c3d6c91d7bae7e65f80fd810edbc83d90e68065088644cbad3f2457ed265a54a9956fb789fcb9a5b521822b3a275 + checksum: 3b0928e73e42346e8a65760a3ff853c87ad693cdf11bb335a23e895e0b5b1f0601118521b3aff2a6946488a580a63afb6a5b5686153a7678b4dff0e4e4604dd7 languageName: node linkType: hard @@ -1489,12 +1489,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.25.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.25.4 - resolution: "@babel/runtime@npm:7.25.4" +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.25.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.25.6 + resolution: "@babel/runtime@npm:7.25.6" dependencies: regenerator-runtime: ^0.14.0 - checksum: 5c2aab03788e77f1f959d7e6ce714c299adfc9b14fb6295c2a17eb7cad0dd9c2ebfb2d25265f507f68c43d5055c5cd6f71df02feb6502cea44b68432d78bcbbe + checksum: ee1a69d3ac7802803f5ee6a96e652b78b8addc28c6a38c725a4ad7d61a059d9e6cb9f6550ed2f63cce67a1bd82e0b1ef66a1079d895be6bfb536a5cfbd9ccc32 languageName: node linkType: hard @@ -1510,28 +1510,28 @@ __metadata: linkType: hard "@babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.1, @babel/traverse@npm:^7.25.2, @babel/traverse@npm:^7.25.3, @babel/traverse@npm:^7.25.4": - version: 7.25.4 - resolution: "@babel/traverse@npm:7.25.4" + version: 7.25.6 + resolution: "@babel/traverse@npm:7.25.6" dependencies: "@babel/code-frame": ^7.24.7 - "@babel/generator": ^7.25.4 - "@babel/parser": ^7.25.4 + "@babel/generator": ^7.25.6 + "@babel/parser": ^7.25.6 "@babel/template": ^7.25.0 - "@babel/types": ^7.25.4 + "@babel/types": ^7.25.6 debug: ^4.3.1 globals: ^11.1.0 - checksum: 3b6d879b9d843b119501585269b3599f047011ae21eb7820d00aef62fc3a2bcdaf6f4cdf2679795a2d7c0b6b5d218974916e422f08dea08613dc42188ef21e4b + checksum: 11ee47269aa4356f2d6633a05b9af73405b5ed72c09378daf644289b686ef852035a6ac9aa410f601991993c6bbf72006795b5478283b78eb1ca77874ada7737 languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.4, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.25.4 - resolution: "@babel/types@npm:7.25.4" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.2, @babel/types@npm:^7.25.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.25.6 + resolution: "@babel/types@npm:7.25.6" dependencies: "@babel/helper-string-parser": ^7.24.8 "@babel/helper-validator-identifier": ^7.24.7 to-fast-properties: ^2.0.0 - checksum: 497f8b583c54a92a59c3ec542144695064cd5c384fcca46ba1aa301d5e5dd6c1d011f312ca024cb0f9c956da07ae82fb4c348c31a30afa31a074c027720d2aa8 + checksum: 9b2f84ff3f874ad05b0b9bf06862c56f478b65781801f82296b4cc01bee39e79c20a7c0a06959fed0ee582c8267e1cb21638318655c5e070b0287242a844d1c9 languageName: node linkType: hard @@ -2507,11 +2507,10 @@ __metadata: linkType: hard "@mui/x-date-pickers@npm:^7.7.1": - version: 7.14.0 - resolution: "@mui/x-date-pickers@npm:7.14.0" + version: 7.15.0 + resolution: "@mui/x-date-pickers@npm:7.15.0" dependencies: - "@babel/runtime": ^7.25.0 - "@mui/system": ^5.16.7 + "@babel/runtime": ^7.25.4 "@mui/utils": ^5.16.6 "@types/react-transition-group": ^4.4.11 clsx: ^2.1.1 @@ -2520,7 +2519,8 @@ __metadata: peerDependencies: "@emotion/react": ^11.9.0 "@emotion/styled": ^11.8.1 - "@mui/material": ^5.15.14 + "@mui/material": ^5.15.14 || ^6.0.0 + "@mui/system": ^5.15.14 || ^6.0.0 date-fns: ^2.25.0 || ^3.2.0 date-fns-jalali: ^2.13.0-0 || ^3.2.0-0 dayjs: ^1.10.7 @@ -2549,7 +2549,7 @@ __metadata: optional: true moment-jalaali: optional: true - checksum: b7a4c2f23e6136a50b6f520c285a2cf7be418ffe6b26b580b4540c80daa93d8f0ece3873c64e2e41cbe9651dadbf61cfcb0d73a36f6b3c331c6008eb4a836636 + checksum: 00374f0072db2e759e4ae00f45d16cae12f778820d2658ce21ed33e963524d02ec95d4aafc0bbaee8e9559b5fa9ea9c240187d36647cba405a96d0e6e526cfc7 languageName: node linkType: hard @@ -3048,14 +3048,14 @@ __metadata: linkType: hard "@tanstack/react-virtual@npm:^3.8.4": - version: 3.10.5 - resolution: "@tanstack/react-virtual@npm:3.10.5" + version: 3.10.6 + resolution: "@tanstack/react-virtual@npm:3.10.6" dependencies: - "@tanstack/virtual-core": 3.10.5 + "@tanstack/virtual-core": 3.10.6 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 9030eaf58eb948af44454698ececd512c52909202496299846472e0aa0b5ba5dfc188502936880c0249a7fb48a166f471e8dc253826de81a24438686858a80fd + checksum: 50624357867ce8eca4084bfe132b73fe7a2e0f9bcb2a7241b16425d58c0f7602c6f889f28d29a9416f7c3ab12d89239da48d0d84ca470ace94f77cf982d6246e languageName: node linkType: hard @@ -3066,10 +3066,10 @@ __metadata: languageName: node linkType: hard -"@tanstack/virtual-core@npm:3.10.5": - version: 3.10.5 - resolution: "@tanstack/virtual-core@npm:3.10.5" - checksum: 6751500bdfad8f176d7848feaa3c89a3853160169b88b4fa6e7aeea6ed460a9d7326da9e134a61267642a0de2ffcd5524be3b5cf496205a99cbf2dd44f6a098d +"@tanstack/virtual-core@npm:3.10.6": + version: 3.10.6 + resolution: "@tanstack/virtual-core@npm:3.10.6" + checksum: c7eeda2a2ad49b0b5065127094225877656b718301bcd972b80e55b19a7f2f063867a8d598544e824cc230d080124c9c34abeb8517c0ed3f5759c16c1f86acd8 languageName: node linkType: hard @@ -3800,9 +3800,9 @@ __metadata: linkType: hard "ace-builds@npm:^1.31.1": - version: 1.36.0 - resolution: "ace-builds@npm:1.36.0" - checksum: c32ce439489c0b5d294f7d8dfd71c5ffd1693132ecbfd8b77ba2b9ed86e46fe3b0ff1c8dc236b87b86a90f1870b2ce516605b81bf92508be240e02af691c91cd + version: 1.36.1 + resolution: "ace-builds@npm:1.36.1" + checksum: 79a6fa893c775d07af17c2e29a985da209b9e8504936554b46c15761d7a9e2bdfb94840202fd9dd1df4e321b6f89d644b9cec404785bb91bee1ca469ea8feb41 languageName: node linkType: hard @@ -8286,13 +8286,13 @@ __metadata: languageName: node linkType: hard -"html-dom-parser@npm:5.0.9": - version: 5.0.9 - resolution: "html-dom-parser@npm:5.0.9" +"html-dom-parser@npm:5.0.10": + version: 5.0.10 + resolution: "html-dom-parser@npm:5.0.10" dependencies: domhandler: 5.0.3 htmlparser2: 9.1.0 - checksum: babc50f37e74521f777a304155eae59bdf4662568b880e17120187c5980e90a655a419e093effa1594d20f3f6992c2a6c71910dc2aea883315a3f4a1d5e2d491 + checksum: ec0470f9f6046af7d4d591aea15b49ca0a178ce430d09cadbb098212749a0b2c9b078cf9ede5df50238bc2842c17c7895126788c51927ff726088aad36c98ab5 languageName: node linkType: hard @@ -8313,11 +8313,11 @@ __metadata: linkType: hard "html-react-parser@npm:^5.0.6": - version: 5.1.14 - resolution: "html-react-parser@npm:5.1.14" + version: 5.1.15 + resolution: "html-react-parser@npm:5.1.15" dependencies: domhandler: 5.0.3 - html-dom-parser: 5.0.9 + html-dom-parser: 5.0.10 react-property: 2.0.2 style-to-js: 1.1.13 peerDependencies: @@ -8326,7 +8326,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: fddfd507d932ebe289852596d5037f3ac826aff13f32bbbd20790ee5ad5abc45ab80025f76ffe0008d3f186c2972785ade8d72c1c59c1c4a9f3ac7734fd07acc + checksum: 83e21ac01809a85d20a5fd2c554cd837e94a6a7e3fa34f59d3bc56dfeb73ea49c4f22f3af79648d08ca59dda0f26b850c21c69104a22fec4e4017dd8d6e111cc languageName: node linkType: hard @@ -13273,7 +13273,7 @@ __metadata: languageName: node linkType: hard -"react-rnd@npm:^10.3.5": +"react-rnd@npm:^10.4.12": version: 10.4.12 resolution: "react-rnd@npm:10.4.12" dependencies: @@ -13880,7 +13880,7 @@ __metadata: react-leaflet: ^4.2.1 react-new-window: ^1.0.1 react-resize-detector: ^11.0.1 - react-rnd: ^10.3.5 + react-rnd: ^10.4.12 react-select: ^5.7.2 react-timer-hook: ^3.0.5 react-virtualized-auto-sizer: ^1.0.6 @@ -13907,7 +13907,7 @@ __metadata: webpack-cli: ^5.1.4 wkx: ^0.5.0 yarn-audit-html: 4.0.0 - zustand: ^4.4.1 + zustand: ^4.5.4 languageName: unknown linkType: soft @@ -15465,11 +15465,11 @@ __metadata: linkType: hard "uglify-js@npm:^3.1.4": - version: 3.19.2 - resolution: "uglify-js@npm:3.19.2" + version: 3.19.3 + resolution: "uglify-js@npm:3.19.3" bin: uglifyjs: bin/uglifyjs - checksum: 2236220638223f72340d770daa46704a6f54bcd3022e04510a55bb693a40c32e38a9a439333703f16c9880226cc9952c0dddfe67e7b870c287d915b54757ab51 + checksum: 7ed6272fba562eb6a3149cfd13cda662f115847865c03099e3995a0e7a910eba37b82d4fccf9e88271bb2bcbe505bb374967450f433c17fa27aa36d94a8d0553 languageName: node linkType: hard @@ -16387,7 +16387,7 @@ __metadata: languageName: node linkType: hard -"zustand@npm:^4.4.1": +"zustand@npm:^4.5.4": version: 4.5.5 resolution: "zustand@npm:4.5.5" dependencies: From 91c58432f19fdb392e39d74a26d7ab8a75c46c2d Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Fri, 30 Aug 2024 00:46:53 +0530 Subject: [PATCH 02/26] Provide the name of the view during the registration as production code will mangled the name of the component function, which would lead to 'View not found' error. --- web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx | 2 +- web/pgadmin/static/js/SchemaView/FieldSetView.jsx | 2 +- web/pgadmin/static/js/SchemaView/FormView.jsx | 2 +- web/pgadmin/static/js/SchemaView/InlineView.jsx | 2 +- web/pgadmin/static/js/SchemaView/SchemaView.jsx | 2 +- web/pgadmin/static/js/SchemaView/registry.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx index 9c8ff2baa43..b60363f7e32 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx @@ -195,4 +195,4 @@ DataGridView.propTypes = { field: PropTypes.object, }; -registerView(DataGridView); +registerView(DataGridView, 'DataGridView'); diff --git a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx index 6bdad064fc9..1c7e41442f4 100644 --- a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx +++ b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx @@ -73,4 +73,4 @@ FieldSetView.propTypes = { field: PropTypes.object, }; -registerView(FieldSetView); +registerView(FieldSetView, 'FieldSetView'); diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index 5f7e77c161e..f1d43eac0ac 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -276,4 +276,4 @@ FormView.propTypes = { showError: PropTypes.bool, }; -registerView(FormView); +registerView(FormView, 'FormView'); diff --git a/web/pgadmin/static/js/SchemaView/InlineView.jsx b/web/pgadmin/static/js/SchemaView/InlineView.jsx index 1398e0310c3..c692729b8ce 100644 --- a/web/pgadmin/static/js/SchemaView/InlineView.jsx +++ b/web/pgadmin/static/js/SchemaView/InlineView.jsx @@ -53,4 +53,4 @@ InlineView.propTypes = { ]) }; -registerView(InlineView); +registerView(InlineView, 'InlineView'); diff --git a/web/pgadmin/static/js/SchemaView/SchemaView.jsx b/web/pgadmin/static/js/SchemaView/SchemaView.jsx index 61aa9ae919e..537658822f8 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaView.jsx @@ -34,4 +34,4 @@ SchemaView.propTypes = { formType: PropTypes.oneOf(['tab', 'dialog']), }; -registerView(SchemaView); +registerView(SchemaView, 'SchemaView'); diff --git a/web/pgadmin/static/js/SchemaView/registry.js b/web/pgadmin/static/js/SchemaView/registry.js index 0c454f0b0d3..9392887a5ef 100644 --- a/web/pgadmin/static/js/SchemaView/registry.js +++ b/web/pgadmin/static/js/SchemaView/registry.js @@ -34,7 +34,7 @@ export function View(name) { const view = _views[name]; if (view) return view; - throw new Error('View is not found in the registry.'); + throw new Error(`View ${name} is not found in the registry.`); } export function hasView(name) { From 664e9da458625ba78e736e5d133f245a6d775565 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Fri, 30 Aug 2024 17:24:12 +0530 Subject: [PATCH 03/26] Wait to element to appear before finding the source of the rolename --- .../feature_tests/xss_checks_roles_control_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/regression/feature_tests/xss_checks_roles_control_test.py b/web/regression/feature_tests/xss_checks_roles_control_test.py index c1173b7000e..78e88ed3e21 100644 --- a/web/regression/feature_tests/xss_checks_roles_control_test.py +++ b/web/regression/feature_tests/xss_checks_roles_control_test.py @@ -78,6 +78,11 @@ def _check_role_membership_control(self): By.XPATH, "//button[normalize-space(text())='Membership']"))) membership_tab.click() + WebDriverWait(self.page.driver, 2).until( + EC.presence_of_element_located(( + By.XPATH, "//div[contains(@class, 'pgrd-row-cell')]")) + ) + # Fetch the source code for our custom control source_code = self.page.find_by_xpath( "//div[contains(@class, 'pgrd-row-cell')]" From 044931dfd41ca4cdd94822f7ff18e3ece3b7d830 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Fri, 30 Aug 2024 17:34:17 +0530 Subject: [PATCH 04/26] fixed typo introduced during merging the 'master' branch. --- .../preferences/static/js/components/PreferencesComponent.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx index 4aa5af6d149..5e4f384d988 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -698,10 +698,10 @@ export default function PreferencesComponent({ ...props }) { />
- }> + }> {gettext('Reset all preferences')} - { props.closeModal();}} startIcon={ { props.closeModal();}} />}> { props.closeModal();}} startIcon={ From ab2e467bd961f9c4f533b3b02a435d6baf95073a Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Sat, 31 Aug 2024 02:23:04 +0530 Subject: [PATCH 05/26] Pass the reset key to 'FormView' from SchemaDialogView, and set the reset the tab only, when reset key is changed. --- web/pgadmin/static/js/SchemaView/FormView.jsx | 9 ++++--- .../static/js/SchemaView/SchemaDialogView.jsx | 2 +- web/regression/README.md | 2 +- .../xss_checks_roles_control_test.py | 7 +---- web/regression/feature_utils/pgadmin_page.py | 26 ++++++++++++------- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index f1d43eac0ac..f97cf83c341 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -59,7 +59,7 @@ const ErrorMessageBox = () => { export default function FormView({ accessPath, schema=null, isNested=false, dataDispatch, className, hasSQLTab, getSQLValue, isTabView=true, viewHelperProps, field, - showError=false + showError=false, resetKey }) { const [key, setKey] = useState(0); const schemaState = useContext(SchemaStateContext); @@ -104,10 +104,11 @@ export default function FormView({ // Upon reset, set the tab to first. useEffect(() => { - if (!visible) return; - if (schemaState?.isReady) + if (!visible || !resetKey) return; + if (resetKey) { setTabValue(0); - }, [schemaState?.isReady]); + } + }, [resetKey]); const finalGroups = useMemo( () => createFieldControls({ diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index dfcd11cc697..b6591e3c9f8 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -197,7 +197,7 @@ export default function SchemaDialogView({ hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} firstEleRef={firstEleRef} isTabView={isTabView} className={props.formClassName} - showError={true} + showError={true} resetKey={props.resetKey} /> {showFooter && diff --git a/web/regression/README.md b/web/regression/README.md index a565228a654..32178728f22 100644 --- a/web/regression/README.md +++ b/web/regression/README.md @@ -142,7 +142,7 @@ Python Tests: 'pgadmin4/web/pgadmin/utils/test.py' file. - To run Feature Tests in parallel using selenoid(grid + docker), selenoid - need to be installed nad should be run only with SERVER_MODE=True. + need to be installed and should be run only with SERVER_MODE=True. Steps to install selenoid - - Install & Start docker diff --git a/web/regression/feature_tests/xss_checks_roles_control_test.py b/web/regression/feature_tests/xss_checks_roles_control_test.py index 78e88ed3e21..e113c5aea18 100644 --- a/web/regression/feature_tests/xss_checks_roles_control_test.py +++ b/web/regression/feature_tests/xss_checks_roles_control_test.py @@ -73,16 +73,11 @@ def _check_role_membership_control(self): edit_object = self.wait.until(EC.visibility_of_element_located( (By.CSS_SELECTOR, NavMenuLocators.edit_obj_css))) edit_object.click() - membership_tab = WebDriverWait(self.page.driver, 4).until( + membership_tab = WebDriverWait(self.page.driver, 2).until( EC.presence_of_element_located(( By.XPATH, "//button[normalize-space(text())='Membership']"))) membership_tab.click() - WebDriverWait(self.page.driver, 2).until( - EC.presence_of_element_located(( - By.XPATH, "//div[contains(@class, 'pgrd-row-cell')]")) - ) - # Fetch the source code for our custom control source_code = self.page.find_by_xpath( "//div[contains(@class, 'pgrd-row-cell')]" diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 2ec898a3d59..8361d50afe6 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -1147,29 +1147,37 @@ def check_if_element_exists_with_scroll(self, xpath): bottom_ele = self.driver.find_element( By.XPATH, "//div[@id='id-object-explorer']" - "/div/div/div/div/div[last()]") - bottom_ele_location = int( - bottom_ele.value_of_css_property('top').split("px")[0]) + "/div/div/div/div/div/div[last()]") + bottom_ele_top = bottom_ele.value_of_css_property('top') + bottom_ele_location = 1 + + if (bottom_ele_top != 'auto'): + bottom_ele_location = int( + bottom_ele_top.split("px")[0] + ) if tree_height - bottom_ele_location < 25: - f_scroll = 0 + f_scroll = bottom_ele_location - 25 else: self.driver.execute_script( - self.js_executor_scrollintoview_arg, bottom_ele) + self.js_executor_scrollintoview_arg, bottom_ele + ) f_scroll -= 1 elif r_scroll > 0: top_el = self.driver.find_element( By.XPATH, "//div[@id='id-object-explorer']" "/div/div/div/div/div[1]") - top_el_location = int( - top_el.value_of_css_property('top').split("px")[0]) + top_el_top = top_el.value_of_css_property('top') + top_el_location = 0 + + if (top_el_top != 'auto'): + top_el_location = int(top_el_top.split("px")[0]) if (tree_height - top_el_location) == tree_height: r_scroll = 0 else: - webdriver.ActionChains(self.driver).move_to_element( - top_el).perform() + self.js_executor_scrollintoview_arg, top_el) r_scroll -= 1 else: break From 4c80dd9cad559c9193c9d2b5e0ed20817dffb68e Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Sat, 31 Aug 2024 02:30:27 +0530 Subject: [PATCH 06/26] Fixed a typo --- web/regression/feature_utils/pgadmin_page.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 8361d50afe6..2b5a7f296d4 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -1177,7 +1177,9 @@ def check_if_element_exists_with_scroll(self, xpath): if (tree_height - top_el_location) == tree_height: r_scroll = 0 else: - self.js_executor_scrollintoview_arg, top_el) + self.driver.execute_script( + self.js_executor_scrollintoview_arg, top_el + ) r_scroll -= 1 else: break From 3ff9916a41586730edb7a8d9c300a7a4ab8dab84 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 2 Sep 2024 14:03:34 +0530 Subject: [PATCH 07/26] Used 'origData' variable to get the initiail data for validation of the data. I never realised that - we were using 'origData' for schema validation to get the initial data, and replaced it with 'sessData' variable by mistake. Changing it back to use 'origData' based on the feedback from Aditya. --- .../publications/static/js/publication.ui.js | 2 +- .../static/js/domain_constraints.ui.js | 2 +- .../schemas/domains/static/js/domain.ui.js | 4 ++- .../schemas/packages/static/js/package.ui.js | 4 +-- .../tables/columns/static/js/column.ui.js | 2 +- .../static/js/check_constraint.ui.js | 2 +- .../foreign_key/static/js/foreign_key.ui.js | 28 +++++++++++++------ .../tables/indexes/static/js/index.ui.js | 3 +- .../schemas/tables/static/js/table.ui.js | 2 +- .../schemas/views/static/js/mview.ui.js | 6 +++- .../schemas/views/static/js/view.ui.js | 12 ++++---- .../databases/static/js/database.ui.js | 2 +- .../static/js/subscription.ui.js | 10 +++++-- .../servers/static/js/server.ui.js | 10 +++---- 14 files changed, 57 insertions(+), 32 deletions(-) diff --git a/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js b/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js index 2e22f476559..e174d7acbb7 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/publications/static/js/publication.ui.js @@ -209,7 +209,7 @@ export default class PublicationSchema extends BaseUISchema { } if ( !_.isUndefined(table) && table.length > 0 && - !_.isEqual(this.sessData.pubtable, state.pubtable) + !_.isEqual(this.origData.pubtable, state.pubtable) ){ return false; } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js index d3a793e00f3..cae3a8c7b56 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/domain_constraints/static/js/domain_constraints.ui.js @@ -51,7 +51,7 @@ export default class DomainConstraintSchema extends BaseUISchema { cell:'boolean', group: gettext('Definition'), min_version: 90200, mode: ['properties', 'create', 'edit'], readonly: function(state) { - return !obj.isNew(state) && obj.sessData.convalidated; + return !obj.isNew(state) && obj.origData.convalidated; } } ]; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js index bc745ec5a95..b2ce98d4f61 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js @@ -39,7 +39,9 @@ export class DomainConstSchema extends BaseUISchema { id: 'convalidated', label: gettext('Validate?'), cell: 'checkbox', type: 'checkbox', readonly: function(state) { - let currCon = _.find(obj.top.sessData.constraints, (con)=>con.conoid == state.conoid); + let currCon = _.find( + obj.top.origData.constraints, (con) => con.conoid == state.conoid + ); return !obj.isNew(state) && currCon.convalidated; }, } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js index 1149cb0350a..f48b752d5f8 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/packages/static/js/package.ui.js @@ -101,7 +101,7 @@ export default class PackageSchema extends BaseUISchema { depChange: (state, source, topState, actionObj) => { if( - packageSchemaObj.sessData.oid && + packageSchemaObj.origData.oid && state.pkgheadsrc != actionObj.oldState.pkgheadsrc ) { packageSchemaObj.warningText = gettext( @@ -120,7 +120,7 @@ export default class PackageSchema extends BaseUISchema { depChange: (state, source, topState, actionObj) => { if( - packageSchemaObj.sessData.oid && + packageSchemaObj.origData.oid && state.pkgbodysrc != actionObj.oldState.pkgbodysrc ) { packageSchemaObj.warningText = gettext( diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js index 8c74097aec2..afa4b8d5d1d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js @@ -622,7 +622,7 @@ export default class ColumnSchema extends BaseUISchema { } if (!this.isNew(state) && state.colconstype == 'i' - && (this.sessData.attidentity == 'a' || this.sessData.attidentity == 'd') + && (this.origData.attidentity == 'a' || this.origData.attidentity == 'd') && (state.attidentity == 'a' || state.attidentity == 'd')) { if(isEmptyString(state.seqincrement)) { msg = gettext('Increment value cannot be empty.'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js index 4ea95a7984d..57046b0d4c1 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js @@ -90,7 +90,7 @@ export default class CheckConstraintSchema extends BaseUISchema { if(obj.inTable && obj.top && !obj.top.isNew()) { return !(_.isUndefined(state.oid) || state.convalidated); } - return !obj.isNew(state) && !obj.sessData.convalidated; + return !obj.isNew(state) && !obj.origData.convalidated; }, mode: ['properties', 'create', 'edit'], }]; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js index acd577c110f..eb2c9cb0699 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js @@ -59,12 +59,20 @@ class ForeignKeyHeaderSchema extends BaseUISchema { } addDisabled(state) { - return !(state.local_column && (state.references || this.sessData.references) && state.referenced); + return !( + state.local_column && ( + state.references || this.origData.references + ) && state.referenced + ); } /* Data to ForeignKeyColumnSchema will added using the header form */ getNewData(data) { - let references_table_name = _.find(this.refTables, (t)=>t.value==data.references || t.value == this.sessData.references)?.label; + let references_table_name = _.find( + this.refTables, + (t) => t.value == data.references || t.value == this.origData.references + )?.label; + return { local_column: data.local_column, referenced: data.referenced, @@ -228,16 +236,16 @@ export default class ForeignKeySchema extends BaseUISchema { type: 'switch', group: gettext('Definition'), readonly: (state)=>{ if(!obj.isNew(state)) { - let sessData = {}; + let origData = {}; if(obj.inTable && obj.top) { - sessData = _.find( - obj.top.sessData['foreign_key'], + origData = _.find( + obj.top.origData['foreign_key'], (r) => r.cid == state.cid ); } else { - sessData = obj.sessData; + origData = obj.origData; } - return sessData.convalidated; + return origData.convalidated; } return false; }, @@ -363,8 +371,10 @@ export default class ForeignKeySchema extends BaseUISchema { } if(actionObj.type == SCHEMA_STATE_ACTIONS.ADD_ROW) { // Set references value. - obj.fkHeaderSchema.sessData.references = null; - obj.fkHeaderSchema.sessData._disable_references = true; + obj.fkHeaderSchema.origData.references = null; + obj.fkHeaderSchema.origData.references = + obj.fkHeaderSchema.sessData.references; + obj.fkHeaderSchema.origData._disable_references = true; } return {columns: currColumns}; }, diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js index b6cedb88a86..14a36238351 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui.js @@ -156,7 +156,8 @@ class IndexColumnSchema extends BaseUISchema { * to access method selected by user if not selected * send btree related op_class options */ - let amname = obj.top?.sessData.amname; + let amname = obj.top?.sessData.amname || + obj.top?.origData.amname; if(_.isUndefined(amname)) return options; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js index 67d4dab63a4..5001f98c6ca 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js @@ -635,7 +635,7 @@ export default class TableSchema extends BaseUISchema { type: 'switch', mode: ['properties','edit', 'create'], group: 'advanced', min_version: 90600, depChange: (state)=>{ - if (state.rlspolicy && this.sessData.rlspolicy != state.rlspolicy) { + if (state.rlspolicy && this.origData.rlspolicy != state.rlspolicy) { pgAdmin.Browser.notifier.alert( gettext('Check Policy?'), gettext('Please check if any policy exists. If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified by other users') diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js index e1521f07351..d888e2fa915 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/mview.ui.js @@ -7,6 +7,7 @@ // ////////////////////////////////////////////////////////////// +import _ from 'lodash'; import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import SecLabelSchema from '../../../../../static/js/sec_label.ui'; @@ -154,7 +155,10 @@ export default class MViewSchema extends BaseUISchema { if (state.definition) { obj.warningText = null; - if (obj.sessData.oid !== undefined && state.definition !== obj.sessData.definition) { + if ( + !_.isUndefined(obj.origData.oid) && + state.definition !== obj.origData.definition + ) { obj.warningText = gettext( 'Updating the definition will drop and re-create the materialized view. It may result in loss of information about its dependent objects.' ) + '

' + gettext('Do you want to continue?') + ''; diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js index 6ab6338e587..0ffdf161506 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/views/static/js/view.ui.js @@ -7,6 +7,7 @@ // ////////////////////////////////////////////////////////////// +import _ from 'lodash'; import gettext from 'sources/gettext'; import BaseUISchema from 'sources/SchemaView/base_schema.ui'; import SecLabelSchema from '../../../../../static/js/sec_label.ui'; @@ -135,22 +136,23 @@ export default class ViewSchema extends BaseUISchema { } if (state.definition) { - if (!(obj.nodeInfo.server.server_type == 'pg' && + if (!( + obj.nodeInfo.server.server_type == 'pg' && // No need to check this when creating a view - obj.sessData.oid !== undefined + !_.isUndefined(obj.sessData.oid) ) || ( - state.definition === obj.sessData.definition + state.definition === obj.origData.definition )) { obj.warningText = null; return false; } - let old_def = obj.sessData.definition?.replace( + let old_def = obj.origData.definition?.replace( /\s/gi, '' ).split('FROM'), new_def = []; - if (state.definition !== undefined) { + if (!_.isUndefined(state.definition)) { new_def = state.definition.replace( /\s/gi, '' ).split('FROM'); diff --git a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js index 3b0cf68e515..f13c4d7c438 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/static/js/database.ui.js @@ -284,7 +284,7 @@ export default class DatabaseSchema extends BaseUISchema { obj.informText = undefined; } - if(!_.isEqual(obj.sessData.schema_res, state.schema_res)) { + if(!_.isEqual(obj.origData.schema_res, state.schema_res)) { obj.informText = gettext( 'Please refresh the Schemas node to make changes to the schema restriction take effect.' ); diff --git a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js index 0415c918211..a081324de50 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js @@ -129,7 +129,10 @@ export default class SubscriptionSchema extends BaseUISchema{ id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], min: 1, max: 65535, depChange: (state)=>{ - if(obj.sessData.port != state.port && !obj.isNew(state) && state.connected){ + if( + obj.origData.port != state.port && !obj.isNew(state) && + state.connected + ) { obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); @@ -145,7 +148,10 @@ export default class SubscriptionSchema extends BaseUISchema{ id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], depChange: (state)=>{ - if(obj.sessData.username != state.username && !obj.isNew(state) && state.connected){ + if( + obj.origData.username != state.username && !obj.isNew(state) && + state.connected + ) { obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js index 3ca48a917cc..a6a619aa2a1 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js @@ -137,13 +137,13 @@ export default class ServerSchema extends BaseUISchema { controlProps: { maxLength: 64}, mode: ['properties', 'create', 'edit'], deps: ['shared', 'username'], readonly: (s) => { - return !(!this.sessData.shared && s.shared); + return !(!this.origData.shared && s.shared); }, visible: ()=>{ return current_user.is_admin && pgAdmin.server_mode == 'True'; }, depChange: (state, source, _topState, actionObj)=>{ let ret = {}; - if(this.sessData.shared) { + if(this.origData.shared) { return ret; } if(source == 'username' && actionObj.oldState.username == state.shared_username) { @@ -169,7 +169,7 @@ export default class ServerSchema extends BaseUISchema { id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], disabled: obj.isShared, depChange: (state)=>{ - if(obj.sessData.host != state.host && !obj.isNew(state) && state.connected){ + if(obj.origData.host != state.host && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); @@ -182,7 +182,7 @@ export default class ServerSchema extends BaseUISchema { id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: obj.isShared, depChange: (state)=>{ - if(obj.sessData.port != state.port && !obj.isNew(state) && state.connected){ + if(obj.origData.port != state.port && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); @@ -198,7 +198,7 @@ export default class ServerSchema extends BaseUISchema { id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], depChange: (state)=>{ - if(obj.sessData.username != state.username && !obj.isNew(state) && state.connected){ + if(obj.origData.username != state.username && !obj.isNew(state) && state.connected){ obj.informText = gettext( 'To apply changes to the connection configuration, please disconnect from the server and then reconnect.' ); From 1ed674f9a38895dbcb43a418cf48b0d3aa696fd5 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 2 Sep 2024 16:02:01 +0530 Subject: [PATCH 08/26] Keep the value of the input-box locally as well to solve the caret jumping issue. It fixes #7878. Reference: - https://dev.to/kwirke/solving-caret-jumping-in-react-inputs-36ic --- .../static/js/components/FormComponents.jsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index 0ba592aec89..cbd6e1b4985 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -354,12 +354,21 @@ export const InputText = forwardRef(({ cid, helpid, readonly, disabled, value, onChange, controlProps, type, size, inputStyle, ...props }, ref) => { const maxlength = typeof(controlProps?.maxLength) != 'undefined' ? controlProps.maxLength : 255; - const patterns = { 'numeric': '^-?[0-9]\\d*\\.?\\d*$', 'int': '^-?[0-9]\\d*$', }; - let onChangeFinal = (e) => { + + let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value; + + if (controlProps?.formatter) { + finalValue = controlProps.formatter.fromRaw(finalValue); + } + + if (_.isNull(finalValue) || _.isUndefined(finalValue)) finalValue = ''; + + const [val, setVal] = useState(finalValue); + const onChangeFinal = (e) => { let changeVal = e.target.value; /* For type number, we set type as tel with number regex to get validity.*/ @@ -371,14 +380,10 @@ export const InputText = forwardRef(({ if (controlProps?.formatter) { changeVal = controlProps.formatter.toRaw(changeVal); } + setVal(changeVal); onChange?.(changeVal); }; - let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value; - - if (controlProps?.formatter) { - finalValue = controlProps.formatter.fromRaw(finalValue); - } const filteredProps = _.pickBy(props, (_v, key)=>( /* When used in ButtonGroup, following props should be skipped */ @@ -406,7 +411,7 @@ export const InputText = forwardRef(({ disabled={Boolean(disabled)} rows={4} notched={false} - value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue} + value={val} onChange={onChangeFinal} { ...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown }) From 3fe575b74e889df8e67d497f0cf67548df898999 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 2 Sep 2024 16:43:49 +0530 Subject: [PATCH 09/26] Fixed the review comments shared by Aditya. TODO:: Yet to work on the issues for the partition and table dialog UI. --- .../schemas/tables/static/js/table.ui.js | 18 +++++++++--------- .../schemas/types/static/js/type.ui.js | 2 +- .../features/{fixedrows.jsx => fixedRows.jsx} | 0 .../SchemaView/DataGridView/features/index.jsx | 2 +- .../static/js/SchemaView/SchemaDialogView.jsx | 8 ++++---- .../js/SchemaView/SchemaState/SchemaState.js | 9 +++++---- 6 files changed, 20 insertions(+), 19 deletions(-) rename web/pgadmin/static/js/SchemaView/DataGridView/features/{fixedrows.jsx => fixedRows.jsx} (100%) diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js index 5001f98c6ca..80c5829cce9 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js @@ -276,47 +276,47 @@ export class LikeSchema extends BaseUISchema { id: 'like_default_value', label: gettext('With default values?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', },{ id: 'like_constraints', label: gettext('With constraints?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', },{ id: 'like_indexes', label: gettext('With indexes?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', },{ id: 'like_storage', label: gettext('With storage?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', },{ id: 'like_comments', label: gettext('With comments?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', },{ id: 'like_compression', label: gettext('With compression?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - min_version: 140000, inlineGroup: 'like_relelation', + min_version: 140000, inlineGroup: 'like_relation', },{ id: 'like_generated', label: gettext('With generated?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - min_version: 120000, inlineGroup: 'like_relelation', + min_version: 120000, inlineGroup: 'like_relation', },{ id: 'like_identity', label: gettext('With identity?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', },{ id: 'like_statistics', label: gettext('With statistics?'), type: 'switch', mode: ['create'], deps: ['like_relation'], disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), - inlineGroup: 'like_relelation', + inlineGroup: 'like_relation', } ]; } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js index 907b66fab5a..573a650d6ea 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/js/type.ui.js @@ -1024,7 +1024,7 @@ class DataTypeSchema extends BaseUISchema { return [{ id: 'type', label: gettext('Data Type'), - group: gettext('Data Type'), + group: gettext('Definition'), mode: ['edit', 'create'], disabled: false, readonly: function (state) { diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/fixedrows.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/fixedRows.jsx similarity index 100% rename from web/pgadmin/static/js/SchemaView/DataGridView/features/fixedrows.jsx rename to web/pgadmin/static/js/SchemaView/DataGridView/features/fixedRows.jsx diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx index d1dcc08909b..6ab26acaec0 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/index.jsx @@ -10,7 +10,7 @@ // The DataGridView component is feature support better extendability. import { Feature, FeatureSet, register } from './feature'; -import FixedRows from './fixedrows'; +import FixedRows from './fixedRows'; import Reorder from './reorder'; import ExpandedFormView from './expandabledFormView'; import DeletableRow from './deletable'; diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index b6591e3c9f8..1cfae00ac23 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -131,7 +131,7 @@ export default function SchemaDialogView({ const onSaveClick = () => { // Do nothing when there is no change or there is an error if ( - !schemaState.changes || Object.keys(schemaState.changes) === 0 || + !schemaState._changes || Object.keys(schemaState._changes) === 0 || schemaState.errors.name ) return; @@ -139,14 +139,14 @@ export default function SchemaDialogView({ setLoaderText('Saving...'); if (!schema.warningText) { - save(schemaState.Changes(true)); + save(schemaState.changes(true)); return; } Notifier.confirm( gettext('Warning'), schema.warningText, - () => { save(schemaState.Changes(true)); }, + () => { save(schemaState.changes(true)); }, () => { setSaving(false); setLoaderText(''); @@ -165,7 +165,7 @@ export default function SchemaDialogView({ return Promise.resolve('-- ' + gettext('Definition incomplete.')); } - const changeData = schemaState.changes; + const changeData = schemaState._changes; /* * Call the passed incoming getSQLValue func to get the SQL * return of getSQLValue should be a promise. diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js index b74bd4d155d..c799713d180 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js +++ b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js @@ -58,7 +58,8 @@ export class SchemaState extends DepListener { ////// State variables // Diff between the current snapshot and initial data. - this.changes = {}; + // Internal state for keeping the changes + this._changes = {}; // Current Loading state this.loadingState = LOADING_STATE.INIT; this.customLoadingText = loadingText; @@ -220,12 +221,12 @@ export class SchemaState extends DepListener { ) state.setError({}); state.data = sessData; - state.changes = state.Changes(); + state._changes = state.changes(); state.updateOptions(); - state.onDataChange && state.onDataChange(state.isDirty, state.changes); + state.onDataChange && state.onDataChange(state.isDirty, state._changes); } - Changes(includeSkipChange=false) { + changes(includeSkipChange=false) { const state = this; const sessData = this.data; const schema = state.schema; From 740d9c644511c592414440ef2aab4eea1490b393 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 2 Sep 2024 23:41:11 +0530 Subject: [PATCH 10/26] Fixed the InputText testcases, failing after the jumping cursor issues fix. --- web/pgadmin/static/js/components/FormComponents.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index cbd6e1b4985..95b49307a10 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -368,6 +368,8 @@ export const InputText = forwardRef(({ if (_.isNull(finalValue) || _.isUndefined(finalValue)) finalValue = ''; const [val, setVal] = useState(finalValue); + + useEffect(() => setVal(finalValue), [finalValue]); const onChangeFinal = (e) => { let changeVal = e.target.value; From 81885048e2fa3745f4276479924f574aa0c8c621 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 10:31:57 +0530 Subject: [PATCH 11/26] Introduce the 'useFieldSchema' custom hook to calculate the 'visible' option for the field having no 'id'. e.g. a field with type 'nested-tab', 'nested-fieldset'. --- .../static/js/SchemaView/FieldSetView.jsx | 17 +++--- web/pgadmin/static/js/SchemaView/FormView.jsx | 7 ++- .../js/SchemaView/SchemaState/SchemaState.js | 1 + .../static/js/SchemaView/hooks/index.js | 2 + .../js/SchemaView/hooks/useFieldSchema.js | 56 +++++++++++++++++++ 5 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js diff --git a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx index 1c7e41442f4..556b92305f5 100644 --- a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx +++ b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx @@ -7,7 +7,7 @@ // ////////////////////////////////////////////////////////////// -import React, { useContext, useMemo } from 'react'; +import React, { useContext, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import FieldSet from 'sources/components/FieldSet'; @@ -15,7 +15,7 @@ import CustomPropTypes from 'sources/custom_prop_types'; import { FieldControl } from './FieldControl'; import { SchemaStateContext } from './SchemaState'; -import { useFieldOptions } from './hooks'; +import { useFieldSchema, useFieldValue } from './hooks'; import { registerView } from './registry'; import { createFieldControls, listenDepChanges } from './utils'; @@ -23,9 +23,13 @@ import { createFieldControls, listenDepChanges } from './utils'; export default function FieldSetView({ field, accessPath, dataDispatch, viewHelperProps, controlClassName, }) { + const [key, setRefreshKey] = useState(0); const schema = field.schema; const schemaState = useContext(SchemaStateContext); - const options = useFieldOptions(accessPath, schemaState); + const value = useFieldValue(accessPath, schemaState); + const options = useFieldSchema( + field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey + ); const label = field.label; listenDepChanges(accessPath, field, options.visible, schemaState); @@ -42,13 +46,6 @@ export default function FieldSetView({ return <>; } - if (fieldGroups.length > 1) { - throw new Error( - 'Developers: Avoid using multiple groups within a fieldSet.' + - JSON.stringify(field?.id) + JSON.stringify(fieldGroups) - ); - } - return (
{fieldGroups.map( diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index f97cf83c341..2ac52569878 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -27,7 +27,7 @@ import { FieldControl } from './FieldControl'; import { SQLTab } from './SQLTab'; import { FormContentBox } from './StyledComponents'; import { SchemaStateContext } from './SchemaState'; -import { useFieldOptions } from './hooks'; +import { useFieldSchema, useFieldValue } from './hooks'; import { registerView, View } from './registry'; import { createFieldControls, listenDepChanges } from './utils'; @@ -63,7 +63,10 @@ export default function FormView({ }) { const [key, setKey] = useState(0); const schemaState = useContext(SchemaStateContext); - const { visible } = useFieldOptions(accessPath, schemaState); + const value = useFieldValue(accessPath, schemaState); + const { visible } = useFieldSchema( + field, accessPath, value, viewHelperProps, schemaState, key, setKey + ); const [tabValue, setTabValue] = useState(0); const formRef = useRef(); diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js index c799713d180..152560597b6 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js +++ b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js @@ -295,6 +295,7 @@ export class SchemaState extends DepListener { } value(path) { + if (!path || !path.length) return this.data; return _.get(this.data, path); } diff --git a/web/pgadmin/static/js/SchemaView/hooks/index.js b/web/pgadmin/static/js/SchemaView/hooks/index.js index d026c8ae839..c294172fd81 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/index.js +++ b/web/pgadmin/static/js/SchemaView/hooks/index.js @@ -11,11 +11,13 @@ import { useFieldError } from './useFieldError'; import { useFieldOptions } from './useFieldOptions'; import { useFieldValue } from './useFieldValue'; import { useSchemaState } from './useSchemaState'; +import { useFieldSchema } from './useFieldSchema'; export { useFieldError, useFieldOptions, useFieldValue, + useFieldSchema, useSchemaState, }; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js new file mode 100644 index 00000000000..5b798b0c946 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js @@ -0,0 +1,56 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2024, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import _ from 'lodash'; +import { useEffect } from 'react'; + +import { booleanEvaluator } from '../options'; + + +export const useFieldSchema = ( + field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey +) => { + useEffect(() => { + if (!schemaState || !field) return; + + // It already has 'id', 'options' is already evaluated. + if (field.id) + return schemaState.subscribe( + accessPath, () => setRefreshKey?.({id: Date.now()}), 'options' + ); + + // There are no dependencies. + if (!_.isArray(field?.deps)) return; + + // Subscribe to all the dependents. + const unsubscribers = field.deps.map((dep) => ( + schemaState.subscribe( + accessPath.concat(dep), () => setRefreshKey?.({id: Date.now()}), + 'value' + ) + )); + + return () => { + unsubscribers.forEach(unsubscribe => unsubscribe()); + }; + }); + + if (!field) return { visible: true }; + if (field.id) return schemaState?.options(accessPath); + if (!field.schema) return { visible: true }; + + value = value || {}; + + return { + visible: booleanEvaluator({ + schema: field.schema, field, option: 'visible', value, viewHelperProps, + defaultVal: true, + }), + }; +}; From 2ce7f9047df411df6ea10cf7533c302ea4f1e5da Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 11:10:59 +0530 Subject: [PATCH 12/26] Set the 'field.schema.top' before validation (if it is not already set). --- web/pgadmin/static/js/SchemaView/SchemaState/common.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/common.js b/web/pgadmin/static/js/SchemaView/SchemaState/common.js index 0dfc9b66271..6e8b74f4801 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaState/common.js +++ b/web/pgadmin/static/js/SchemaView/SchemaState/common.js @@ -290,9 +290,10 @@ export function validateSchema( if(schema.idAttribute === field.id) { continue; } - // If the field is has nested schema, then validate the child schema. if(field.schema && (field.schema instanceof BaseUISchema)) { + if (!field.schema.top) field.schema.top = schema; + // A collection is an array. if(field.type === 'collection') { if (validateCollectionSchema(field, sessData, accessPath, setError)) From 971c8950fa87b09da76ec50105dd7fbddd8514ee Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 14:20:39 +0530 Subject: [PATCH 13/26] Fixed the styling for the grid header --- web/pgadmin/static/js/SchemaView/DataGridView/header.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx index 80c305b1031..30bd3bd1c3c 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/header.jsx @@ -81,12 +81,12 @@ export function DataGridHeader({tableEleRef}) { return ( - {label && {label}} + {label && {label}} - + { canAdd && Date: Tue, 3 Sep 2024 15:43:37 +0530 Subject: [PATCH 14/26] Don't listen dependency changes for a grid 'row' --- web/pgadmin/static/js/SchemaView/DataGridView/row.jsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx index 7c6ce2ac213..e688dc7b059 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -17,7 +17,6 @@ import { import { SchemaStateContext } from '../SchemaState'; import { useFieldOptions } from '../hooks'; -import { listenDepChanges } from '../utils'; import { DataGridContext, DataGridRowContext } from './context'; @@ -36,8 +35,6 @@ export function DataGridRow({rowId, isResizing}) { const rowRef = useRef(null); const row = table.getRowModel().rows[rowId]; - listenDepChanges(rowAccessPath, field, true, schemaState); - /* * Memoize the row to avoid unnecessary re-render. If table data changes, * then react-table re-renders the complete tables. From ee522f66ac62db34848e1d53e63fb297955cb141 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 15:47:36 +0530 Subject: [PATCH 15/26] Removed ununsed variable --- web/pgadmin/static/js/SchemaView/DataGridView/row.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx index e688dc7b059..3829bee5408 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -25,7 +25,7 @@ export function DataGridRow({rowId, isResizing}) { const [key, setKey] = useState(0); const schemaState = useContext(SchemaStateContext); - const { accessPath, field, options, table, features } = useContext( + const { accessPath, options, table, features } = useContext( DataGridContext ); From f8401989826c31964541b1874b9c6a1bd8d7c88b Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 18:01:05 +0530 Subject: [PATCH 16/26] Don't rerender the cell on every rowValue change. --- .../server_groups/servers/static/js/privilege.ui.js | 11 +++++++---- .../static/js/SchemaView/DataGridView/mappedCell.jsx | 10 +--------- web/pgadmin/static/js/SchemaView/DataGridView/row.jsx | 11 +++++------ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/web/pgadmin/browser/server_groups/servers/static/js/privilege.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/privilege.ui.js index faf3141f9d1..b8c17bc269d 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/privilege.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/privilege.ui.js @@ -28,7 +28,7 @@ export default class PrivilegeRoleSchema extends BaseUISchema { super({ grantee: undefined, grantor: nodeInfo?.server?.user?.name, - privileges: undefined, + privileges: [], }); this.granteeOptions = granteeOptions; this.grantorOptions = grantorOptions; @@ -56,9 +56,12 @@ export default class PrivilegeRoleSchema extends BaseUISchema { { id: 'privileges', label: gettext('Privileges'), type: 'text', group: null, - cell: ()=>({cell: 'privilege', controlProps: { - supportedPrivs: this.supportedPrivs, - }}), + cell: () => ({ + cell: 'privilege', + controlProps: { + supportedPrivs: this.supportedPrivs, + } + }), disabled : function(state) { return !( obj.nodeInfo && diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx index 05f002cab8a..558a06bab6d 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx @@ -29,18 +29,10 @@ export function getMappedCell({field}) { const schemaState = useContext(SchemaStateContext); const { dataDispatch } = useContext(DataGridContext); const { rowAccessPath, row } = useContext(DataGridRowContext); - - // When cell is dynamic, it should be rerendered on change of row value. - const refreshOnRowChange = ( - (field.id && _.isFunction(field.cell)) ? [key, setKey] : [] - ); - const colAccessPath = schemaState.accessPath(rowAccessPath, field.id); + let colOptions = useFieldOptions(colAccessPath, schemaState, key, setKey); let value = useFieldValue(colAccessPath, schemaState, key, setKey); - let rowValue = useFieldValue( - rowAccessPath, schemaState, ...refreshOnRowChange - ); listenDepChanges(colAccessPath, field, true, schemaState); diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx index 3829bee5408..9f495c1694f 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -30,7 +30,7 @@ export function DataGridRow({rowId, isResizing}) { ); const rowAccessPath = schemaState.accessPath(accessPath, rowId); - const rowOptions = useFieldOptions(rowAccessPath, schemaState, key, setKey); + const rowOptions = useFieldOptions(rowAccessPath, schemaState); const rowRef = useRef(null); const row = table.getRowModel().rows[rowId]; @@ -41,7 +41,6 @@ export function DataGridRow({rowId, isResizing}) { * * We can avoid re-render by if row data has not changed. */ - let depsMap = [JSON.stringify(row?.original)]; let classList = []; let attributes = {}; let expandedRowContents = []; @@ -51,9 +50,9 @@ export function DataGridRow({rowId, isResizing}) { rowOptions, tableOptions: options }); - depsMap = depsMap.concat([ - row?.getIsExpanded(), key, isResizing, expandedRowContents.length - ]); + let depsMap = [ + rowId, row?.getIsExpanded(), key, isResizing, expandedRowContents.length + ]; return useMemo(() => ( !row ? <> : @@ -91,5 +90,5 @@ export function DataGridRow({rowId, isResizing}) { : <> } - ), depsMap); + ), [...depsMap]); } From c909e0a53c48f04e6a9670438388a2d8432957f5 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 18:23:03 +0530 Subject: [PATCH 17/26] Fixed the linter issue --- web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx | 2 ++ web/pgadmin/static/js/SchemaView/DataGridView/row.jsx | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx index 558a06bab6d..92e25c3a6d6 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx @@ -33,6 +33,7 @@ export function getMappedCell({field}) { let colOptions = useFieldOptions(colAccessPath, schemaState, key, setKey); let value = useFieldValue(colAccessPath, schemaState, key, setKey); + let rowValue = useFieldValue(rowAccessPath, schemaState); listenDepChanges(colAccessPath, field, true, schemaState); @@ -43,6 +44,7 @@ export function getMappedCell({field}) { colOptions = { disabled: true, readonly: true }; } else { colOptions['readonly'] = !colOptions['editable']; + rowValue = value; } let cellProps = {}; diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx index 9f495c1694f..d5e8816c752 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -22,7 +22,6 @@ import { DataGridContext, DataGridRowContext } from './context'; export function DataGridRow({rowId, isResizing}) { - const [key, setKey] = useState(0); const schemaState = useContext(SchemaStateContext); const { accessPath, options, table, features } = useContext( @@ -51,7 +50,7 @@ export function DataGridRow({rowId, isResizing}) { }); let depsMap = [ - rowId, row?.getIsExpanded(), key, isResizing, expandedRowContents.length + rowId, row?.getIsExpanded(), isResizing, expandedRowContents.length ]; return useMemo(() => ( From 490b9e5ee7d122f5bc2179135e49357f5a64ff53 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Tue, 3 Sep 2024 18:26:38 +0530 Subject: [PATCH 18/26] Removed unused import (linter issue) --- web/pgadmin/static/js/SchemaView/DataGridView/row.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx index d5e8816c752..a95ca912f90 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -7,7 +7,7 @@ // ////////////////////////////////////////////////////////////// -import React, { useContext, useMemo, useRef, useState } from 'react'; +import React, { useContext, useMemo, useRef } from 'react'; import { flexRender } from '@tanstack/react-table'; From d5f56c9db62d34ac43d1ad14c82ea15c2fecf165 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Wed, 4 Sep 2024 12:26:58 +0530 Subject: [PATCH 19/26] No need to pass 'sessData' to 'FormView' as it is not using it. --- web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index 1cfae00ac23..2d443945173 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -49,7 +49,7 @@ export default function SchemaDialogView({ const onDataChange = props.onDataChange; // Schema data state manager - const {schemaState, dataDispatch, sessData, reset} = useSchemaState({ + const {schemaState, dataDispatch, reset} = useSchemaState({ schema: schema, getInitData: getInitData, immutableData: {}, viewHelperProps: viewHelperProps, onDataChange: onDataChange, loadingText, @@ -190,7 +190,7 @@ export default function SchemaDialogView({ - Date: Wed, 4 Sep 2024 15:59:04 +0530 Subject: [PATCH 20/26] Resubscribe to the value, options, and error changes on schemaState object change. --- web/pgadmin/static/js/SchemaView/FormView.jsx | 4 +++- .../static/js/SchemaView/SchemaPropertiesView.jsx | 14 +++++++------- .../static/js/SchemaView/SchemaState/store.js | 4 +++- .../static/js/SchemaView/hooks/useFieldError.js | 2 +- .../static/js/SchemaView/hooks/useFieldOptions.js | 2 +- .../static/js/SchemaView/hooks/useFieldSchema.js | 2 +- .../static/js/SchemaView/hooks/useFieldValue.js | 2 +- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index 2ac52569878..f87ad906969 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -249,7 +249,9 @@ export default function FormView({ finalGroups.map((group, idx) => { group.controls.map( - (item, idx) => + (item, idx) => ) } ) diff --git a/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx b/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx index 9dd26df8636..23614c3753e 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaPropertiesView.jsx @@ -35,7 +35,6 @@ import { createFieldControls } from './utils'; export default function SchemaPropertiesView({ getInitData, viewHelperProps, schema={}, updatedData, ...props }) { - const pgAdmin = usePgAdmin(); const Notifier = pgAdmin.Browser.notifier; @@ -59,8 +58,8 @@ export default function SchemaPropertiesView({ if (!finalTabs) return <>; - return ( - + return useMemo( + () => @@ -91,9 +90,9 @@ export default function SchemaPropertiesView({ { group.controls.map( - (item, idx) => - + (item, idx) => ) } @@ -104,7 +103,8 @@ export default function SchemaPropertiesView({ - + , + [schema._id] ); } diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/store.js b/web/pgadmin/static/js/SchemaView/SchemaState/store.js index 67eb0e0eb6e..991d9593c8e 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaState/store.js +++ b/web/pgadmin/static/js/SchemaView/SchemaState/store.js @@ -64,7 +64,9 @@ export const createStore = (initialState) => { pathListeners.add(data); - return () => pathListeners.delete(data); + return () => { + return pathListeners.delete(data); + }; }; return { diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js index 9c2540ca390..07542725098 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldError.js @@ -25,7 +25,7 @@ export const useFieldError = ( }; return schemaState.subscribe(['errors'], checkPathError, 'states'); - }, [key]); + }, [key, schemaState?._id]); const errors = schemaState?.errors || {}; const error = errors.name === path ? errors.message : null; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js index f21d17e3c0f..5763edc2426 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js @@ -19,7 +19,7 @@ export const useFieldOptions = ( return schemaState.subscribe( path, () => setRefreshKey?.({id: Date.now()}), 'options' ); - }, [key]); + }, [key, schemaState?._id]); return schemaState?.options(path) || {visible: true}; }; diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js index 5b798b0c946..0cbd2bd94c4 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js @@ -39,7 +39,7 @@ export const useFieldSchema = ( return () => { unsubscribers.forEach(unsubscribe => unsubscribe()); }; - }); + }, [key, schemaState?._id]); if (!field) return { visible: true }; if (field.id) return schemaState?.options(accessPath); diff --git a/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js b/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js index ffbe04e12e6..0e92ae9e42a 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js @@ -19,7 +19,7 @@ export const useFieldValue = ( return schemaState.subscribe( path, () => setRefreshKey({id: Date.now()}), 'value' ); - }, [key]); + }, [key, schemaState?._id]); return schemaState?.value(path); }; From 21972d8945fdf1235f1d095df9c83372696cf1e1 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Thu, 5 Sep 2024 09:39:28 +0530 Subject: [PATCH 21/26] Use a table level schema for the PgTable to use showing the expanded row details. It fixes the dashboard issue, where activity table was showing same query for all the rows. [#7895] --- .../js/SchemaView/SchemaState/SchemaState.js | 4 +- .../js/SchemaView/hooks/useSchemaState.js | 9 +- web/pgadmin/static/js/SchemaView/index.jsx | 6 +- web/pgadmin/static/js/components/PgTable.jsx | 148 ++++++++++++------ 4 files changed, 113 insertions(+), 54 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js index 152560597b6..9082840c76d 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js +++ b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js @@ -26,7 +26,7 @@ import { createStore } from './store'; export const LOADING_STATE = { - INIT: 'initializing', + INIT: 'initialising', LOADING: 'loading', LOADED: 'loaded', ERROR: 'Error' @@ -228,7 +228,7 @@ export class SchemaState extends DepListener { changes(includeSkipChange=false) { const state = this; - const sessData = this.data; + const sessData = state.data; const schema = state.schema; // Check if anything changed. diff --git a/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js b/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js index 0e9fda386ce..d22c9d3b1c6 100644 --- a/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js +++ b/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js @@ -22,6 +22,14 @@ export const useSchemaState = ({ schema, getInitData, immutableData, onDataChange, viewHelperProps, loadingText, }) => { + + if (!schema) + return { + schemaState: null, + dataDispatch: null, + reset: null, + }; + let state = schema.state; if (!state) { @@ -130,7 +138,6 @@ export const useSchemaState = ({ return { schemaState: state, dataDispatch: sessDispatchWithListener, - sessData, reset: resetData, }; }; diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 985dd88765f..fa91803bf09 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -7,6 +7,7 @@ // ////////////////////////////////////////////////////////////// +import BaseUISchema from './base_schema.ui'; import DataGridView from './DataGridView'; import FieldSetView from './FieldSetView'; import FormView from './FormView'; @@ -14,13 +15,13 @@ import InlineView from './InlineView'; import SchemaDialogView from './SchemaDialogView'; import SchemaPropertiesView from './SchemaPropertiesView'; import SchemaView from './SchemaView'; -import BaseUISchema from './base_schema.ui'; import { useSchemaState, useFieldState } from './hooks'; import { generateTimeBasedRandomNumberString, isValueEqual, isObjectEqual, - getForQueryParams + getForQueryParams, + prepareData, } from './common'; import { SCHEMA_STATE_ACTIONS, @@ -45,6 +46,7 @@ export { generateTimeBasedRandomNumberString, isValueEqual, isObjectEqual, + prepareData, useFieldState, useSchemaState, }; diff --git a/web/pgadmin/static/js/components/PgTable.jsx b/web/pgadmin/static/js/components/PgTable.jsx index 4fc1135acfa..b28bb66ba29 100644 --- a/web/pgadmin/static/js/components/PgTable.jsx +++ b/web/pgadmin/static/js/components/PgTable.jsx @@ -8,7 +8,10 @@ ////////////////////////////////////////////////////////////// import React, { useMemo, useRef } from 'react'; +import _ from 'lodash'; +import Box from '@mui/material/Box'; +import { styled } from '@mui/material/styles'; import { useReactTable, getCoreRowModel, @@ -24,29 +27,29 @@ import { keepPreviousData, } from '@tanstack/react-query'; import { useVirtualizer } from '@tanstack/react-virtual'; -import { styled } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import { InputText } from './FormComponents'; -import _ from 'lodash'; + +import { + BaseUISchema, FormView, SchemaStateContext, useSchemaState, prepareData, +} from 'sources/SchemaView'; import gettext from 'sources/gettext'; -import SchemaView from '../SchemaView'; + import EmptyPanelMessage from './EmptyPanelMessage'; +import { InputText } from './FormComponents'; import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent, getCheckboxCell, getCheckboxHeaderCell } from './PgReactTableStyled'; -import { Box } from '@mui/material'; + const ROW_HEIGHT = 30; -function TableRow({ index, style, schema, row, measureElement}) { - const [expandComplete, setExpandComplete] = React.useState(false); + +function TableRow({index, style, schema, row, measureElement}) { const rowRef = React.useRef(); React.useEffect(() => { if (rowRef.current) { - if (!expandComplete && rowRef.current.style.height == `${ROW_HEIGHT}px`) { - return; - } + if (rowRef.current.style.height == `${ROW_HEIGHT}px`) return; measureElement(rowRef.current); } - }, [row.getIsExpanded(), expandComplete]); + }, [row.getIsExpanded()]); return ( @@ -62,12 +65,10 @@ function TableRow({ index, style, schema, row, measureElement}) { })} - Promise.resolve(row.original)} - viewHelperProps={{ mode: 'properties' }} + { setExpandComplete(true); }} + viewHelperProps={{ mode: 'properties' }} /> @@ -81,7 +82,43 @@ TableRow.propTypes = { measureElement: PropTypes.func, }; -export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableProps, searchVal, loadNextPage, ...props }) { + +class TableUISchema extends BaseUISchema { + constructor(rowSchema) { + super(); + this.rowSchema = rowSchema; + } + + get baseFields() { + return [{ + id: 'data', type: 'collection', mode: ['properties'], + schema: this.rowSchema, + }]; + } +} + +const getTableSchema = (schema) => {; + if (!schema) return null; + if (!schema.top) schema.top = new TableUISchema(schema); + return schema.top; +}; + +export function Table({ + columns, data, hasSelectRow, schema, sortOptions, tableProps, searchVal, + loadNextPage, ...props +}) { + const { schemaState } = useSchemaState({ + schema: getTableSchema(schema), + getInitData: null, + viewHelperProps: {mode: 'properties'}, + }); + + // We don't care about validation in static table, hence - initialising the + // data directly. + if (data.length && schemaState) { + schemaState.initData = schemaState.data = prepareData({'data': data}); + } + const defaultColumn = React.useMemo( () => ({ size: 150, @@ -103,11 +140,15 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP enableResizing: false, maxSize: 35, }] : []).concat( - columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility).map((c)=>({ + columns.filter( + (c) => _.isUndefined(c.enableVisibility) ? true : c.enableVisibility + ).map((c) => ({ ...c, // if data is null then global search doesn't work // Use accessorFn to return empty string if data is null. - accessorFn: c.accessorFn ?? (c.accessorKey ? (row)=>row[c.accessorKey] ?? '' : undefined), + accessorFn: c.accessorFn ?? ( + c.accessorKey ? (row) => row[c.accessorKey] ?? '' : undefined + ), })) ), [hasSelectRow, columns]); @@ -118,24 +159,24 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP let totalFetched = 0; let totalDBRowCount = 0; - //Infinite scrolling - const { _data, fetchNextPage, isFetching } = - useInfiniteQuery({ - queryKey: ['logs'], - queryFn: async () => { - const fetchedData = loadNextPage ? await loadNextPage() : []; - return fetchedData; - }, - initialPageParam: 0, - getNextPageParam: (_lastGroup, groups) => groups.length, - refetchOnWindowFocus: false, - placeholderData: keepPreviousData, - }); + // Infinite scrolling + const { _data, fetchNextPage, isFetching } = useInfiniteQuery({ + queryKey: ['logs'], + queryFn: async () => { + const fetchedData = loadNextPage ? await loadNextPage() : []; + return fetchedData; + }, + initialPageParam: 0, + getNextPageParam: (_lastGroup, groups) => groups.length, + refetchOnWindowFocus: false, + placeholderData: keepPreviousData, + }); flatData = _data || []; totalFetched = flatData.length; - //called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table + // Called on scroll and possibly on mount to fetch more data as the user + // scrolls and reaches bottom of table. fetchMoreOnBottomReached = React.useCallback( (containerRefElement = HTMLDivElement | null) => { if (containerRefElement) { @@ -194,22 +235,31 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP }); return ( - - - {rows.length == 0 ? - : - - {virtualizer.getVirtualItems().map((virtualRow) => { - const row = rows[virtualRow.index]; - return ; - })} - } - + + + + {rows.length == 0 ? : + + {virtualizer.getVirtualItems().map((virtualRow) => { + const row = rows[virtualRow.index]; + return ; + })} + } + + ); } Table.propTypes = { From 490bb2400744d78937a7b6f5928de1469866e0f2 Mon Sep 17 00:00:00 2001 From: Anil Sahoo Date: Mon, 2 Sep 2024 15:06:11 +0530 Subject: [PATCH 22/26] Fix the query tool restore connection issue on the server disconnection from the left side object explorer. #6502 --- .../browser/server_groups/servers/__init__.py | 6 +- web/pgadmin/tools/sqleditor/__init__.py | 8 ++ .../js/components/QueryToolComponent.jsx | 14 ++- .../js/components/QueryToolConstants.js | 1 + .../js/components/sections/MainToolBar.jsx | 4 +- .../static/js/components/sections/Query.jsx | 4 +- .../js/components/sections/ResultSet.jsx | 98 +++++++++++++++---- .../sqleditor/utils/start_running_query.py | 16 +++ 8 files changed, 122 insertions(+), 29 deletions(-) diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index c1c956522a1..30030840276 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -1377,7 +1377,7 @@ def connect_status(self, gid, sid): } ) - def connect(self, gid, sid): + def connect(self, gid, sid, is_qt=False): """ Connect the Server and return the connection object. Verification Process before Connection: @@ -1443,7 +1443,9 @@ def connect(self, gid, sid): # Connect the Server manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) - if not manager.connection().connected(): + # Update the manager with the server details if not connected and + # the API call is not made from SQL Editor or View/Edit Data tool + if not manager.connection().connected() and not is_qt: manager.update(server) conn = manager.connection() diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index a142e2fe197..b571a34d121 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -802,6 +802,14 @@ def start_view_data(trans_id): # Connect to the Server if not connected. if not default_conn.connected(): + view = SchemaDiffRegistry.get_node_view('server') + response = view.connect(trans_obj.sgid, + trans_obj.sid, True) + if response.status_code == 428: + return response + else: + conn = manager.connection(did=trans_obj.did) + status, msg = default_conn.connect() if not status: return make_json_response( diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx index 321b6e18d78..3f6199278bb 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolComponent.jsx @@ -333,7 +333,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN } }; - const initializeQueryTool = (password)=>{ + const initializeQueryTool = (password, explainObject=null, macroSQL='', executeCursor=false)=>{ let selectedConn = _.find(qtState.connection_list, (c)=>c.is_selected); let baseUrl = ''; if(qtState.params.is_query_tool) { @@ -364,9 +364,9 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN obtaining_conn: false, }); - if(!qtState.params.is_query_tool) { - eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION); - } + eventBus.current.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, explainObject, macroSQL, executeCursor); + let msg = `${selectedConn['server_name']}/${selectedConn['database_name']} - Database connected`; + pgAdmin.Browser.notifier.success(_.escape(msg)); }).catch((error)=>{ if(error.response?.request?.responseText?.search('Ticket expired') !== -1) { Kerberos.fetch_ticket() @@ -415,6 +415,8 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN getSQLScript(); initializeQueryTool(); + eventBus.current.registerListener(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, initializeQueryTool); + eventBus.current.registerListener(QUERY_TOOL_EVENTS.FOCUS_PANEL, (qtPanelId)=>{ docker.current.focus(qtPanelId); }); @@ -684,6 +686,10 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN params: { ...prev.params, trans_id: respData.data.trans_id, + server_name: connectionData.server_name, + database_name: connectionData.database_name, + dbname: connectionData.database_name, + user: connectionData.user, sid: connectionData.sid, did: connectionData.did, title: connectionData.title, diff --git a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js index a1c3825679e..70e909173d7 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js +++ b/web/pgadmin/tools/sqleditor/static/js/components/QueryToolConstants.js @@ -57,6 +57,7 @@ export const QUERY_TOOL_EVENTS = { HANDLE_API_ERROR: 'HANDLE_API_ERROR', SET_FILTER_INFO: 'SET_FILTER_INFO', FETCH_MORE_ROWS: 'FETCH_MORE_ROWS', + REINIT_QT_CONNECTION:'REINIT_QT_CONNECTION', EDITOR_LAST_FOCUS: 'EDITOR_LAST_FOCUS', EDITOR_FIND_REPLACE: 'EDITOR_FIND_REPLACE', diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx index ac15e079c64..47f101cd9f1 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/MainToolBar.jsx @@ -276,10 +276,10 @@ export function MainToolBar({containerRef, onFilterClick, onManageMacros, onAddT }, [queryToolConnCtx.connectionStatus]); const onCommitClick=()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, true); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'COMMIT;', null, '', true); }; const onRollbackClick=()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, true); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, 'ROLLBACK;', null, '', true); }; const executeMacro = (m)=>{ eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_EXECUTION, null, m.sql); diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx index 178c011ad99..e543db30462 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/Query.jsx @@ -152,10 +152,10 @@ export default function Query({onTextSelect}) { query = query || editor.current?.getValue() || ''; } if(query) { - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, external, null, executeCursor); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, macroSQL, external, null, executeCursor); } } else { - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, null, null, ''); } }; diff --git a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx index d6ba03da0cb..efa4327ddce 100644 --- a/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx +++ b/web/pgadmin/tools/sqleditor/static/js/components/sections/ResultSet.jsx @@ -30,6 +30,7 @@ import EmptyPanelMessage from '../../../../../../static/js/components/EmptyPanel import { GraphVisualiser } from './GraphVisualiser'; import { usePgAdmin } from '../../../../../../static/js/BrowserComponent'; import pgAdmin from 'sources/pgadmin'; +import ConnectServerContent from '../../../../../../static/js/Dialogs/ConnectServerContent'; const StyledBox = styled(Box)(({theme}) => ({ display: 'flex', @@ -39,7 +40,7 @@ const StyledBox = styled(Box)(({theme}) => ({ })); export class ResultSetUtils { - constructor(api, transId, isQueryTool=true) { + constructor(api, queryToolCtx, transId, isQueryTool=true) { this.api = api; this.transId = transId; this.startTime = new Date(); @@ -48,6 +49,7 @@ export class ResultSetUtils { this.clientPKLastIndex = 0; this.historyQuerySource = null; this.hasQueryCommitted = false; + this.queryToolCtx = queryToolCtx; } static generateURLReconnectionFlag(baseUrl, transId, shouldReconnect) { @@ -187,8 +189,49 @@ export class ResultSetUtils { ); } } + connectServerModal (modalData, connectCallback, cancelCallback) { + this.queryToolCtx.modal.showModal(gettext('Connect to server'), (closeModal)=>{ + return ( + { + cancelCallback?.(); + closeModal(); + }} + data={modalData} + onOK={(formData)=>{ + connectCallback(Object.fromEntries(formData)); + closeModal(); + }} + /> + ); + }, { + onClose: cancelCallback, + }); + }; + async connectServer (sid, user, formData, connectCallback) { + try { + let {data: respData} = await this.api({ + method: 'POST', + url: url_for('sqleditor.connect_server', { + 'sid': sid, + ...(user ? { + 'usr': user, + }:{}), + }), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + data: formData + }); + connectCallback?.(respData.data); + } catch (error) { + this.connectServerModal(error.response?.data?.result, async (data)=>{ + this.connectServer(sid, user, data, connectCallback); + }, ()=>{ + /*This is intentional (SonarQube)*/ + }); + } + }; - async startExecution(query, explainObject, onIncorrectSQL, flags={ + async startExecution(query, explainObject, macroSQL, onIncorrectSQL, flags={ isQueryTool: true, external: false, reconnect: false, executeCursor: false }) { let startTime = new Date(); @@ -244,16 +287,26 @@ export class ResultSetUtils { } } } catch(e) { - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_END); - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, - e, - { - connectionLostCallback: ()=>{ - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, flags.external, true, flags.executeCursor); - }, - checkTransaction: true, - } - ); + if(e?.response?.status == 428){ + this.connectServerModal(e.response?.data?.result, async (passwordData)=>{ + await this.connectServer(this.queryToolCtx.params.sid, this.queryToolCtx.params.user, passwordData, async ()=>{ + await this.eventBus.fireEvent(QUERY_TOOL_EVENTS.REINIT_QT_CONNECTION, '', explainObject, macroSQL, flags.executeCursor); + }); + }, ()=>{ + /*This is intentional (SonarQube)*/ + }); + } else { + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_END); + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, + e, + { + connectionLostCallback: ()=>{ + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, query, explainObject, '', flags.external, true, flags.executeCursor); + }, + checkTransaction: true, + } + ); + } } return false; } @@ -304,7 +357,7 @@ export class ResultSetUtils { }); this.eventBus.fireEvent(QUERY_TOOL_EVENTS.HANDLE_API_ERROR, error, { connectionLostCallback: ()=>{ - this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, explainObject, flags.external, true, flags.executeCursor); + this.eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, this.query, explainObject, '', flags.external, true, flags.executeCursor); }, checkTransaction: true, }); @@ -767,7 +820,7 @@ export function ResultSet() { const [columns, setColumns] = useState([]); const [isLoadingMore, setIsLoadingMore] = useState(false); const api = getApiInstance(); - const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool)); + const rsu = React.useRef(new ResultSetUtils(api, queryToolCtx, queryToolCtx.params.trans_id, queryToolCtx.params.is_query_tool)); const [dataChangeStore, dispatchDataChange] = React.useReducer(dataChangeReducer, {}); const [selectedRows, setSelectedRows] = useState(new Set()); const [selectedColumns, setSelectedColumns] = useState(new Set()); @@ -802,7 +855,7 @@ export function ResultSet() { eventBus.fireEvent(QUERY_TOOL_EVENTS.SELECTED_ROWS_COLS_CELL_CHANGED, selectedRows.size, selectedColumns.size, selectedRange.current, selectedCell.current?.length); }; - const executionStartCallback = async (query, explainObject, external=false, reconnect=false, executeCursor=false)=>{ + const executionStartCallback = async (query, explainObject, macroSQL, external=false, reconnect=false, executeCursor=false)=>{ const yesCallback = async ()=>{ /* Reset */ eventBus.fireEvent(QUERY_TOOL_EVENTS.HIGHLIGHT_ERROR, null); @@ -813,7 +866,7 @@ export function ResultSet() { setLoaderText(gettext('Waiting for the query to complete...')); setDataOutputQuery(query); return await rsu.current.startExecution( - query, explainObject, + query, explainObject, macroSQL, ()=>{ setColumns([]); setRows([]); @@ -852,8 +905,15 @@ export function ResultSet() { }; const executeAndPoll = async ()=>{ - await yesCallback(); - pollCallback(); + await yesCallback().then((res)=>{ + if(res){ + pollCallback(); + } else { + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_END); + } + }).catch((err)=>{ + console.error(err); + }); }; if(isDataChanged()) { @@ -989,7 +1049,7 @@ export function ResultSet() { e, { connectionLostCallback: ()=>{ - eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, null, false, true); + eventBus.fireEvent(QUERY_TOOL_EVENTS.EXECUTION_START, rsu.current.query, null, '', false, true); }, checkTransaction: true, } diff --git a/web/pgadmin/tools/sqleditor/utils/start_running_query.py b/web/pgadmin/tools/sqleditor/utils/start_running_query.py index 0096b1e9726..595c16f172c 100644 --- a/web/pgadmin/tools/sqleditor/utils/start_running_query.py +++ b/web/pgadmin/tools/sqleditor/utils/start_running_query.py @@ -28,6 +28,7 @@ from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\ CryptKeyMissing from pgadmin.utils.constants import ERROR_MSG_TRANS_ID_NOT_FOUND +from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry class StartRunningQuery: @@ -81,6 +82,21 @@ def execute(self, sql, trans_id, http_session, connect=False): # Connect to the Server if not connected. if connect and not conn.connected(): + view = SchemaDiffRegistry.get_node_view('server') + response = view.connect(transaction_object.sgid, + transaction_object.sid, True) + if response.status_code == 428: + return response + else: + conn = manager.connection( + did=transaction_object.did, + conn_id=self.connection_id, + auto_reconnect=False, + use_binary_placeholder=True, + array_to_string=True, + **({"database": transaction_object.dbname} if hasattr( + transaction_object, 'dbname') else {})) + status, msg = conn.connect() if not status: self.logger.error(msg) From 6ec6cf59732aedc03cc9305d344b642d0b0d98a0 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Mon, 2 Sep 2024 15:09:34 +0530 Subject: [PATCH 23/26] Update release notes. --- docs/en_US/release_notes_8_12.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en_US/release_notes_8_12.rst b/docs/en_US/release_notes_8_12.rst index 3fc03d70dde..9d9cd6337b7 100644 --- a/docs/en_US/release_notes_8_12.rst +++ b/docs/en_US/release_notes_8_12.rst @@ -30,5 +30,6 @@ Housekeeping Bug fixes ********* + | `Issue #6502 `_ - Fix the query tool restore connection issue on the server disconnection from the left side object explorer. | `Issue #7076 `_ - Revamp the current password saving implementation to a keyring and reduce repeated OS user password prompts. - | `Issue #7571 `_ - Fixed an issue where users could not use pgAdmin if they did not have access to the management database. \ No newline at end of file + | `Issue #7571 `_ - Fixed an issue where users could not use pgAdmin if they did not have access to the management database. From 77a7211a2c3981f8da46168316e875db37d3d2c0 Mon Sep 17 00:00:00 2001 From: Yogesh Mahajan Date: Mon, 2 Sep 2024 19:32:25 +0530 Subject: [PATCH 24/26] Fix issues found while testing keyring changes. #7076 --- web/config.py | 9 +++ web/pgadmin/browser/__init__.py | 64 ++++++++++--------- .../browser/server_groups/servers/utils.py | 7 +- web/pgadmin/utils/master_password.py | 55 +++++----------- 4 files changed, 64 insertions(+), 71 deletions(-) diff --git a/web/config.py b/web/config.py index b4c25c0798c..53145d15993 100644 --- a/web/config.py +++ b/web/config.py @@ -583,6 +583,15 @@ # Applicable for desktop mode only ########################################################################## MASTER_PASSWORD_REQUIRED = True +########################################################################## + +########################################################################## +# Allow to save master password which is used to encrypt/decrypt saved +# passwords in the os level secret like Keychain, password store etc. +# Disabling this will require user to enter master password +# if MASTER_PASSWORD_REQUIRED is set to True. Note: this is applicable only +# in case of Desktop mode. +########################################################################## USE_OS_SECRET_STORAGE = True ########################################################################## diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 4cd8d1271be..33f9478bc83 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -19,7 +19,7 @@ from socket import error as SOCKETErrorException import keyring -from keyring.errors import KeyringLocked +from keyring.errors import KeyringLocked, NoKeyringError from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \ KEY_RING_USER_NAME,MessageType @@ -632,7 +632,7 @@ def get_nodes(): def form_master_password_response(existing=True, present=False, errmsg=None, - keyring_name='', master_password_hook=''): + keyring_name='', master_password_hook=False): return make_json_response(data={ 'present': present, 'reset': existing, @@ -680,7 +680,7 @@ def reset_master_password(): # Set masterpass_check if MASTER_PASSWORD_HOOK is set which provides # encryption key - if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK: + if config.SERVER_MODE and config.MASTER_PASSWORD_HOOK: set_masterpass_check_text(crypt_key) return make_json_response(data=status) @@ -705,6 +705,7 @@ def set_master_password(): keyring_name = '' errmsg = '' + master_password_hook = False if not config.SERVER_MODE: if config.USE_OS_SECRET_STORAGE: try: @@ -715,7 +716,8 @@ def set_master_password(): if not master_key: # Generate new one and migration required master_key = secrets.token_urlsafe(12) - + keyring.delete_password(KEY_RING_SERVICE_NAME, + 'entry_to_check_keychain_access') # migrate existing server passwords from pgadmin.browser.server_groups.servers.utils \ import migrate_saved_passwords @@ -751,40 +753,39 @@ def set_master_password(): errmsg=errmsg, keyring_name=keyring_name) else: - current_app.logger.warning( - ' Master key was already present in the keyring,' - ' hence not doing any migration') + current_app.logger.debug( + 'Master key was already present in the keyring,' + 'hence not doing any migration') # Key is already generated and set, no migration required # set crypt key set_crypt_key(master_key) return form_master_password_response( present=True) - except KeyringLocked as e: - current_app.logger.warning( - 'Failed to set because Access Denied.' - ' Error: {0}'.format(e)) - config.USE_OS_SECRET_STORAGE = False except Exception as e: - current_app.logger.warning( - 'Failed to set encryption key using OS password manager' - ', fallback to master password. Error: {0}'.format(e)) - # Also if masterpass_check is none it means previously - # passwords were migrated using keyring crypt key. - # Reset all passwords because we are going to master password - # again and while setting master password, all server - # passwords are decrypted using old key before re-encryption - if current_user.masterpass_check is None: + error = 'Failed to get/set encryption key using OS password ' \ + 'manager because of exception.' \ + ' Error: {0}'.format(e) + current_app.logger.warning(error) + # Disable local os storage if any exception other than + # access denied + if not isinstance(e, KeyringLocked): + config.USE_OS_SECRET_STORAGE = False + # delete key if exception other than no keyring backend + # error + if not isinstance(e, NoKeyringError): + delete_local_storage_master_key() + + # Delete saved password encrypted with kecyhain master key from pgadmin.browser.server_groups.servers.utils \ import remove_saved_passwords, update_session_manager remove_saved_passwords(current_user.id) update_session_manager(current_user.id) - # Disable local os storage if any exception while creation - config.USE_OS_SECRET_STORAGE = False - delete_local_storage_master_key() - else: - # if os secret storage disabled now, but was used once then - # remove all the saved passwords - delete_local_storage_master_key() + + return form_master_password_response( + existing=False, + present=True, + errmsg=errmsg, + keyring_name=keyring_name) else: # If the master password is required and the master password hook # is specified then try to retrieve the encryption key and update data. @@ -817,11 +818,16 @@ def set_master_password(): if current_user.masterpass_check is not None and \ data.get('submit_password', False) and \ not validate_master_password(data.get('password')): + + if config.SERVER_MODE and config.MASTER_PASSWORD_HOOK: + master_password_hook = True + else: + errmsg = gettext("Incorrect master password") return form_master_password_response( existing=True, present=False, errmsg=errmsg, - master_password_hook=config.MASTER_PASSWORD_HOOK, + master_password_hook=master_password_hook, keyring_name=keyring_name ) diff --git a/web/pgadmin/browser/server_groups/servers/utils.py b/web/pgadmin/browser/server_groups/servers/utils.py index 6511ea28685..44ae1b47d89 100644 --- a/web/pgadmin/browser/server_groups/servers/utils.py +++ b/web/pgadmin/browser/server_groups/servers/utils.py @@ -355,9 +355,9 @@ def migrate_saved_passwords(master_key, master_password): elif master_password: old_key = master_password else: - current_app.logger.warning( - 'Saved password were already migrated once. ' - 'Hence not migrating again. ' + current_app.logger.info( + 'Passwords saved with Master password were already' + ' migrated once. Hence not migrating again. ' 'May be the old master key was deleted.') else: old_key = current_user.password @@ -432,7 +432,6 @@ def remove_saved_passwords(user_id): """ This function will remove all the saved passwords for the server """ - try: db.session.query(Server) \ .filter(Server.user_id == user_id) \ diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py index 74685d0d34c..b25734cd347 100644 --- a/web/pgadmin/utils/master_password.py +++ b/web/pgadmin/utils/master_password.py @@ -44,21 +44,15 @@ def get_crypt_key(): def get_master_password_key_from_os_secret(): - master_key = None - try: - # Try to get master key is from local os storage - master_key = keyring.get_password( - KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME) - except KeyringLocked as e: - current_app.logger.warning( - 'Failed to retrieve master key because Access Denied.' - ' Error: {0}'.format(e)) - config.USE_OS_SECRET_STORAGE = False - except Exception as e: - current_app.logger.warning( - 'Failed to set encryption key using OS password manager' - ', fallback to master password. Error: {0}'.format(e)) - config.USE_OS_SECRET_STORAGE = False + # Try to get master key is from local os storage + master_key = keyring.get_password(KEY_RING_SERVICE_NAME, + KEY_RING_USER_NAME) + if not master_key: + # If master password does not exist, keychain does not ask for + # permission. This will forces to ask for permission + keyring.set_password(KEY_RING_SERVICE_NAME, + 'entry_to_check_keychain_access', + 'dummy_password') return master_key @@ -147,32 +141,17 @@ def delete_local_storage_master_key(): if master_key: keyring.delete_password(KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME) - from pgadmin.browser.server_groups.servers.utils \ - import remove_saved_passwords - remove_saved_passwords(current_user.id) - - from pgadmin.utils.driver import get_driver - driver = get_driver(config.PG_DEFAULT_DRIVER) - for server in Server.query.filter_by( - user_id=current_user.id).all(): - manager = driver.connection_manager(server.id) - manager.update(server) current_app.logger.warning( 'Deleted master key stored in OS password manager.') - except NoKeyringError as e: - current_app.logger.warning( - ' Failed to delete master key stored in OS password manager' - ' because Keyring backend not found. Error: {0}'.format(e)) - config.USE_OS_SECRET_STORAGE = False - except KeyringLocked as e: - current_app.logger.warning( - ' Failed to delete master key stored in OS password manager' - ' because of Access Denied. Error: {0}'.format(e)) - config.USE_OS_SECRET_STORAGE = False + except (NoKeyringError, KeyringLocked) as e: + error = 'Failed to delete master key stored in OS password ' \ + 'manager because Keyring backend not found or ' \ + 'access denied. Error: {0}'.format(e) + current_app.logger.warning(error) except Exception as e: - current_app.logger.warning( - 'Failed to delete master key stored in OS password manager.') - config.USE_OS_SECRET_STORAGE = False + error = 'Failed to delete master key stored in OS password ' \ + 'manager. Error: {0}'.format(e) + current_app.logger.warning(error) def process_masterpass_disabled(): From b06b466f3a4cdce5ee8c8e06cf732b5682ad60ce Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Wed, 4 Sep 2024 19:46:40 +0530 Subject: [PATCH 25/26] Add debug logs to observe the OpenID token response. --- web/pgadmin/authenticate/oauth2.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/pgadmin/authenticate/oauth2.py b/web/pgadmin/authenticate/oauth2.py index b7642bb40e9..e3d35d4499e 100644 --- a/web/pgadmin/authenticate/oauth2.py +++ b/web/pgadmin/authenticate/oauth2.py @@ -134,6 +134,7 @@ def validate(self, form): def login(self, form): profile = self.get_user_profile() + current_app.logger.warning(f"profile : {profile}") email_key = \ [value for value in self.email_keys if value in profile.keys()] email = profile[email_key[0]] if (len(email_key) > 0) else None @@ -146,8 +147,13 @@ def login(self, form): self.oauth2_current_client ]['OAUTH2_USERNAME_CLAIM'] if username_claim is not None: + id_token = session['oauth2_token'].get('userinfo', {}) if username_claim in profile: username = profile[username_claim] + current_app.logger.warning('Found username claim in profile') + elif username_claim in id_token: + username = id_token[username_claim] + current_app.logger.warning('Found username claim in id_token') else: error_msg = "The claim '%s' is required to login into " \ "pgAdmin. Please update your OAuth2 profile." % ( From 15d97de5322ed0af15e7744e4bad1f377babc7ad Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 9 Sep 2024 13:01:20 +0530 Subject: [PATCH 26/26] Focus on first focussable element in the FormView control --- .../features/expandabledFormView.jsx | 6 ++-- web/pgadmin/static/js/SchemaView/FormView.jsx | 32 +++++++++++++++++-- .../static/js/SchemaView/SchemaDialogView.jsx | 8 ++--- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx index 57a03af2315..2e954a816c6 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx @@ -13,7 +13,6 @@ import { getExpandedRowModel } from '@tanstack/react-table'; import { getEditCell } from 'sources/components/PgReactTableStyled'; import gettext from 'sources/gettext'; import FormView from 'sources/SchemaView/FormView'; -import { requestAnimationAndFocus } from 'sources/utils'; import { SchemaStateContext } from '../../SchemaState'; import { useFieldOptions } from '../../hooks'; @@ -81,9 +80,8 @@ export default class ExpandedFormView extends Feature { isNested={true} className='DataGridView-expandedForm' isDataGridForm={true} - firstEleRef={(ele) => { - requestAnimationAndFocus(ele); - }}/> + focusOnFirstInput={true} + /> ); } } diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index f87ad906969..fce33f112d3 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -59,7 +59,7 @@ const ErrorMessageBox = () => { export default function FormView({ accessPath, schema=null, isNested=false, dataDispatch, className, hasSQLTab, getSQLValue, isTabView=true, viewHelperProps, field, - showError=false, resetKey + showError=false, resetKey, focusOnFirstInput=false }) { const [key, setKey] = useState(0); const schemaState = useContext(SchemaStateContext); @@ -75,6 +75,35 @@ export default function FormView({ if (!schema) schema = field.schema; + // Set focus on the first focusable element. + useEffect(() => { + if (!focusOnFirstInput) return; + setTimeout(() => { + const formEle = formRef.current; + if (!formEle) return; + const activeTabElement = formEle.querySelector( + '[data-test="tabpanel"]:not([hidden])' + ); + if (!activeTabElement) return; + + // Find the first focusable input, which is either: + // * An editable Input element. + // * A select element, which is not disabled. + // * An href element. + // * Any element with 'tabindex', but - tabindex is not set to '-1'. + const firstFocussableElement = activeTabElement.querySelector([ + 'button:not([role="tab"])', + '[href]', + 'input:not(disabled)', + 'select:not(disabled)', + 'textarea', + '[tabindex]:not([tabindex="-1"]):not([data-test="tabpanel"])', + ].join(', ')); + + if (firstFocussableElement) firstFocussableElement.focus(); + }, 200); + }, [tabValue]); + useEffect(() => { // Refresh on message changes. return schemaState.subscribe( @@ -87,7 +116,6 @@ export default function FormView({ ); }, [key]); - useEffect(() => { if (!visible) return; diff --git a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx index 2d443945173..7a9da51ba9d 100644 --- a/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx +++ b/web/pgadmin/static/js/SchemaView/SchemaDialogView.jsx @@ -7,7 +7,7 @@ // ////////////////////////////////////////////////////////////// -import React, { useEffect, useMemo, useRef } from 'react'; +import React, { useEffect, useMemo } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import DoneIcon from '@mui/icons-material/Done'; @@ -61,7 +61,6 @@ export default function SchemaDialogView({ // First element to be set by the FormView to set the focus after loading // the data. - const firstEleRef = useRef(); const checkIsMounted = useIsMounted(); // Notifier object. @@ -73,7 +72,6 @@ export default function SchemaDialogView({ * Docker on load focusses itself, so our focus should execute later. */ let focusTimeout = setTimeout(()=>{ - firstEleRef.current?.focus(); }, 250); // Clear the focus timeout if unmounted. @@ -89,7 +87,6 @@ export default function SchemaDialogView({ const onResetClick = () => { const resetIt = () => { - firstEleRef.current?.focus(); reset(); return true; }; @@ -195,9 +192,10 @@ export default function SchemaDialogView({ schema={schema} accessPath={[]} dataDispatch={dataDispatch} hasSQLTab={props.hasSQL} getSQLValue={getSQLValue} - firstEleRef={firstEleRef} isTabView={isTabView} + isTabView={isTabView} className={props.formClassName} showError={true} resetKey={props.resetKey} + focusOnFirstInput={true} /> {showFooter &&