From e9af0c3226625c53fc5448827b8829d4eb485569 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 9 Sep 2024 14:27:31 +0530 Subject: [PATCH] Improved the extendability of the SchemaView and DataGridView. (#7876) Restructured these modules for ease of maintenance and apply the single responsibility principle (wherever 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, whether to rerender the control itself or the whole dialog/view. (single responsibility principle) - Introduced a new data store with a subscription facility. - Moving the field metadata (option) evaluation to a separate place for better management, and each option can be defined for a particular kind of field (for example - collection, row, cell, general, etc). - Allow to provide custom control for all kind of Schema field. * DataGridView - Same as SchemaView, split the DataGridView call into smaller, manageable chunks. (For example - grid, row, mappedCell, etc). - Use context based approach for providing the row and table data instead of passing them as parameters to every component separately. - Have a facility to extend this feature separately in future. (for example - selectable 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 customize 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'. --- web/.eslintrc.js | 2 +- web/package.json | 4 +- .../schemas/domains/static/js/domain.ui.js | 4 +- .../static/js/fts_configuration.ui.js | 6 +- .../static/js/exclusion_constraint.ui.js | 12 +- .../foreign_key/static/js/foreign_key.ui.js | 41 +- .../tables/indexes/static/js/index.ui.js | 30 +- .../schemas/tables/static/js/table.ui.js | 69 +- .../schemas/types/static/js/type.ui.js | 2 +- .../schemas/views/static/js/mview.ui.js | 6 +- .../schemas/views/static/js/view.ui.js | 8 +- .../static/js/subscription.ui.js | 10 +- .../servers/static/js/privilege.ui.js | 11 +- .../servers/static/js/server.ui.js | 2 +- .../servers/static/js/variable.ui.js | 2 +- .../static/js/server_group.ui.js | 3 +- .../misc/properties/ObjectNodeProperties.jsx | 3 +- .../js/components/PreferencesComponent.jsx | 125 ++-- .../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 | 88 +++ .../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 | 105 +++ .../static/js/SchemaView/DataGridView/row.jsx | 93 +++ .../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 | 161 +---- .../static/js/SchemaView/FormLoader.jsx | 30 + web/pgadmin/static/js/SchemaView/FormView.jsx | 596 +++++++---------- .../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 | 125 ++-- .../js/SchemaView/SchemaPropertiesView.jsx | 206 ++---- .../js/SchemaView/SchemaState/SchemaState.js | 330 +++++++++ .../{schemaUtils.js => SchemaState/common.js} | 58 +- .../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 | 80 +++ .../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 | 23 + .../js/SchemaView/hooks/useFieldError.js | 34 + .../js/SchemaView/hooks/useFieldOptions.js | 25 + .../js/SchemaView/hooks/useFieldSchema.js | 56 ++ .../js/SchemaView/hooks/useFieldValue.js | 25 + .../js/SchemaView/hooks/useSchemaState.js | 143 ++++ web/pgadmin/static/js/SchemaView/index.jsx | 29 +- .../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 | 31 +- .../js/components/PgReactTableStyled.jsx | 13 - web/pgadmin/static/js/components/PgTable.jsx | 148 +++-- .../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 +- web/regression/README.md | 2 +- .../xss_checks_roles_control_test.py | 2 +- web/regression/feature_utils/pgadmin_page.py | 28 +- .../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, 5429 insertions(+), 3058 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} (86%) 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/useFieldSchema.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/schemas/domains/static/js/domain.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/domains/static/js/domain.ui.js index c5db4e78b41..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.origData.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/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/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..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 @@ -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,20 @@ class ForeignKeyHeaderSchema extends BaseUISchema { } addDisabled(state) { - return !(state.local_column && (state.references || this.origData.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.origData.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, @@ -229,7 +238,10 @@ export default class ForeignKeySchema extends BaseUISchema { if(!obj.isNew(state)) { let origData = {}; if(obj.inTable && obj.top) { - origData = _.find(obj.top.origData['foreign_key'], (r)=>r.cid == state.cid); + origData = _.find( + obj.top.origData['foreign_key'], + (r) => r.cid == state.cid + ); } else { origData = obj.origData; } @@ -304,14 +316,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,9 +370,10 @@ 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.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 650dca4a571..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 @@ -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,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 ? - obj._top?._sessData.amname : - obj._top?.origData.amname; + let amname = obj.top?.sessData.amname || + obj.top?.origData.amname; if(_.isUndefined(amname)) return options; @@ -573,10 +573,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..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 @@ -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_relation', },{ 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_relation', },{ 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_relation', },{ 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_relation', },{ 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_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, inlineNext: true, + 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, inlineNext: true, + 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), - inlineNext: true, + 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_relation', } ]; } @@ -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,9 +629,6 @@ export default class TableSchema extends BaseUISchema { } }); }, - },{ - id: 'advanced', label: gettext('Advanced'), type: 'group', - visible: true, }, { id: 'rlspolicy', label: gettext('RLS Policy?'), cell: 'switch', @@ -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..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 @@ -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..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.origData.oid !== undefined && state.definition !== obj.origData.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 7885792d0dd..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,9 +136,10 @@ 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.origData.oid !== undefined + !_.isUndefined(obj.sessData.oid) ) || ( state.definition === obj.origData.definition )) { @@ -150,7 +152,7 @@ export default class ViewSchema extends BaseUISchema { ).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/subscriptions/static/js/subscription.ui.js b/web/pgadmin/browser/server_groups/servers/databases/subscriptions/static/js/subscription.ui.js index f6775c187df..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.origData.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.origData.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/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/browser/server_groups/servers/static/js/server.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/server.ui.js index 34e3e57ffa1..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 @@ -136,7 +136,7 @@ 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)=>{ + readonly: (s) => { return !(!this.origData.shared && s.shared); }, visible: ()=>{ return current_user.is_admin && pgAdmin.server_mode == 'True'; 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 ab6d4aac2b3..5e4f384d988 100644 --- a/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx +++ b/web/pgadmin/preferences/static/js/components/PreferencesComponent.jsx @@ -87,6 +87,7 @@ const StyledBox = styled(Box)(({theme}) => ({ }, })); + class PreferencesSchema extends BaseUISchema { constructor(initValues = {}, schemaFields = []) { super({ @@ -100,8 +101,8 @@ class PreferencesSchema extends BaseUISchema { return 'id'; } - setSelectedCategory(category) { - this.category = category; + categoryUpdated() { + this.state?.validate(this.sessData); } get baseFields() { @@ -110,7 +111,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); @@ -118,20 +120,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); + }} + /> +
); } @@ -144,6 +157,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({}); @@ -214,12 +228,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 = ''; @@ -335,9 +354,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'] }, ); } @@ -351,28 +371,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); @@ -386,10 +403,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) { @@ -655,32 +668,54 @@ 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.')} + /> - }> + }> {gettext('Reset all preferences')} - { 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..2e954a816c6 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/features/expandabledFormView.jsx @@ -0,0 +1,88 @@ +///////////////////////////////////////////////////////////// +// +// 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 { 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, + ); + } + } +} 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..6ab26acaec0 --- /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..b60363f7e32 --- /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, '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..30bd3bd1c3c --- /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..92e25c3a6d6 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx @@ -0,0 +1,105 @@ +///////////////////////////////////////////////////////////// +// +// 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); + 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); + + 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']; + rowValue = value; + } + + 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..a95ca912f90 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/DataGridView/row.jsx @@ -0,0 +1,93 @@ +///////////////////////////////////////////////////////////// +// +// 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 } 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 { DataGridContext, DataGridRowContext } from './context'; + + +export function DataGridRow({rowId, isResizing}) { + const schemaState = useContext(SchemaStateContext); + + const { accessPath, options, table, features } = useContext( + DataGridContext + ); + + const rowAccessPath = schemaState.accessPath(accessPath, rowId); + const rowOptions = useFieldOptions(rowAccessPath, schemaState); + + const rowRef = useRef(null); + const row = table.getRowModel().rows[rowId]; + + /* + * 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 classList = []; + let attributes = {}; + let expandedRowContents = []; + + features.current?.onRow({ + index: rowId, row, rowRef, classList, attributes, expandedRowContents, + rowOptions, tableOptions: options + }); + + let depsMap = [ + rowId, row?.getIsExpanded(), 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..556b92305f5 100644 --- a/web/pgadmin/static/js/SchemaView/FieldSetView.jsx +++ b/web/pgadmin/static/js/SchemaView/FieldSetView.jsx @@ -7,156 +7,67 @@ // ////////////////////////////////////////////////////////////// -import React, { useContext, useEffect } from 'react'; - -import Grid from '@mui/material/Grid'; -import _ from 'lodash'; +import React, { useContext, useMemo, useState } 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 { useFieldSchema, useFieldValue } 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 [key, setRefreshKey] = useState(0); + const schema = field.schema; const schemaState = useContext(SchemaStateContext); + const value = useFieldValue(accessPath, schemaState); + const options = useFieldSchema( + field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey + ); + 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} - - ); - } - 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, '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..fce33f112d3 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -8,80 +8,117 @@ ////////////////////////////////////////////////////////////// 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 { useFieldSchema, useFieldValue } 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, resetKey, focusOnFirstInput=false +}) { + const [key, setKey] = useState(0); + const schemaState = useContext(SchemaStateContext); + const value = useFieldValue(accessPath, schemaState); + const { visible } = useFieldSchema( + field, accessPath, value, viewHelperProps, schemaState, key, setKey + ); - 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; + + // 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( + ['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 +130,184 @@ 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(()=>{ - if (schemaState?.isReady) + // Upon reset, set the tab to first. + useEffect(() => { + if (!visible || !resetKey) return; + if (resetKey) { 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); - } } - } + }, [resetKey]); - 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, '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..c692729b8ce --- /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, '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..7a9da51ba9d 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 } 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,22 @@ 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({ + const {schemaState, dataDispatch, 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(); @@ -84,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. @@ -93,24 +80,13 @@ 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(); reset(); return true; }; @@ -128,7 +104,7 @@ export default function SchemaDialogView({ }; const save = (changeData) => { - props.onSave(isNew, changeData) + props.onSave(schemaState.isNew, changeData) .then(()=>{ if(schema.informText) { Notifier.alert( @@ -151,20 +127,23 @@ 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...'); 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(''); @@ -173,29 +152,22 @@ 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.')); } - 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. */ - return props.getSQLValue(isNew, getForQueryParams(changeData)); + return props.getSQLValue(schemaState.isNew, getForQueryParams(changeData)); }; const getButtonIcon = () => { @@ -207,29 +179,24 @@ 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(() => - - + - + isTabView={isTabView} + className={props.formClassName} + showError={true} resetKey={props.resetKey} + focusOnFirstInput={true} + /> {showFooter && @@ -237,13 +204,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 +221,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..23614c3753e 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,179 +16,95 @@ 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] ); - 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]} - - - - ); - })} + if (!finalTabs) return <>; + + return useMemo( + () => + + + + + 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) => + ) + } + + + + ); + })} + - - + + , + [schema._id] ); } 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..9082840c76d --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js @@ -0,0 +1,330 @@ +///////////////////////////////////////////////////////////// +// +// 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: 'initialising', + 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. + // Internal state for keeping the changes + 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 = state.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) { + if (!path || !path.length) return this.data; + 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 86% rename from web/pgadmin/static/js/SchemaView/schemaUtils.js rename to web/pgadmin/static/js/SchemaView/SchemaState/common.js index 0fc204f50af..6e8b74f4801 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) => ( @@ -276,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)) @@ -331,3 +346,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..991d9593c8e --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/SchemaState/store.js @@ -0,0 +1,80 @@ +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 () => { + 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..537658822f8 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, '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..c294172fd81 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/index.js @@ -0,0 +1,23 @@ +///////////////////////////////////////////////////////////// +// +// 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'; +import { useFieldSchema } from './useFieldSchema'; + + +export { + useFieldError, + useFieldOptions, + useFieldValue, + useFieldSchema, + 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..07542725098 --- /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, schemaState?._id]); + + 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..5763edc2426 --- /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, 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 new file mode 100644 index 00000000000..0cbd2bd94c4 --- /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()); + }; + }, [key, schemaState?._id]); + + 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, + }), + }; +}; 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..0e92ae9e42a --- /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, schemaState?._id]); + + 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..d22c9d3b1c6 --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js @@ -0,0 +1,143 @@ +///////////////////////////////////////////////////////////// +// +// 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, +}) => { + + if (!schema) + return { + schemaState: null, + dataDispatch: null, + reset: null, + }; + + 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, + reset: resetData, + }; +}; diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 11f6bfff480..fa91803bf09 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -7,43 +7,46 @@ // ////////////////////////////////////////////////////////////// +import BaseUISchema from './base_schema.ui'; 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 + getForQueryParams, + prepareData, } 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 + prepareData, + 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..9392887a5ef --- /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 ${name} 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..95b49307a10 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), + } })); @@ -351,12 +354,23 @@ 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); + + useEffect(() => setVal(finalValue), [finalValue]); + const onChangeFinal = (e) => { let changeVal = e.target.value; /* For type number, we set type as tel with number regex to get validity.*/ @@ -368,14 +382,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 */ @@ -403,7 +413,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 }) @@ -626,7 +636,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 +1119,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/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 = { 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/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 c1173b7000e..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,7 +73,7 @@ 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() diff --git a/web/regression/feature_utils/pgadmin_page.py b/web/regression/feature_utils/pgadmin_page.py index 2ec898a3d59..2b5a7f296d4 100644 --- a/web/regression/feature_utils/pgadmin_page.py +++ b/web/regression/feature_utils/pgadmin_page.py @@ -1147,29 +1147,39 @@ 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.driver.execute_script( + self.js_executor_scrollintoview_arg, top_el + ) r_scroll -= 1 else: break 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: