From 261cec1d208febc4ca01cb9be9e96a571edac1a4 Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Mon, 23 Aug 2021 13:40:14 +0530 Subject: [PATCH] Port Table, Column, Primary key, Foreign key, Check constraint, Unique constraint, Exclusion constraint. --- .../tables/columns/static/js/column.js | 778 +----------- .../tables/columns/static/js/column.ui.js | 613 +++++++++ .../static/js/check_constraint.js | 107 +- .../static/js/check_constraint.ui.js | 103 ++ .../static/js/exclusion_constraint.js | 1000 +-------------- .../static/js/exclusion_constraint.ui.js | 449 +++++++ .../foreign_key/static/js/foreign_key.js | 1024 +-------------- .../foreign_key/static/js/foreign_key.ui.js | 405 ++++++ .../index_constraint/static/js/primary_key.js | 580 +-------- .../static/js/primary_key.ui.js | 271 ++++ .../static/js/unique_constraint.js | 578 +-------- .../static/js/unique_constraint.ui.js | 273 ++++ .../tables/static/js/partition.utils.ui.js | 386 ++++++ .../schemas/tables/static/js/table.js | 1093 +---------------- .../schemas/tables/static/js/table.ui.js | 873 +++++++++++++ .../servers/static/js/vacuum.ui.js | 4 +- web/pgadmin/browser/static/js/node_view.jsx | 2 +- .../static/js/SchemaView/DataGridView.jsx | 10 +- web/pgadmin/static/js/SchemaView/FormView.jsx | 2 +- .../static/js/SchemaView/base_schema.ui.js | 10 +- web/pgadmin/static/js/SchemaView/index.jsx | 18 +- .../static/js/components/FormComponents.jsx | 7 +- .../components/FormComponents.spec.js | 12 +- .../jasmine_capture_warnings_beforeall.js | 22 +- .../check_constraint.ui.spec.js | 161 +++ .../schema_ui_files/collation.ui.spec.js | 1 - .../schema_ui_files/column.ui.spec.js | 307 +++++ .../exclusion_constraint.ui.spec.js | 323 +++++ .../schema_ui_files/foreign_key.ui.spec.js | 266 ++++ .../partition.utils.ui.spec.js | 299 +++++ .../schema_ui_files/primary_key.ui.spec.js | 217 ++++ .../schema_ui_files/server.ui.spec.js | 2 +- .../schema_ui_files/server_group.ui.spec.js | 2 +- .../schema_ui_files/table.ui.spec.js | 359 ++++++ .../schema_ui_files/tablespace.ui.spec.js | 57 - .../unique_constraint.ui.spec.js | 216 ++++ 36 files changed, 5662 insertions(+), 5168 deletions(-) create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils.ui.js create mode 100644 web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js create mode 100644 web/regression/javascript/schema_ui_files/check_constraint.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/column.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/exclusion_constraint.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/foreign_key.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/partition.utils.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/primary_key.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/table.ui.spec.js create mode 100644 web/regression/javascript/schema_ui_files/unique_constraint.ui.spec.js diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js index 09ef94a75..e526302ff 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.js @@ -7,6 +7,8 @@ // ////////////////////////////////////////////////////////////// +import { getNodeColumnSchema } from './column.ui'; + define('pgadmin.node.column', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid', @@ -28,68 +30,6 @@ define('pgadmin.node.column', [ }); } - // This Node model will be used for variable control for column - var VariablesModel = Backform.VariablesModel = pgBrowser.Node.Model.extend({ - idAttribute: 'name', - defaults: { - name: null, - value: null, - }, - schema: [{ - id: 'name', label: gettext('Name'), cell: 'select2', - type: 'text', disabled: false, node: 'column', - options: [['n_distinct', 'n_distinct'], - ['n_distinct_inherited','n_distinct_inherited']], - select2: {placeholder: gettext('Select variable')}, - cellHeaderClasses:'width_percent_50', - },{ - id: 'value', label: gettext('Value'), - type: 'text', disabled: false, - cellHeaderClasses:'width_percent_50', - }], - validate: function() { - if ( - _.isUndefined(this.get('value')) || - _.isNull(this.get('value')) || - String(this.get('value')).replace(/^\s+|\s+$/g, '') == '' - ) { - var errmsg = gettext('Please provide input for variable.'); - this.errorModel.set('value', errmsg); - return errmsg; - } else { - this.errorModel.unset('value'); - } - return null; - }, - }); - - // Integer Cell for Columns Length and Precision - var IntegerDepCell = Backgrid.Extension.IntegerDepCell = - Backgrid.IntegerCell.extend({ - initialize: function() { - Backgrid.NumberCell.prototype.initialize.apply(this, arguments); - Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments); - }, - dependentChanged: function () { - this.model.set(this.column.get('name'), null); - this.render(); - return this; - }, - render: function() { - Backgrid.NumberCell.prototype.render.apply(this, arguments); - - var model = this.model, - column = this.column, - editable = this.column.get('editable'), - is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable; - - if (is_editable){ this.$el.addClass('editable'); } - else { this.$el.removeClass('editable'); } - return this; - }, - remove: Backgrid.Extension.DependentCell.prototype.remove, - }); - if (!pgBrowser.Nodes['column']) { pgBrowser.Nodes['column'] = pgBrowser.Node.extend({ parent_type: ['table', 'view', 'mview'], @@ -155,690 +95,30 @@ define('pgadmin.node.column', [ }, ]); }, + getSchema: function(treeNodeInfo, itemNodeData) { + return getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser); + }, model: pgBrowser.Node.Model.extend({ idAttribute: 'attnum', defaults: { name: undefined, - attowner: undefined, - atttypid: undefined, attnum: undefined, - cltype: undefined, - collspcname: undefined, - attacl: undefined, description: undefined, - parent_tbl: undefined, - min_val_attlen: undefined, - min_val_attprecision: undefined, - max_val_attlen: undefined, - max_val_attprecision: undefined, - edit_types: undefined, - is_primary_key: false, - inheritedfrom: undefined, - attstattarget:undefined, - attnotnull: false, - attlen: null, - attprecision: null, - attidentity: 'a', - seqincrement: undefined, - seqstart: undefined, - seqmin: undefined, - seqmax: undefined, - seqcache: undefined, - seqcycle: undefined, - colconstype: 'n', - genexpr: undefined, - }, - initialize: function(attrs) { - if (_.size(attrs) !== 0) { - this.set({ - 'old_attidentity': this.get('attidentity'), - }, {silent: true}); - } - pgBrowser.Node.Model.prototype.initialize.apply(this, arguments); - }, schema: [{ id: 'name', label: gettext('Name'), cell: 'string', type: 'text', disabled: 'inSchemaWithColumnCheck', cellHeaderClasses:'width_percent_30', editable: 'editable_check_for_table', - },{ - // Need to show this field only when creating new table - // [in SubNode control] - id: 'is_primary_key', label: gettext('Primary key?'), - cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch', - deps:['name'], cellHeaderClasses:'width_percent_5', - options: { - onText: gettext('Yes'), offText: gettext('No'), - onColor: 'success', offColor: 'ternary', - }, - visible: function(m) { - return _.isUndefined( - m.top.node_info['table'] || m.top.node_info['view'] || - m.top.node_info['mview'] - ); - }, - disabled: function(m){ - // Disable it, when one of this: - // - Primary key already exist - // - Table is a partitioned table - if ( - m.top && (( - !_.isUndefined(m.top.get('oid')) && - !_.isUndefined(m.top.get('primary_key')) && - m.top.get('primary_key').length > 0 && - !_.isUndefined(m.top.get('primary_key').first().get('oid')) - ) || ( - m.top.has('is_partitioned') && m.top.get('is_partitioned') && - m.top.node_info.server && m.top.node_info.server.version < 11000 - )) - ) { - return true; - } - - var name = m.get('name'); - - if(!m.inSchemaWithColumnCheck.apply(this, [m]) && - (_.isUndefined(name) || _.isNull(name) || name == '')) { - return true; - } - return false; - }, - editable: function(m) { - // If HeaderCell then allow True - if(m instanceof Backbone.Collection) { - return true; - } - // If primary key already exist then disable. - if (m.top && !_.isUndefined(m.top.get('oid')) && - !_.isUndefined(m.top.get('primary_key')) && - m.top.get('primary_key').length > 0 && - !_.isUndefined(m.top.get('primary_key').first().get('oid'))) { - - return false; - } - - // If table is partitioned table then disable - if (m.top && !_.isUndefined(m.top.get('is_partitioned')) && - m.top.get('is_partitioned') && m.top.node_info.server && - m.top.node_info.server.version < 11000) - { - setTimeout(function () { - m.set('is_primary_key', false); - }, 10); - - return false; - } - - if(!m.inSchemaWithColumnCheck.apply(this, [m])) { - return true; - } - return false; - }, },{ id: 'attnum', label: gettext('Position'), cell: 'string', type: 'text', disabled: 'notInSchema', mode: ['properties'], - },{ - id: 'cltype', label: gettext('Data type'), - cell: Backgrid.Extension.NodeAjaxOptionsCell, - type: 'text', disabled: 'inSchemaWithColumnCheck', - control: 'node-ajax-options', url: 'get_types', node: 'table', - cellHeaderClasses:'width_percent_30', first_empty: true, - select2: { allowClear: false }, group: gettext('Definition'), - cache_node: 'table', - transform: function(data, cell) { - /* 'transform' function will be called by control, and cell both. - * The way, we use the transform in cell, and control is different. - * Because - options are shared using 'column' object in backgrid, - * hence - the cell is passed as second parameter, while the control - * uses (this) as a object. - */ - var control = cell || this, - m = control.model; - - /* We need different data in create mode & in edit mode - * if we are in create mode then return data as it is - * if we are in edit mode then we need to filter data - */ - control.model.datatypes = data; - var edit_types = m.get('edit_types'), - result = []; - - // If called from Table, We will check if in edit mode - // then send edit_types only - if( !_.isUndefined(m.top) && !m.top.isNew() ) { - _.each(data, function(t) { - if (_.indexOf(edit_types, t.value) != -1) { - result.push(t); - } - }); - // There may be case that user adds new column in existing collection - // we will not have edit types then - return result.length > 0 ? result : data; - } - - // If called from Column - if(m.isNew()) { - return data; - } else { - //edit mode - _.each(data, function(t) { - if (_.indexOf(edit_types, t.value) != -1) { - result.push(t); - } - }); - - return result; - } - }, - editable: 'editable_check_for_table', - },{ - // Need to show this field only when creating new table [in SubNode control] - id: 'inheritedfrom', label: gettext('Inherited from table'), - type: 'text', readonly: true, editable: false, - cellHeaderClasses:'width_percent_10', - visible: function(m) { - return _.isUndefined(m.top.node_info['table'] || m.top.node_info['view'] || m.top.node_info['mview']); - }, - },{ - id: 'attlen', label: gettext('Length/Precision'), cell: IntegerDepCell, - deps: ['cltype'], type: 'int', group: gettext('Definition'), cellHeaderClasses:'width_percent_20', - disabled: function(m) { - var of_type = m.get('cltype'), - flag = true; - _.each(m.datatypes, function(o) { - if ( of_type == o.value ) { - if(o.length) - { - m.set('min_val_attlen', o.min_val, {silent: true}); - m.set('max_val_attlen', o.max_val, {silent: true}); - flag = false; - } - } - }); - - flag && setTimeout(function() { - if(m.get('attlen')) { - m.set('attlen', null); - } - },10); - - return flag; - }, - editable: function(m) { - // inheritedfrom has value then we should disable it - if(!_.isUndefined(m.get('inheritedfrom'))) { - return false; - } - - if (!m.datatypes) { - // datatypes not loaded, may be this call is from CallByNeed from backgrid cell initialize. - return true; - } - var of_type = m.get('cltype'), - flag = false; - - _.each(m.datatypes, function(o) { - if ( of_type == o.value ) { - if(o.length) { - m.set('min_val_attlen', o.min_val, {silent: true}); - m.set('max_val_attlen', o.max_val, {silent: true}); - flag = true; - } - } - }); - - !flag && setTimeout(function() { - if(m.get('attlen')) { - m.set('attlen', null, {silent: true}); - } - },10); - - return flag; - }, - },{ - id: 'attprecision', label: gettext('Scale'), cell: IntegerDepCell, - deps: ['cltype'], type: 'int', group: gettext('Definition'), cellHeaderClasses:'width_percent_20', - disabled: function(m) { - var of_type = m.get('cltype'), - flag = true; - _.each(m.datatypes, function(o) { - if ( of_type == o.value ) { - if(o.precision) { - m.set('min_val_attprecision', 0, {silent: true}); - m.set('max_val_attprecision', o.max_val, {silent: true}); - flag = false; - } - } - }); - - flag && setTimeout(function() { - if(m.get('attprecision')) { - m.set('attprecision', null); - } - },10); - return flag; - }, - editable: function(m) { - // inheritedfrom has value then we should disable it - if(!_.isUndefined(m.get('inheritedfrom'))) { - return false; - } - - if (!m.datatypes) { - // datatypes not loaded yet, may be this call is from CallByNeed from backgrid cell initialize. - return true; - } - - var of_type = m.get('cltype'), - flag = false; - _.each(m.datatypes, function(o) { - if ( of_type == o.value ) { - if(o.precision) { - m.set('min_val_attprecision', 0, {silent: true}); - m.set('max_val_attprecision', o.max_val, {silent: true}); - flag = true; - } - } - }); - - !flag && setTimeout(function() { - if(m.get('attprecision')) { - m.set('attprecision', null); - } - },10); - - return flag; - }, - },{ - id: 'collspcname', label: gettext('Collation'), cell: 'string', - type: 'text', control: 'node-ajax-options', url: 'get_collations', - group: gettext('Definition'), node: 'collation', - deps: ['cltype'], disabled: function(m) { - var of_type = m.get('cltype'), - flag = true; - _.each(m.datatypes, function(o) { - if ( of_type == o.value ) { - if(o.is_collatable) - { - flag = false; - } - } - }); - if (flag) { - setTimeout(function(){ - if(m.get('collspcname') && m.get('collspcname') !== '') { - m.set('collspcname', ''); - } - }, 10); - } - return flag; - }, - },{ - id: 'attstattarget', label: gettext('Statistics'), cell: 'string', - type: 'text', disabled: 'inSchemaWithColumnCheck', mode: ['properties', 'edit'], - group: gettext('Definition'), - },{ - id: 'attstorage', label: gettext('Storage'), group: gettext('Definition'), - type: 'text', mode: ['properties', 'edit'], - cell: 'string', disabled: 'inSchemaWithColumnCheck', first_empty: true, - control: 'select2', select2: { placeholder: gettext('Select storage'), - allowClear: false, - width: '100%', - }, - options: [ - {label: 'PLAIN', value: 'p'}, - {label: 'MAIN', value: 'm'}, - {label: 'EXTERNAL', value: 'e'}, - {label: 'EXTENDED', value: 'x'}, - ], - },{ - id: 'defval', label: gettext('Default'), cell: 'string', - type: 'text', group: gettext('Constraints'), deps: ['cltype', 'colconstype'], - disabled: function(m) { - var is_disabled = false; - if(!m.inSchemaWithModelCheck.apply(this, [m])) { - var type = m.get('cltype'); - is_disabled = (type == 'serial' || type == 'bigserial' || type == 'smallserial'); - } - - is_disabled = is_disabled || m.get('colconstype') != 'n'; - if (is_disabled && m.isNew()) { - setTimeout(function () { - m.set('defval', undefined); - }, 10); - } - - return is_disabled; - }, - },{ - id: 'attnotnull', label: gettext('Not NULL?'), cell: 'switch', - type: 'switch', cellHeaderClasses:'width_percent_20', - group: gettext('Constraints'), editable: 'editable_check_for_table', - options: { onText: gettext('Yes'), offText: gettext('No'), onColor: 'success', offColor: 'ternary' }, - deps: ['colconstype'], - disabled: function(m) { - if (m.get('colconstype') == 'i') { - setTimeout(function () { - m.set('attnotnull', true); - }, 10); - } - return m.inSchemaWithColumnCheck(m); - }, - }, { - id: 'colconstype', - label: gettext('Type'), - cell: 'string', - type: 'radioModern', - controlsClassName: 'pgadmin-controls col-12 col-sm-9', - controlLabelClassName: 'control-label col-sm-3 col-12', - group: gettext('Constraints'), - options: function(m) { - var opt_array = [ - {'label': gettext('NONE'), 'value': 'n'}, - {'label': gettext('IDENTITY'), 'value': 'i'}, - ]; - - if (m.top.node_info && m.top.node_info.server && - m.top.node_info.server.version >= 120000) { - // You can't change the existing column to Generated column. - if (m.isNew()) { - opt_array.push({ - 'label': gettext('GENERATED'), - 'value': 'g', - }); - } else { - opt_array.push({ - 'label': gettext('GENERATED'), - 'value': 'g', - 'disabled': true, - }); - } - } - - return opt_array; - }, - disabled: function(m) { - if (!m.isNew() && m.get('colconstype') == 'g') { - return true; - } - return false; - }, - visible: function(m) { - if (m.top.node_info && m.top.node_info.server && - m.top.node_info.server.version >= 100000) { - return true; - } - return false; - }, - }, { - id: 'attidentity', label: gettext('Identity'), control: 'select2', - cell: 'select2', - select2: {placeholder: gettext('Select identity'), allowClear: false, width: '100%'}, - min_version: 100000, group: gettext('Constraints'), - 'options': [ - {label: gettext('ALWAYS'), value: 'a'}, - {label: gettext('BY DEFAULT'), value: 'd'}, - ], - deps: ['colconstype'], visible: 'isTypeIdentity', - disabled: function(m) { - if (!m.isNew()) { - if (m.get('attidentity') == '' && m.get('colconstype') == 'i') { - setTimeout(function () { - m.set('attidentity', m.get('old_attidentity')); - }, 10); - } - } - return false; - }, - }, { - id: 'seqincrement', label: gettext('Increment'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', - visible: 'isTypeIdentity', - },{ - id: 'seqstart', label: gettext('Start'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - disabled: 'isIdentityColumn', deps: ['attidentity', 'colconstype'], - visible: 'isTypeIdentity', - },{ - id: 'seqmin', label: gettext('Minimum'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', - visible: 'isTypeIdentity', - },{ - id: 'seqmax', label: gettext('Maximum'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', - visible: 'isTypeIdentity', - },{ - id: 'seqcache', label: gettext('Cache'), type: 'int', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - min: 1, deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', - visible: 'isTypeIdentity', - },{ - id: 'seqcycle', label: gettext('Cycled'), type: 'switch', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - deps: ['attidentity', 'colconstype'], disabled: 'isIdentityColumn', - visible: 'isTypeIdentity', - },{ - id: 'genexpr', label: gettext('Expression'), type: 'text', - mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), - min_version: 120000, deps: ['colconstype'], visible: 'isTypeGenerated', - readonly: function(m) { - return !m.isNew(); - }, - },{ - id: 'is_pk', label: gettext('Primary key?'), - type: 'switch', mode: ['properties'], - group: gettext('Definition'), - },{ - id: 'is_fk', label: gettext('Foreign key?'), - type: 'switch', mode: ['properties'], - group: gettext('Definition'), - },{ - id: 'is_inherited', label: gettext('Inherited?'), - type: 'switch', mode: ['properties'], - group: gettext('Definition'), - },{ - id: 'tbls_inherited', label: gettext('Inherited from table(s)'), - type: 'text', mode: ['properties'], deps: ['is_inherited'], - group: gettext('Definition'), - visible: function(m) { - return (!_.isUndefined(m.get('is_inherited')) && m.get('is_inherited')); - }, - },{ - id: 'is_sys_column', label: gettext('System column?'), cell: 'string', - type: 'switch', mode: ['properties'], },{ id: 'description', label: gettext('Comment'), cell: 'string', type: 'multiline', mode: ['properties', 'create', 'edit'], disabled: 'notInSchema', - },{ - id: 'attoptions', label: gettext('Variables'), type: 'collection', - group: gettext('Variables'), control: 'unique-col-collection', - model: VariablesModel, uniqueCol : ['name'], - mode: ['edit', 'create'], canAdd: true, canEdit: false, - canDelete: true, - }, pgBrowser.SecurityGroupSchema, { - id: 'attacl', label: gettext('Privileges'), type: 'collection', - group: 'security', control: 'unique-col-collection', - model: pgBrowser.Node.PrivilegeRoleModel.extend({ - privileges: ['a','r','w','x']}), - mode: ['edit'], canAdd: true, canDelete: true, - uniqueCol : ['grantee'], - },{ - id: 'seclabels', label: gettext('Security labels'), canAdd: true, - model: pgBrowser.SecLabelModel, group: 'security', - mode: ['edit', 'create'], editable: false, type: 'collection', - min_version: 90100, canEdit: false, canDelete: true, - control: 'unique-col-collection', }], - validate: function(keys) { - var msg = undefined; - - // Nothing to validate - if (keys && keys.length == 0) { - this.errorModel.clear(); - return null; - } else { - this.errorModel.clear(); - } - - if (_.isUndefined(this.get('name')) - || String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Column name cannot be empty.'); - this.errorModel.set('name', msg); - return msg; - } - - if (_.isUndefined(this.get('cltype')) - || String(this.get('cltype')).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Column type cannot be empty.'); - this.errorModel.set('cltype', msg); - return msg; - } - - if (!_.isUndefined(this.get('cltype')) - && !_.isUndefined(this.get('attlen')) - && !_.isNull(this.get('attlen')) - && this.get('attlen') !== '') { - // Validation for Length field - if (this.get('attlen') < this.get('min_val_attlen')) - msg = gettext('Length/Precision should not be less than: ') + this.get('min_val_attlen'); - if (this.get('attlen') > this.get('max_val_attlen')) - msg = gettext('Length/Precision should not be greater than: ') + this.get('max_val_attlen'); - // If we have any error set then throw it to user - if(msg) { - this.errorModel.set('attlen', msg); - return msg; - } - } - - if (!_.isUndefined(this.get('cltype')) - && !_.isUndefined(this.get('attprecision')) - && !_.isNull(this.get('attprecision')) - && this.get('attprecision') !== '') { - // Validation for precision field - if (this.get('attprecision') < this.get('min_val_attprecision')) - msg = gettext('Scale should not be less than: ') + this.get('min_val_attprecision'); - if (this.get('attprecision') > this.get('max_val_attprecision')) - msg = gettext('Scale should not be greater than: ') + this.get('max_val_attprecision'); - // If we have any error set then throw it to user - if(msg) { - this.errorModel.set('attprecision', msg); - return msg; - } - } - - let genexpr = this.get('genexpr'); - if (this.get('colconstype') == 'g' && - (_.isUndefined(genexpr) || _.isNull(genexpr) || genexpr == '')) { - msg = gettext('Expression value cannot be empty.'); - this.errorModel.set('genexpr', msg); - return msg; - } else { - this.errorModel.unset('genexpr'); - } - - var minimum = this.get('seqmin'), - maximum = this.get('seqmax'), - start = this.get('seqstart'); - - if (!this.isNew() && this.get('colconstype') == 'i' && - (this.get('old_attidentity') == 'a' || this.get('old_attidentity') == 'd') && - (this.get('attidentity') == 'a' || this.get('attidentity') == 'd')) { - if (_.isUndefined(this.get('seqincrement')) - || String(this.get('seqincrement')).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Increment value cannot be empty.'); - this.errorModel.set('seqincrement', msg); - return msg; - } else { - this.errorModel.unset('seqincrement'); - } - - if (_.isUndefined(this.get('seqmin')) - || String(this.get('seqmin')).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Minimum value cannot be empty.'); - this.errorModel.set('seqmin', msg); - return msg; - } else { - this.errorModel.unset('seqmin'); - } - - if (_.isUndefined(this.get('seqmax')) - || String(this.get('seqmax')).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Maximum value cannot be empty.'); - this.errorModel.set('seqmax', msg); - return msg; - } else { - this.errorModel.unset('seqmax'); - } - - if (_.isUndefined(this.get('seqcache')) - || String(this.get('seqcache')).replace(/^\s+|\s+$/g, '') == '') { - msg = gettext('Cache value cannot be empty.'); - this.errorModel.set('seqcache', msg); - return msg; - } else { - this.errorModel.unset('seqcache'); - } - } - var min_lt = gettext('Minimum value must be less than maximum value.'), - start_lt = gettext('Start value cannot be less than minimum value.'), - start_gt = gettext('Start value cannot be greater than maximum value.'); - - if (_.isEmpty(minimum) || _.isEmpty(maximum)) - return null; - - if ((minimum == 0 && maximum == 0) || - (parseInt(minimum, 10) >= parseInt(maximum, 10))) { - this.errorModel.set('seqmin', min_lt); - return min_lt; - } else { - this.errorModel.unset('seqmin'); - } - - if (start && minimum && parseInt(start) < parseInt(minimum)) { - this.errorModel.set('seqstart', start_lt); - return start_lt; - } else { - this.errorModel.unset('seqstart'); - } - - if (start && maximum && parseInt(start) > parseInt(maximum)) { - this.errorModel.set('seqstart', start_gt); - return start_gt; - } else { - this.errorModel.unset('seqstart'); - } - - return null; - }, - // Check whether the column is identity column or not - isIdentityColumn: function(m) { - let isIdentity = m.get('attidentity'); - if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity)) - return false; - return true; - }, - // Check whether the column is a identity column - isTypeIdentity: function(m) { - let colconstype = m.get('colconstype'); - if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'i') { - return true; - } - return false; - }, - // Check whether the column is a generated column - isTypeGenerated: function(m) { - let colconstype = m.get('colconstype'); - if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'g') { - return true; - } - return false; - }, // We will check if we are under schema node & in 'create' mode notInSchema: function() { if(this.node_info && 'catalog' in this.node_info) @@ -847,54 +127,6 @@ define('pgadmin.node.column', [ } return false; }, - // We will check if we are under schema node & in 'create' mode - inSchemaWithModelCheck: function(m) { - if(this.node_info && 'schema' in this.node_info) - { - // We will disable control if it's in 'edit' mode - return !(m.isNew()); - } - return true; - }, - // Checks weather to enable/disable control - inSchemaWithColumnCheck: function(m) { - var node_info = this.node_info || m.node_info || m.top.node_info; - - // disable all fields if column is listed under view or mview - if (node_info && ('view' in node_info || 'mview' in node_info)) { - if (this && _.has(this, 'name') && (this.name != 'defval')) { - return true; - } - } - - if(node_info && 'schema' in node_info) - { - // We will disable control if it's system columns - // inheritedfrom check is useful when we use this schema in table node - // inheritedfrom has value then we should disable it - if(!_.isUndefined(m.get('inheritedfrom'))) { - return true; - } - // ie: it's position is less then 1 - if (m.isNew()) { - return false; - } - // if we are in edit mode - return !(!_.isUndefined(m.get('attnum')) && m.get('attnum') > 0 ); - } - return true; - }, - editable_check_for_table: function(arg) { - if (arg instanceof Backbone.Collection) { - return !arg.model.prototype.inSchemaWithColumnCheck.apply( - this, [arg.top] - ); - } else { - return !arg.inSchemaWithColumnCheck.apply( - this, [arg] - ); - } - }, }), // Below function will enable right click menu for creating column canCreate: function(itemData, item, data) { diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js new file mode 100644 index 000000000..8910fa844 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui.js @@ -0,0 +1,613 @@ +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import VariableSchema from 'top/browser/server_groups/servers/static/js/variable.ui'; +import SecLabelSchema from 'top/browser/server_groups/servers/static/js/sec_label.ui'; +import _ from 'lodash'; +import { isEmptyString } from '../../../../../../../../../static/js/validators'; +import { getNodePrivilegeRoleSchema } from '../../../../../../static/js/privilege.ui'; +import { getNodeAjaxOptions } from '../../../../../../../../static/js/node_ajax'; + +export function getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser) { + return new ColumnSchema( + (privileges)=>getNodePrivilegeRoleSchema(this, treeNodeInfo, itemNodeData, privileges), + treeNodeInfo, + ()=>getNodeAjaxOptions('get_types', pgBrowser.Nodes['table'], treeNodeInfo, itemNodeData, { + cacheLevel: 'table', + }), + ()=>getNodeAjaxOptions('get_collations', pgBrowser.Nodes['collation'], treeNodeInfo, itemNodeData), + ); +} + +export default class ColumnSchema extends BaseUISchema { + constructor(getPrivilegeRoleSchema, nodeInfo, cltypeOptions, collspcnameOptions) { + super({ + name: undefined, + attowner: undefined, + atttypid: undefined, + attnum: undefined, + cltype: undefined, + collspcname: undefined, + attacl: undefined, + description: undefined, + parent_tbl: undefined, + min_val_attlen: undefined, + min_val_attprecision: undefined, + max_val_attlen: undefined, + max_val_attprecision: undefined, + edit_types: undefined, + is_primary_key: false, + inheritedfrom: undefined, + attstattarget:undefined, + attnotnull: false, + attlen: null, + attprecision: null, + attidentity: 'a', + attoptions: [], + seqincrement: undefined, + seqstart: undefined, + seqmin: undefined, + seqmax: undefined, + seqcache: undefined, + seqcycle: undefined, + colconstype: 'n', + genexpr: undefined, + }); + + this.getPrivilegeRoleSchema = getPrivilegeRoleSchema; + this.nodeInfo = nodeInfo; + this.cltypeOptions = cltypeOptions; + this.collspcnameOptions = collspcnameOptions; + + this.datatypes = []; + } + + get idAttribute() { + return 'attnum'; + } + + inSchemaWithColumnCheck(state) { + // disable all fields if column is listed under view or mview + if (this.nodeInfo && ('view' in this.nodeInfo || 'mview' in this.nodeInfo)) { + return true; + } + + if('schema' in this.nodeInfo) + { + // We will disable control if it's system columns + // inheritedfrom check is useful when we use this schema in table node + // inheritedfrom has value then we should disable it + if(!_.isUndefined(state.inheritedfrom)) { + return true; + } + // ie: it's position is less than 1 + if(!_.isUndefined(state.attnum) && state.attnum <= 0) { + return true; + } + // if we are in edit mode + return !this.isNew(state); + } + return false; + } + + editableCheckForTable(state) { + return !this.inSchemaWithColumnCheck(state); + } + + // Check whether the column is identity column or not + isIdentityColumn(state) { + let isIdentity = state.attidentity; + if(!_.isUndefined(isIdentity) && !_.isNull(isIdentity) && !_.isEmpty(isIdentity)) + return false; + return true; + } + + // Check whether the column is a identity column + isTypeIdentity(state) { + let colconstype = state.colconstype; + if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'i') { + return true; + } + return false; + } + + // Check whether the column is a generated column + isTypeGenerated(state) { + let colconstype = state.colconstype; + if (!_.isUndefined(colconstype) && !_.isNull(colconstype) && colconstype == 'g') { + return true; + } + return false; + } + + // We will check if we are under schema node & in 'create' mode + inSchemaWithModelCheck(state) { + if(this.nodeInfo && 'schema' in this.nodeInfo) + { + // We will disable control if it's in 'edit' mode + return !(this.isNew(state)); + } + return true; + } + + attlenRange(state) { + for(let o of this.datatypes) { + if ( state.cltype == o.value ) { + if(o.length) return {min: o.min_val || 0, max: o.max_val}; + } + } + return null; + } + + attprecisionRange(state) { + for(let o of this.datatypes) { + if ( state.cltype == o.value ) { + if(o.precision) return {min: o.min_val || 0, max: o.max_val}; + } + } + return null; + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), cell: 'text', + type: 'text', disabled: obj.inSchemaWithColumnCheck, + editable: this.editableCheckForTable, noEmpty: true, + minWidth: 115, + },{ + // Need to show this field only when creating new table + // [in SubNode control] + id: 'is_primary_key', label: gettext('Primary key?'), + cell: 'switch', type: 'switch', minWidth: 100, deps:['name'], + visible: ()=>{ + return _.isUndefined( + this.nodeInfo['table'] || this.nodeInfo['view'] || + this.nodeInfo['mview'] + ); + }, + disabled: (state)=>{ + // Disable it, when one of this: + // - Primary key already exist + // - Table is a partitioned table + if ( + obj.top && (( + !_.isUndefined(obj.top.origData['oid']) + && !_.isUndefined(obj.top.origData['primary_key']) + && obj.top.origData['primary_key'].length > 0 + && !_.isUndefined(obj.top.origData['primary_key'][0]['oid']) + ) || ( + 'is_partitioned' in obj.top.origData + && obj.top.origData['is_partitioned'] + && obj.nodeInfo.server && obj.nodeInfo.server.version < 11000 + )) + ) { + return true; + } + + var name = state.name; + + if(!obj.inSchemaWithColumnCheck(state) + && (_.isUndefined(name) || _.isNull(name) || name == '')) { + return true; + } + return false; + }, + editable: function(state) { + // If primary key already exist then disable. + if ( + obj.top && (( + !_.isUndefined(obj.top.origData['oid']) + && !_.isUndefined(obj.top.origData['primary_key']) + && obj.top.origData['primary_key'].length > 0 + && !_.isUndefined(obj.top.origData['primary_key'][0]['oid']) + )) + ) { + return false; + } + + // If table is partitioned table then disable + if( + 'is_partitioned' in obj.top.origData + && obj.top.origData['is_partitioned'] + && obj.getServerVersion() < 11000 + ) { + return false; + } + + if(!obj.inSchemaWithColumnCheck(state)) { + return true; + } + return false; + }, + },{ + id: 'attnum', label: gettext('Position'), cell: 'text', + type: 'text', disabled: this.inCatalog, mode: ['properties'], + },{ + id: 'cltype', label: gettext('Data type'), + disabled: obj.inSchemaWithColumnCheck, minWidth: 150, + group: gettext('Definition'), noEmpty: true, + editable: this.editableCheckForTable, + options: this.cltypeOptions, optionsLoaded: (options)=>{obj.datatypes = options;}, + type: (state)=>{ + return { + type: 'select', + options: this.cltypeOptions, + controlProps: { + allowClear: false, + filter: (options)=>{ + let result = options; + let edit_types = state?.edit_types || []; + if(!obj.isNew(state)) { + result = _.filter(options, (o)=>edit_types.indexOf(o.value) > -1); + } + return result; + }, + } + }; + }, + cell: (row)=>{ + return { + cell: 'select', + options: this.cltypeOptions, + controlProps: { + allowClear: false, + filter: (options)=>{ + let result = options; + let edit_types = row?.edit_types || []; + if(!obj.isNew(row)) { + result = _.filter(options, (o)=>edit_types.indexOf(o.value) > -1); + } + return result; + }, + } + }; + } + },{ + /* This field is required to send it to back end */ + id: 'inheritedid', label: gettext(''), type: 'text', visible: false, + },{ + // Need to show this field only when creating new table [in SubNode control] + id: 'inheritedfrom', label: gettext('Inherited from table'), + type: 'text', readonly: true, editable: false, + visible: function() { + return _.isUndefined(this.nodeInfo['table'] || this.nodeInfo['view'] || this.nodeInfo['mview']); + }, + },{ + id: 'attlen', label: gettext('Length/Precision'), cell: 'int', + deps: ['cltype'], type: 'int', group: gettext('Definition'), width: 120, minWidth: 120, + depChange: (state)=>{ + let range = this.attlenRange(state); + if(range) { + return { + ...state, min_val_attlen: range.min, max_val_attlen: range.max, + }; + } else { + return { + ...state, attlen: null, + }; + } + }, + disabled: function(state) { + return !obj.attlenRange(state); + }, + editable: function(state) { + // inheritedfrom has value then we should disable it + if (!isEmptyString(state.inheritedfrom)) { + return false; + } + return Boolean(obj.attlenRange(state)); + }, + },{ + id: 'attprecision', label: gettext('Scale'), cell: 'int', minWidth: 60, + deps: ['cltype'], type: 'int', group: gettext('Definition'), + depChange: (state)=>{ + let range = this.attprecisionRange(state); + if(range) { + return { + ...state, min_val_attprecision: range.min, max_val_attprecision: range.max, + }; + } else { + return { + ...state, attprecision: null, + }; + } + }, + disabled: function(state) { + return !this.attprecisionRange(state); + }, + editable: function(state) { + // inheritedfrom has value then we should disable it + if (!isEmptyString(state.inheritedfrom)) { + return false; + } + return Boolean(this.attprecisionRange(state)); + }, + },{ + id: 'collspcname', label: gettext('Collation'), cell: 'select', + type: 'select', group: gettext('Definition'), + deps: ['cltype'], options: this.collspcnameOptions, + disabled: (state)=>{ + for(let o of this.datatypes) { + if ( state.cltype == o.value ) { + if(o.is_collatable) return false; + } + } + return true; + }, depChange: (state)=>{ + for(let o of this.datatypes) { + if ( state.cltype == o.value ) { + if(o.is_collatable) return {}; + } + } + return {collspcname: null}; + } + },{ + id: 'attstattarget', label: gettext('Statistics'), cell: 'text', + type: 'text', disabled: obj.inSchemaWithColumnCheck, mode: ['properties', 'edit'], + group: gettext('Definition'), + },{ + id: 'attstorage', label: gettext('Storage'), group: gettext('Definition'), + type: 'select', mode: ['properties', 'edit'], + cell: 'select', disabled: obj.inSchemaWithColumnCheck, first_empty: true, + controlProps: { placeholder: gettext('Select storage'), + allowClear: false, + }, + options: [ + {label: 'PLAIN', value: 'p'}, + {label: 'MAIN', value: 'm'}, + {label: 'EXTERNAL', value: 'e'}, + {label: 'EXTENDED', value: 'x'}, + ], + },{ + id: 'defval', label: gettext('Default'), cell: 'text', + type: 'text', group: gettext('Constraints'), deps: ['cltype', 'colconstype'], + disabled: function(state) { + var isDisabled = false; + if(!obj.inSchemaWithModelCheck(state)) { + isDisabled = ['serial', 'bigserial', 'smallserial'].indexOf(state.cltype) > -1; + } + isDisabled = isDisabled || state.colconstype != 'n'; + return isDisabled; + }, depChange: (state)=>{ + var isDisabled = false; + if(!obj.inSchemaWithModelCheck(state)) { + isDisabled = ['serial', 'bigserial', 'smallserial'].indexOf(state.cltype) > -1; + } + isDisabled = isDisabled || state.colconstype != 'n'; + if (isDisabled && obj.isNew(state)) { + return {defval: undefined}; + } + } + },{ + id: 'attnotnull', label: gettext('Not NULL?'), cell: 'switch', + type: 'switch', minWidth: 80, + group: gettext('Constraints'), editable: this.editableCheckForTable, + deps: ['colconstype'], + disabled: (state) => { + return obj.inSchemaWithColumnCheck(state); + }, depChange:(state)=>{ + if (state.colconstype == 'i') { + return {attnotnull: true}; + } + } + }, { + id: 'colconstype', + label: gettext('Type'), + cell: 'text', + group: gettext('Constraints'), + type: (state)=>{ + var options = [ + {'label': gettext('NONE'), 'value': 'n'}, + {'label': gettext('IDENTITY'), 'value': 'i'}, + ]; + + if (this.nodeInfo && this.nodeInfo.server && + this.nodeInfo.server.version >= 120000) { + // You can't change the existing column to Generated column. + if (this.isNew(state)) { + options.push({ + 'label': gettext('GENERATED'), + 'value': 'g', + }); + } else { + options.push({ + 'label': gettext('GENERATED'), + 'value': 'g', + 'disabled': true, + }); + } + } + + return { + type: 'toggle', + options: options, + }; + }, + disabled: function(state) { + if (!this.isNew(state) && state.colconstype == 'g') { + return true; + } + return false; + }, min_version: 100000, + }, { + id: 'attidentity', label: gettext('Identity'), + cell: 'select', type: 'select', + controlProps: {placeholder: gettext('Select identity'), allowClear: false}, + min_version: 100000, group: gettext('Constraints'), + options: [ + {label: gettext('ALWAYS'), value: 'a'}, + {label: gettext('BY DEFAULT'), value: 'd'}, + ], + deps: ['colconstype'], + visible: this.isTypeIdentity, + disabled: false, + },{ + id: 'seqincrement', label: gettext('Increment'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + min: 1, deps: ['attidentity', 'colconstype'], disabled: this.isIdentityColumn, + visible: this.isTypeIdentity, + },{ + id: 'seqstart', label: gettext('Start'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + disabled: this.isIdentityColumn, deps: ['attidentity', 'colconstype'], + visible: this.isTypeIdentity, + },{ + id: 'seqmin', label: gettext('Minimum'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + deps: ['attidentity', 'colconstype'], disabled: this.isIdentityColumn, + visible: this.isTypeIdentity, + },{ + id: 'seqmax', label: gettext('Maximum'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + deps: ['attidentity', 'colconstype'], disabled: this.isIdentityColumn, + visible: this.isTypeIdentity, + },{ + id: 'seqcache', label: gettext('Cache'), type: 'int', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + min: 1, deps: ['attidentity', 'colconstype'], disabled: this.isIdentityColumn, + visible: this.isTypeIdentity, + },{ + id: 'seqcycle', label: gettext('Cycled'), type: 'switch', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + deps: ['attidentity', 'colconstype'], disabled: this.isIdentityColumn, + visible: this.isTypeIdentity, + },{ + id: 'genexpr', label: gettext('Expression'), type: 'text', + mode: ['properties', 'create', 'edit'], group: gettext('Constraints'), + min_version: 120000, deps: ['colconstype'], visible: this.isTypeGenerated, + readonly: function(state) { + return !this.isNew(state); + }, + },{ + id: 'is_pk', label: gettext('Primary key?'), + type: 'switch', mode: ['properties'], + group: gettext('Definition'), + },{ + id: 'is_fk', label: gettext('Foreign key?'), + type: 'switch', mode: ['properties'], + group: gettext('Definition'), + },{ + id: 'is_inherited', label: gettext('Inherited?'), + type: 'switch', mode: ['properties'], + group: gettext('Definition'), + },{ + id: 'tbls_inherited', label: gettext('Inherited from table(s)'), + type: 'text', mode: ['properties'], deps: ['is_inherited'], + group: gettext('Definition'), + visible: function(state) { + return !_.isUndefined(state.is_inherited) && state.is_inherited; + }, + },{ + id: 'is_sys_column', label: gettext('System column?'), cell: 'text', + type: 'switch', mode: ['properties'], + },{ + id: 'description', label: gettext('Comment'), cell: 'text', + type: 'multiline', mode: ['properties', 'create', 'edit'], + disabled: this.inCatalog, + },{ + id: 'attoptions', label: gettext('Variables'), type: 'collection', + group: gettext('Variables'), + schema: new VariableSchema([], null, null, ['name', 'value']), + uniqueCol : ['name'], mode: ['edit', 'create'], + canAdd: true, canEdit: false, canDelete: true, + }, { + id: 'attacl', label: gettext('Privileges'), type: 'collection', + group: gettext('Security'), + schema: this.getPrivilegeRoleSchema(['a','r','w','x']), + mode: ['edit'], canAdd: true, canDelete: true, + uniqueCol : ['grantee'], + },{ + id: 'seclabels', label: gettext('Security labels'), canAdd: true, + schema: new SecLabelSchema(), group: gettext('Security'), + mode: ['edit', 'create'], editable: false, type: 'collection', + min_version: 90100, canEdit: false, canDelete: true, + uniqueCol : ['provider'], + }]; + } + + validate(state, setError) { + var msg = undefined; + + if (!_.isUndefined(state.cltype) && !isEmptyString(state.attlen)) { + // Validation for Length field + if (state.attlen < state.min_val_attlen) + msg = gettext('Length/Precision should not be less than: ') + state.min_val_attlen; + if (state.attlen > state.max_val_attlen) + msg = gettext('Length/Precision should not be greater than: ') + state.max_val_attlen; + // If we have any error set then throw it to user + if(msg) { + setError('attlen', msg); + return true; + } + } + + if (!_.isUndefined(state.cltype) && !isEmptyString(state.attprecision)) { + // Validation for precision field + if (state.attprecision < state.min_val_attprecision) + msg = gettext('Scale should not be less than: ') + state.min_val_attprecision; + if (state.attprecision > state.max_val_attprecision) + msg = gettext('Scale should not be greater than: ') + state.max_val_attprecision; + // If we have any error set then throw it to user + if(msg) { + setError('attprecision', msg); + return true; + } + } + + if (state.colconstype == 'g' && isEmptyString(state.genexpr)) { + msg = gettext('Expression value cannot be empty.'); + setError('genexpr', msg); + return true; + } + + if (!this.isNew(state) && state.colconstype == 'i' + && (this.origData.attidentity == 'a' || this.origData.attidentity == 'd') + && (state.attidentity == 'a' || state.attidentity == 'd')) { + if(isEmptyString(state.seqincrement)) { + msg = gettext('Increment value cannot be empty.'); + setError('seqincrement', msg); + return true; + } + + if(isEmptyString(state.seqmin)) { + msg = gettext('Minimum value cannot be empty.'); + setError('seqmin', msg); + return true; + } + + if(isEmptyString(state.seqmax)) { + msg = gettext('Maximum value cannot be empty.'); + setError('seqmax', msg); + return true; + } + + if(isEmptyString(state.seqcache)) { + msg = gettext('Cache value cannot be empty.'); + setError('seqcache', msg); + return true; + } + } + + if (isEmptyString(state.seqmin) || isEmptyString(state.seqmax)) + return false; + + if ((state.seqmin == 0 && state.seqmax == 0) || + (parseInt(state.seqmin, 10) >= parseInt(state.seqmax, 10))) { + setError('seqmin', gettext('Minimum value must be less than maximum value.')); + return true; + } + + if (state.seqstart && state.seqmin && parseInt(state.seqstart) < parseInt(state.seqmin)) { + setError('seqstart', gettext('Start value cannot be less than minimum value.')); + return true; + } + + if (state.seqstart && state.seqmax && parseInt(state.seqstart) > parseInt(state.seqmax)) { + setError('seqstart', gettext('Start value cannot be greater than maximum value.')); + return true; + } + + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js index 5ec59f7b9..1e1454c7d 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.js @@ -7,6 +7,8 @@ // ////////////////////////////////////////////////////////////// +import CheckConstraintSchema from './check_constraint.ui'; + // Check Constraint Module: Node define('pgadmin.node.check_constraint', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', @@ -90,108 +92,9 @@ define('pgadmin.node.check_constraint', [ }, }, canDrop: schemaChildTreeNode.isTreeItemOfChildOfSchema, - model: pgAdmin.Browser.Node.Model.extend({ - idAttribute: 'oid', - defaults: { - name: undefined, - oid: undefined, - description: undefined, - consrc: undefined, - connoinherit: undefined, - convalidated: true, - }, - // Check Constraint Schema - schema: [{ - id: 'name', label: gettext('Name'), type:'text', cell:'string', - mode: ['properties', 'create', 'edit'], editable:true, - cellHeaderClasses:'width_percent_40', - },{ - id: 'oid', label: gettext('OID'), cell: 'string', - type: 'text' , mode: ['properties'], - },{ - id: 'is_sys_obj', label: gettext('System check constraint?'), - cell:'boolean', type: 'switch', mode: ['properties'], - },{ - id: 'comment', label: gettext('Comment'), type: 'multiline', cell: - 'string', mode: ['properties', 'create', 'edit'], - deps:['name'], disabled:function(m) { - var name = m.get('name'); - if (!(name && name != '')) { - setTimeout(function(){ - if(m.get('comment') && m.get('comment') !== '') - m.set('comment', null); - },10); - return true; - } else { - return false; - } - }, - },{ - id: 'consrc', label: gettext('Check'), type: 'multiline', cell: - 'string', group: gettext('Definition'), mode: ['properties', 'create', 'edit'], - readonly: 'isReadonly', editable: false, - },{ - id: 'connoinherit', label: gettext('No inherit?'), type: - 'switch', cell: 'boolean', group: gettext('Definition'), mode: - ['properties', 'create', 'edit'], min_version: 90200, - disabled: function(m) { - // Disabled if table is a partitioned table. - if ((_.has(m , 'top') && !_.isUndefined(m.top) && m.top.get('is_partitioned')) || - (_.has(m, 'node_info') && _.has(m.node_info, 'table') && - _.has(m.node_info.table, 'is_partitioned') && m.node_info.table.is_partitioned) - ){ - setTimeout(function(){ - m.set('connoinherit', false); - },10); - - return true; - } - - return false; - }, - readonly: 'isReadonly', - },{ - id: 'convalidated', label: gettext('Don\'t validate?'), type: 'switch', cell: - 'boolean', group: gettext('Definition'), min_version: 90200, - disabled: function(m) { - if ((_.isFunction(m.isNew) && !m.isNew()) || - (_.has(m, 'handler') && - !_.isUndefined(m.handler) && - !_.isUndefined(m.get('oid')))) { - - return !m.get('convalidated'); - } else { - return false; - } - }, - mode: ['properties', 'create', 'edit'], - }], - // Client Side Validation - validate: function() { - var err = {}, - errmsg; - - if (_.isUndefined(this.get('consrc')) || String(this.get('consrc')).replace(/^\s+|\s+$/g, '') == '') { - err['consrc'] = gettext('Check cannot be empty.'); - errmsg = err['consrc']; - } - - this.errorModel.clear().set(err); - - if (_.size(err)) { - this.trigger('on-status', {msg: errmsg}); - return errmsg; - } - - return null; - - }, - isReadonly: function(m) { - return ((_.has(m, 'handler') && - !_.isUndefined(m.handler) && - !_.isUndefined(m.get('oid'))) || (_.isFunction(m.isNew) && !m.isNew())); - }, - }), + getSchema: function(){ + return new CheckConstraintSchema(); + }, // Below function will enable right click menu for creating check constraint. canCreate: function(itemData, item, data) { // If check is false then , we will allow create menu diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js new file mode 100644 index 000000000..6730e31ff --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui.js @@ -0,0 +1,103 @@ +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import { isEmptyString } from 'sources/validators'; +export default class CheckConstraintSchema extends BaseUISchema { + constructor() { + super({ + name: undefined, + oid: undefined, + description: undefined, + consrc: undefined, + connoinherit: undefined, + convalidated: true, + }); + } + + get idAttribute() { + return 'oid'; + } + + get inTable() { + if(_.isUndefined(this.nodeInfo)) { + return true; + } + return !_.isUndefined(this.nodeInfo['table']); + } + + isReadonly(state) { + // If we are in table edit mode then + if(this.top) { + return !_.isUndefined(state.oid); + } + return !this.isNew(state); + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), type:'text', cell:'text', + mode: ['properties', 'create', 'edit'], editable:true, + },{ + id: 'oid', label: gettext('OID'), cell: 'text', + type: 'text' , mode: ['properties'], + },{ + id: 'is_sys_obj', label: gettext('System check constraint?'), + cell:'boolean', type: 'switch', mode: ['properties'], + },{ + id: 'comment', label: gettext('Comment'), type: 'multiline', cell: 'text', + mode: ['properties', 'create', 'edit'], + deps:['name'], disabled: function(state) { + if(isEmptyString(state.name)) { + return true; + } + return false; + }, + depChange: (state)=>{ + if(isEmptyString(state.name)) { + return {comment: ''}; + } + }, + },{ + id: 'consrc', label: gettext('Check'), type: 'multiline', cell: 'text', + group: gettext('Definition'), mode: ['properties', 'create', 'edit'], + readonly: obj.isReadonly, editable: false, noEmpty: true, + },{ + id: 'connoinherit', label: gettext('No inherit?'), type: 'switch', cell: 'switch', + group: gettext('Definition'), mode: ['properties', 'create', 'edit'], min_version: 90200, + deps: [['is_partitioned']], + disabled: function() { + // Disabled if table is a partitioned table. + if(obj.inTable && obj.top?.sessData.is_partitioned) { + return true; + } + return false; + }, + depChange: ()=>{ + if(obj.inTable && obj.top?.sessData.is_partitioned) { + return {connoinherit: false}; + } + }, + readonly: obj.isReadonly, + },{ + id: 'convalidated', label: gettext('Don\'t validate?'), type: 'switch', cell: 'switch', + group: gettext('Definition'), min_version: 90200, + readonly: (state)=>{ + // If we are in table edit mode then + if(obj.inTable && obj.top && !obj.top.isNew()) { + return !(_.isUndefined(state.oid) || state.convalidated); + } + if(!obj.isNew(state) && !obj.origData.convalidated) { + return true; + } + return false; + }, + mode: ['properties', 'create', 'edit'], + }]; + } + + validate() { + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js index 2ac92bf14..416ce41a2 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.js @@ -7,634 +7,14 @@ // ////////////////////////////////////////////////////////////// +import { getNodeExclusionConstraintSchema } from './exclusion_constraint.ui'; + define('pgadmin.node.exclusion_constraint', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone', - 'sources/pgadmin', 'pgadmin.browser', 'alertify', 'pgadmin.backform', - 'pgadmin.backgrid', 'pgadmin.browser.collection', + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection', ], function( - gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Alertify, Backform, - Backgrid + gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser ) { - - var ExclusionConstraintColumnModel = pgBrowser.Node.Model.extend({ - defaults: { - column: undefined, - is_exp: false, - oper_class: undefined, - order: false, - nulls_order: false, - operator:undefined, - col_type:undefined, - is_sort_nulls_applicable: true, - }, - toJSON: function () { - var d = pgBrowser.Node.Model.prototype.toJSON.apply(this, arguments); - delete d.col_type; - return d; - }, - schema: [{ - id: 'column', label: gettext('Col/Exp'), type:'text', editable: false, - cell:'string', - },{ - id: 'is_exp', label: '', type:'boolean', editable: false, - cell: Backgrid.StringCell.extend({ - formatter: { - fromRaw: function (rawValue) { - return rawValue ? 'E' : 'C'; - }, - toRaw: function (val) { - return val; - }, - }, - }), visible: false, - },{ - id: 'oper_class', label: gettext('Operator class'), type:'text', - node: 'table', url: 'get_oper_class', first_empty: true, - editable: function(m) { - if (m instanceof Backbone.Collection) { - return true; - } else if ((_.has(m.collection, 'handler') && - !_.isUndefined(m.collection.handler) && - !_.isUndefined(m.collection.handler.get('oid')))) { - return false; - } else if (_.has(m.collection, 'handler') && - !_.isUndefined(m.collection.handler) && - !_.isUndefined(m.collection.handler.get('amname')) && - m.collection.handler.get('amname') != 'btree') { - // Disable if access method is not btree - return false; - } - return true; - }, - select2: { - allowClear: true, width: 'style', tags: true, - placeholder: gettext('Select the operator class'), - }, cell: Backgrid.Extension.Select2Cell.extend({ - initialize: function () { - Backgrid.Extension.Select2Cell.prototype.initialize.apply(this, arguments); - - var self = this, - url = self.column.get('url') || self.defaults.url, - m = self.model, - indextype = self.model.collection.handler.get('amname'); - - if (url && (indextype == 'btree' || _.isUndefined(indextype) || - _.isNull(indextype) || indextype == '')) { - // Set sort_order and nulls to true if access method is btree - setTimeout(function() { - m.set('order', true); - m.set('nulls_order', true); - }, 10); - - var node = this.column.get('schema_node'), - eventHandler = m.top || m, - node_info = this.column.get('node_info'), - full_url = node.generate_url.apply( - node, [ - null, url, this.column.get('node_data'), - this.column.get('url_with_id') || false, node_info, - ]), - data = []; - - indextype = 'btree'; - - if (this.column.get('version_compatible')) { - eventHandler.trigger('pgadmin:view:fetching', m, self.column); - $.ajax({ - async: false, - data : {indextype:indextype}, - url: full_url, - }) - .done(function(res) { - data = res.data; - self.column.set('options', data); - }) - .fail(function() { - eventHandler.trigger('pgadmin:view:fetch:error', m, self.column); - }); - eventHandler.trigger('pgadmin:view:fetched', m, self.column); - } - } else { - self.column.set('options', []); - } - }, - }), - },{ - id: 'order', label: gettext('DESC'), type: 'switch', - options: { - onText: 'ASC', - offText: 'DESC', - }, - editable: 'isEditable', - },{ - id: 'nulls_order', label: gettext('NULLs order'), type:'switch', - options: { - onText: 'FIRST', - offText: 'LAST', - }, - editable: 'isEditable', - },{ - id: 'operator', label: gettext('Operator'), type: 'text', - node: 'table', url: 'get_operator', - editable: function(m) { - if (m instanceof Backbone.Collection) { - return true; - } - if ((_.has(m.collection, 'handler') && - !_.isUndefined(m.collection.handler) && - !_.isUndefined(m.collection.handler.get('oid')))) { - return false; - } - return true; - }, - select2: { - allowClear: false, width: 'style', - }, cell: Backgrid.Extension.Select2Cell.extend({ - initialize: function () { - Backgrid.Extension.Select2Cell.prototype.initialize.apply(this, arguments); - - var self = this, - url = self.column.get('url') || self.defaults.url, - m = self.model, - col_type = self.model.get('col_type'); - - self.column.set('options', []); - - if (url) { - var node = this.column.get('schema_node'), - eventHandler = m.top || m, - node_info = this.column.get('node_info'), - full_url = node.generate_url.apply( - node, [ - null, url, this.column.get('node_data'), - this.column.get('url_with_id') || false, node_info, - ]), - data = []; - - if (this.column.get('version_compatible')) { - eventHandler.trigger('pgadmin:view:fetching', m, self.column); - $.ajax({ - async: false, - data : {col_type:col_type}, - url: full_url, - }) - .done(function(res) { - data = res.data; - self.column.set('options', data); - }) - .fail(function() { - eventHandler.trigger('pgadmin:view:fetch:error', m, self.column); - }); - eventHandler.trigger('pgadmin:view:fetched', m, self.column); - } - } - }, - }), - }, - ], - isEditable: function(m) { - if (m instanceof Backbone.Collection) { - return true; - } else if ((_.has(m.collection, 'handler') && - !_.isUndefined(m.collection.handler) && - !_.isUndefined(m.collection.handler.get('oid')))) { - return false; - } else if (m.top.get('amname') === 'btree') { - m.set('is_sort_nulls_applicable', true); - return true; - } else { - m.set('is_sort_nulls_applicable', false); - return false; - } - }, - validate: function() { - this.errorModel.clear(); - var operator = this.get('operator'), - column_name = this.get('column'), - is_exp = this.get('is_exp'); - if (_.isUndefined(operator) || _.isNull(operator)) { - var msg = gettext('Please specify operator for column: ') + column_name; - if(is_exp) msg = gettext('Please specify operator for expression: ') + column_name; - this.errorModel.set('operator', msg); - return msg; - } - return null; - }, - }); - - var ExclusionConstraintColumnControl = Backform.ExclusionConstraintColumnControl = - Backform.UniqueColCollectionControl.extend({ - - initialize: function() { - Backform.UniqueColCollectionControl.prototype.initialize.apply( - this, arguments - ); - - var self = this, - node = 'exclusion_constraint', - headerSchema = [{ - id: 'is_exp', label: gettext('Is expression ?'), type: 'switch', - control: 'switch', controlLabelClassName: 'control-label pg-el-sm-4 pg-el-12', - controlsClassName: 'pgadmin-controls pg-el-sm-6 pg-el-12', - },{ - id: 'column', label: gettext('Column'), type:'text', - controlLabelClassName: 'control-label pg-el-sm-4 pg-el-12', - controlsClassName: 'pgadmin-controls pg-el-sm-6 pg-el-12', - node: 'column', deps: ['is_exp'], - control: Backform.NodeListByNameControl.extend({ - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - if(self.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - this.listenTo(tableCols, 'remove' , this.removeColumn); - this.listenTo(tableCols, 'change:name', this.resetColOptions); - this.listenTo(tableCols, 'change:cltype', this.resetColOptions); - } - this.custom_options(); - } else { - Backform.NodeListByNameControl.prototype.initialize.apply(this, arguments); - } - }, - removeColumn: function () { - var that = this; - setTimeout(function () { - that.custom_options(); - that.render.apply(that); - }, 50); - }, - resetColOptions: function(m) { - var that = this; - - if (m.previous('name') == self.headerData.get('column')) { - /* - * Table column name has changed so update - * column name in exclusion constraint as well. - */ - self.headerData.set( - {'column': m.get('name')}); - self.headerDataChanged(); - } - - setTimeout(function () { - that.custom_options(); - that.render.apply(that); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = self.model.top.get('columns'), - added_columns_from_tables = [], - col_types = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push({ - label: col, value: col, image:'icon-column', - }); - col_types.push({name:col, type:m.get('cltype')}); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - self.field.set('col_types', col_types); - }, - remove: function () { - if(self.model.handler) { - var tableCols = self.model.top.get('columns'); - this.stopListening(tableCols, 'remove' , this.removeColumn); - this.stopListening(tableCols, 'change:name' , this.resetColOptions); - this.stopListening(tableCols, 'change:cltype' , this.resetColOptions); - - Backform.Select2Control.prototype.remove.apply(this, arguments); - - } else { - Backform.NodeListByNameControl.prototype.remove.apply(this, arguments); - } - }, - template: _.template([ - '<%=label%>', - '
', - ' ', - '
'].join('\n')), - }), - transform: function(rows) { - // This will only get called in case of NodeListByNameControl. - - var that = this, - schema_node = that.field.get('schema_node'), - res = [], - col_types = [], - filter = that.field.get('filter') || function() { return true; }; - - filter = filter.bind(that); - - _.each(rows, function(r) { - if (filter(r)) { - var l = (_.isFunction(schema_node['node_label']) ? - (schema_node['node_label']).apply(schema_node, [r, that.model, that]) : - r.label), - image = (_.isFunction(schema_node['node_image']) ? - (schema_node['node_image']).apply( - schema_node, [r, that.model, that] - ) : - (schema_node['node_image'] || ('icon-' + schema_node.type))); - res.push({ - 'value': r.label, - 'image': image, - 'label': l, - }); - col_types.push({name:r.label, type:r.datatype}); - } - }); - self.field.set('col_types', col_types); - return res; - }, - canAdd: function(m) { - return !((_.has(m, 'handler') && - !_.isUndefined(m.handler) && - !_.isUndefined(m.get('oid'))) || (_.isFunction(m.isNew) && !m.isNew())); - }, - select2: { - allowClear: false, width: 'style', - placeholder: gettext('Select column'), - }, first_empty: !self.model.isNew(), - readonly: function() { - return !_.isUndefined(self.model.get('oid')); - }, - disabled: function(m) { - return m.get('is_exp'); - }, - },{ - id: 'exp', label: gettext('Expression'), type: 'text', - editable: true, deps: ['is_exp'], - controlLabelClassName: 'control-label pg-el-sm-4 pg-el-12', - controlsClassName: 'pgadmin-controls pg-el-sm-6 pg-el-12', - disabled: function(m) { - return !m.get('is_exp'); - }, - }], - headerDefaults = {is_exp: false, column: null, exp: null}, - - gridCols = ['is_exp', 'column', 'oper_class', 'order', 'nulls_order', 'operator']; - - self.headerData = new (Backbone.Model.extend({ - defaults: headerDefaults, - schema: headerSchema, - }))({}); - - var headerGroups = Backform.generateViewSchema( - self.field.get('node_info'), self.headerData, 'create', - node, self.field.get('node_data') - ), - fields = []; - - _.each(headerGroups, function(o) { - fields = fields.concat(o.fields); - }); - - self.headerFields = new Backform.Fields(fields); - self.gridSchema = Backform.generateGridColumnsFromModel( - self.field.get('node_info'), self.field.get('model'), 'edit', gridCols, self.field.get('schema_node') - ); - - self.controls = []; - self.listenTo(self.headerData, 'change', self.headerDataChanged); - self.listenTo(self.headerData, 'select2', self.headerDataChanged); - self.listenTo(self.collection, 'add', self.onAddorRemoveColumns); - self.listenTo(self.collection, 'remove', self.onAddorRemoveColumns); - }, - - generateHeader: function(data) { - var isNew = _.isUndefined(this.model.get('oid')); - var header = [ - '
', - '
', - '
', - '
', - '
', - '
', - '
'].join('\n'); - - var self = this, - headerTmpl = _.template(header), - $header = $(headerTmpl(data)), - controls = this.controls; - - this.headerFields.each(function(field) { - var control = new (field.get('control'))({ - field: field, - model: self.headerData, - }); - - $header.find('div[header="' + field.get('name') + '"]').append( - control.render().$el - ); - - controls.push(control); - }); - - // We should not show in properties mode - if (data.mode == 'properties') { - $header.html(''); - } - - self.$header = $header; - - return $header; - }, - - events: _.extend( - {}, Backform.UniqueColCollectionControl.prototype.events, - {'click button.add': 'addColumns'} - ), - - showGridControl: function(data) { - var self = this, - titleTmpl = _.template([ - '
', - ' ', - ' ', - '
'].join('\n')), - $gridBody = - $('
').append( - // Append titleTmpl only if create/edit mode - data.mode !== 'properties' ? titleTmpl({label: data.label, canAdd: data.canAdd}) : '' - ); - - $gridBody.append(self.generateHeader(data)); - - var gridColumns = _.clone(this.gridSchema.columns); - - // Insert Delete Cell into Grid - if (data.disabled == false && data.canDelete) { - gridColumns.unshift({ - name: 'pg-backform-delete', label: '', - cell: Backgrid.Extension.DeleteCell, - editable: false, cell_priority: -1, - }); - } - - if (self.grid) { - self.grid.remove(); - self.grid = null; - } - // Initialize a new Grid instance - var grid = self.grid = new Backgrid.Grid({ - columns: gridColumns, - collection: self.collection, - className: 'backgrid table-bordered table-noouter-border table-hover', - }); - self.$grid = grid.render().$el; - - $gridBody.append(self.$grid); - - setTimeout(function() { - self.headerData.set({ - 'column': self.$header.find( - 'div[header="column"] select' - ).val(), - }, {silent:true} - ); - }, 10); - - // Remove unwanted class from grid to display it properly - if(data.mode === 'properties') - $gridBody.find('.subnode-header-form').removeClass('subnode-header-form'); - - // Render node grid - self.$gridBody = $gridBody; - return $gridBody; - }, - - headerDataChanged: function() { - var self = this, val, - data = this.headerData.toJSON(), - inSelected = false, - checkVars = ['column']; - - if (!self.$header) { - return; - } - - if (self.control_data.canAdd) { - if(data['is_exp']) { - inSelected = false; - } else { - self.collection.each(function(m) { - if (!inSelected) { - _.each(checkVars, function(v) { - if (!inSelected) { - val = m.get(v); - inSelected = (( - (_.isUndefined(val) || _.isNull(val)) && - (_.isUndefined(data[v]) || _.isNull(data[v])) - ) || - (val == data[v])); - } - }); - } - }); - } - } - else { - inSelected = true; - } - - self.$gridBody.find('button.add').prop('disabled', inSelected); - }, - - addColumns: function(ev) { - ev.preventDefault(); - var self = this; - let newHeaderData = { - is_exp: self.headerData.get('is_exp'), - column: self.headerData.get('is_exp') ? self.headerData.get('exp') : self.headerData.get('column'), - }; - - if (newHeaderData.column && newHeaderData.column != '') { - var coll = self.model.get(self.field.get('name')), - m = new (self.field.get('model'))( - newHeaderData, { - silent: true, top: self.model.top, - collection: coll, handler: coll, - }), - col_types =self.field.get('col_types') || []; - - if(!m.get('is_exp')) { - for(var i=0; i < col_types.length; i++) { - var col_type = col_types[i]; - if (col_type['name'] == m.get('column')) { - m.set({'col_type':col_type['type']}); - break; - } - } - } - - coll.add(m); - - var idx = coll.indexOf(m); - - // idx may not be always > -1 because our UniqueColCollection may - // remove 'm' if duplicate value found. - if (idx > -1) { - self.$grid.find('.new').removeClass('new'); - - var newRow = self.grid.body.rows[idx].$el; - - newRow.addClass('new'); - $(newRow).pgMakeVisible('backform-tab'); - } else { - //delete m; - } - } - - return false; - }, - - onAddorRemoveColumns: function() { - var self = this; - - // Wait for collection to be updated before checking for the button to be - // enabled, or not. - setTimeout(function() { - self.collection.trigger('pgadmin:columns:updated', self.collection); - self.headerDataChanged(); - }, 10); - }, - - remove: function() { - /* - * Stop listening the events registered by this control. - */ - this.stopListening(this.headerData, 'change', this.headerDataChanged); - this.listenTo(this.headerData, 'select2', this.headerDataChanged); - this.listenTo(this.collection, 'remove', this.onAddorRemoveColumns); - - // Remove header controls. - _.each(this.controls, function(controls) { - controls.remove(); - }); - - ExclusionConstraintColumnControl.__super__.remove.apply(this, arguments); - - // Remove the header model - delete (this.headerData); - - }, - }); - // Extend the browser's node class for exclusion constraint node if (!pgBrowser.Nodes['exclusion_constraint']) { pgAdmin.Browser.Nodes['exclusion_constraint'] = pgBrowser.Node.extend({ @@ -652,6 +32,7 @@ define('pgadmin.node.exclusion_constraint', [ hasStatistics: true, statsPrettifyFields: [gettext('Index size')], url_jump_after_node: 'schema', + width: pgBrowser.stdW.md + 'px', Init: function() { /* Avoid multiple registration of menus */ if (this.initialized) @@ -670,374 +51,9 @@ define('pgadmin.node.exclusion_constraint', [ is_not_valid: function(node) { return (node && !node.valid); }, - // Define the model for exclusion constraint node - model: pgAdmin.Browser.Node.Model.extend({ - idAttribute: 'oid', - - defaults: { - name: undefined, - oid: undefined, - is_sys_obj: undefined, - comment: undefined, - spcname: undefined, - amname: 'gist', - fillfactor: undefined, - condeferrable: undefined, - condeferred: undefined, - columns: [], - include: [], - }, - - // Define the schema for the exclusion constraint node - schema: [{ - id: 'name', label: gettext('Name'), type: 'text', - mode: ['properties', 'create', 'edit'], editable: true, - },{ - id: 'oid', label: gettext('OID'), cell: 'string', - type: 'text' , mode: ['properties'], - },{ - id: 'is_sys_obj', label: gettext('System exclusion constraint?'), - cell:'boolean', type: 'switch', mode: ['properties'], - },{ - id: 'comment', label: gettext('Comment'), cell: 'string', - type: 'multiline', mode: ['properties', 'create', 'edit'], - deps:['name'], disabled:function(m) { - var name = m.get('name'); - if (!(name && name != '')) { - setTimeout(function(){ - if(m.get('comment') && m.get('comment') !== '') - m.set('comment', null); - },10); - return true; - } else { - return false; - } - }, - },{ - id: 'spcname', label: gettext('Tablespace'), - type: 'text', group: gettext('Definition'), - control: 'node-list-by-name', node: 'tablespace', - select2:{allowClear:false}, - filter: function(m) { - // Don't show pg_global tablespace in selection. - return (m.label !== 'pg_global'); - }, - },{ - id: 'amname', label: gettext('Access method'), - type: 'text', group: gettext('Definition'), - url:'get_access_methods', node: 'table', - control: Backform.NodeAjaxOptionsControl.extend({ - // When access method changes we need to clear columns collection - onChange: function() { - Backform.NodeAjaxOptionsControl.prototype.onChange.apply(this, arguments); - var self = this, - // current access method - current_am = self.model.get('amname'), - // previous access method - previous_am = self.model.previous('amname'), - column_collection = self.model.get('columns'); - - if (column_collection.length > 0 && current_am != previous_am) { - var msg = gettext('Changing access method will clear columns collection'); - Alertify.confirm(msg, function () { - // User clicks Ok, lets clear collection. - column_collection.each(function(m) { - /* - * Our datamodel do not support collection reset method. - * So remove model one by one. - */ - column_collection.remove(m); - }); - setTimeout(function() { - column_collection.trigger('pgadmin:columns:updated', column_collection); - }, 10); - - }, function() { - // User clicks Cancel set previous value again in combo box - setTimeout(function(){ - self.model.set('amname', previous_am); - }, 10); - }); - } - }, - }), - select2:{allowClear:true}, - readonly: 'isReadonly', - },{ - id: 'fillfactor', label: gettext('Fill factor'), - type: 'int', group: gettext('Definition'), allowNull: true, - },{ - id: 'condeferrable', label: gettext('Deferrable?'), - type: 'switch', group: gettext('Definition'), deps: ['index'], - readonly: 'isReadonly', - },{ - id: 'condeferred', label: gettext('Deferred?'), - type: 'switch', group: gettext('Definition'), - deps: ['condeferrable'], - disabled: function(m) { - // Disable if condeferred is false or unselected. - if(m.get('condeferrable') == true) { - return false; - } else { - setTimeout(function(){ - if(m.get('condeferred')) - m.set('condeferred', false); - },10); - return true; - } - }, - readonly: function(m) { - if((_.has(m, 'handler') && - !_.isUndefined(m.handler) && - !_.isUndefined(m.get('oid'))) || (_.isFunction(m.isNew) && !m.isNew())) { - return true; - } - }, - },{ - id: 'indconstraint', label: gettext('Constraint'), cell: 'string', - type: 'multiline', mode: ['create', 'edit', 'properties'], editable: false, - group: gettext('Definition'), readonly: 'isReadonly', - },{ - id: 'columns', label: gettext('Columns/Expressions'), - type: 'collection', group: gettext('Columns'), - deps:['amname'], canDelete: true, editable: false, - canAdd: function(m) { - // We can't update columns of existing exclusion constraint. - return !((_.has(m, 'handler') && - !_.isUndefined(m.handler) && - !_.isUndefined(m.get('oid'))) || (_.isFunction(m.isNew) && !m.isNew())); - }, - control: ExclusionConstraintColumnControl, - model: ExclusionConstraintColumnModel, - readonly: 'isReadonly', - cell: Backgrid.StringCell.extend({ - initialize: function() { - Backgrid.StringCell.prototype.initialize.apply(this, arguments); - var self = this; - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - - self.listenTo(tableCols, 'remove' , self.removeColumn); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - self.listenTo(tableCols, 'change:cltype', self.removeColumnWithType); - } - - this.model.get('columns').on('pgadmin:columns:updated', function() { - self.render.apply(self); - }); - }, - removeColumnWithType: function(m){ - var self = this, - cols = self.model.get('columns'), - removedCols = cols.where( - {col_type: m.previous('cltype')} - ); - - cols.remove(removedCols); - setTimeout(function () { - self.render(); - }, 10); - - setTimeout(function () { - var constraints = self.model.top.get('exclude_constraint'); - var removed = []; - constraints.each(function(constraint) { - if (constraint.get('columns').length == 0) { - removed.push(constraint); - } - }); - constraints.remove(removed); - },100); - }, - removeColumn: function(m){ - var self = this, - removedCols = self.model.get('columns').where( - {column: m.get('name')} - ); - - self.model.get('columns').remove(removedCols); - setTimeout(function () { - self.render(); - }, 10); - - setTimeout(function () { - var constraints = self.model.top.get('exclude_constraint'); - var removed = []; - constraints.each(function(constraint) { - if (constraint.get('columns').length == 0) { - removed.push(constraint); - } - }); - constraints.remove(removed); - },100); - }, - resetColOptions : function(m) { - var self = this, - updatedCols = self.model.get('columns').where( - {'column': m.previous('name')} - ); - - if (updatedCols.length > 0) { - /* - * Table column name has changed so update - * column name in foreign key as well. - */ - updatedCols[0].set( - {'column': m.get('name')}); - } - - setTimeout(function () { self.render(); }, 10); - }, - formatter: { - fromRaw: function (rawValue) { - return rawValue.pluck('column').toString(); - }, - toRaw: function (val) { - return val; - }, - }, - render: function() { - return Backgrid.StringCell.prototype.render.apply(this, arguments); - }, - remove: function() { - var tableCols = this.model.top.get('columns'), - cols = this.model.get('columns'); - if (cols) { - cols.off('pgadmin:columns:updated'); - } - - this.stopListening(tableCols, 'remove' , self.removeColumn); - this.stopListening(tableCols, 'change:name' , self.resetColOptions); - this.stopListening(tableCols, 'change:cltype' , self.removeColumnWithType); - - Backgrid.StringCell.prototype.remove.apply(this, arguments); - }, - }), - },{ - id: 'include', label: gettext('Include columns'), - type: 'array', group: gettext('Columns'), - editable: false, - canDelete: true, canAdd: true, mode: ['properties', 'create', 'edit'], - visible: function(m) { - /* In table properties, m.node_info is not available */ - m = m.top; - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version >= 110000) - return true; - - return false; - }, - control: Backform.MultiSelectAjaxControl.extend({ - defaults: _.extend( - {}, - Backform.NodeListByNameControl.prototype.defaults, - { - select2: { - allowClear: false, - width: 'style', - multiple: true, - placeholder: gettext('Select the column(s)'), - }, - } - ), - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - var self = this; - if(this.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.resetColOptions); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - self.custom_options(); - } else { - Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments); - } - }, - resetColOptions: function() { - var self = this; - - setTimeout(function () { - self.custom_options(); - self.render.apply(self); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = this.model.top.get('columns'), - added_columns_from_tables = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push( - {label: col, value: col, image:'icon-column'} - ); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - }, - }), - deps: ['index'], node: 'column', - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - var col = m.get('columns'); - col.reset(); - return true; - } - }, - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should be allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update columns of existing index constraint. - return !m.isNew(); - }, - }], - isReadonly: function(m) { - return ((_.has(m, 'handler') && - !_.isUndefined(m.handler) && - !_.isUndefined(m.get('oid'))) || (_.isFunction(m.isNew) && !m.isNew())); - }, - validate: function() { - this.errorModel.clear(); - var columns = this.get('columns'), - name = this.get('name'), - msg; - - if ((_.isUndefined(name) || _.isNull(name) || name.length < 1)) { - msg = gettext('Please specify name for exclusion constraint.'); - this.errorModel.set('name', msg); - return msg; - } else if ( - (_.isUndefined(columns) || _.isNull(columns) || columns.length < 1) - ) { - msg = gettext('Please specify columns for exclusion constraint.'); - this.errorModel.set('columns', msg); - return msg; - } - - return null; - }, - }), + getSchema: function(treeNodeInfo, itemNodeData) { + return getNodeExclusionConstraintSchema(treeNodeInfo, itemNodeData, pgAdmin.Browser); + }, canCreate: function(itemData, item, data) { // If check is false then , we will allow create menu 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 new file mode 100644 index 000000000..e29d6b656 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui.js @@ -0,0 +1,449 @@ +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 { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax'; +import { pgAlertify } from '../../../../../../../../../../static/js/helpers/legacyConnector'; + +export function getNodeExclusionConstraintSchema(treeNodeInfo, itemNodeData, pgBrowser, noColumns=false) { + let tableNode = pgBrowser.Nodes['table']; + return new ExclusionConstraintSchema({ + columns: noColumns ? [] : ()=>getNodeListByName('column', treeNodeInfo, itemNodeData, {includeItemKeys: ['datatype']}), + amname: ()=>getNodeAjaxOptions('get_access_methods', tableNode, treeNodeInfo, itemNodeData), + spcname: ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{ + return (m.label != 'pg_global'); + }), + getOperClass: (urlParams)=>getNodeAjaxOptions('get_oper_class', tableNode, treeNodeInfo, itemNodeData, {urlParams: urlParams}, (data)=>{ + let res = []; + if (data && _.isArray(data)) { + _.each(data, function(d) { + res.push({label: d[0], value: d[1]}); + }); + } + return res; + }), + getOperator: (urlParams)=>getNodeAjaxOptions('get_operator', tableNode, treeNodeInfo, itemNodeData, {urlParams: urlParams}, (data)=>{ + let res = []; + if (data && _.isArray(data)) { + _.each(data, function(d) { + res.push({label: d[0], value: d[1]}); + }); + } + return res; + }), + }); +} + +class ExclusionColHeaderSchema extends BaseUISchema { + constructor(columns) { + super({ + is_exp: undefined, + column: undefined, + expression: undefined, + }); + + this.columns = columns; + } + + changeColumnOptions(columns) { + this.columns = columns; + } + + addDisabled(state) { + return !(state.is_exp ? state.expression : state.column); + } + + /* Data to ExclusionColumnSchema will added using the header form */ + getNewData(data) { + let colType = data.is_exp ? null : _.find(this.columnOptions, (col)=>col.value==data.column)?.datatype; + return this.exColumnSchema.getNewData({ + is_exp: data.is_exp, + column: data.is_exp ? data.expression : data.column, + col_type: colType, + }); + } + + get baseFields() { + return [{ + id: 'is_exp', label: gettext('Is expression'), type:'switch', editable: false, + },{ + id: 'column', label: gettext('Column'), type: 'select', editable: false, + options: this.columns, deps: ['is_exp'], + optionsReloadBasis: this.columns?.map ? _.join(this.columns.map((c)=>c.label), ',') : null, + optionsLoaded: (res)=>this.columnOptions=res, + disabled: (state)=>state.is_exp, + },{ + id: 'expression', label: gettext('Expression'), editable: false, deps: ['is_exp'], + type: 'text', disabled: (state)=>!state.is_exp, + }]; + } +} + +class ExclusionColumnSchema extends BaseUISchema { + constructor(getOperator) { + super({ + column: undefined, + is_exp: false, + oper_class: undefined, + order: false, + nulls_order: false, + operator:undefined, + col_type:undefined, + is_sort_nulls_applicable: false, + }); + + this.operClassOptions = []; + this.operatorOptions = []; + this.getOperator = getOperator; + this.isNewExCons = true; + this.amname = null; + } + + isEditable() { + if(!this.isNewExCons) { + return false; + } else if(this.amname === 'btree') { + return true; + } + return false; + } + + setOperClassOptions(options) { + this.operClassOptions = options; + } + + changeDefaults(data) { + this.defaultColVals = data; + } + + getNewData(data) { + return { + ...super.getNewData(data), + ...this.defaultColVals, + }; + } + + get baseFields() { + let obj = this; + return [{ + id: 'is_exp', label: '', type:'', editable: false, minWidth: 20, + controlProps: { + formatter: { + fromRaw: function (rawValue) { + return rawValue ? 'E' : 'C'; + }, + } + }, visible: false, + },{ + id: 'column', label: gettext('Col/Exp'), type:'', editable: false, + cell:'', minWidth: 125, + },{ + id: 'oper_class', label: gettext('Operator class'), cell:'select', + options: this.operClassOptions, + minWidth: 185, + editable: obj.isEditable, + controlProps: { + allowClear: true, placeholder: gettext('Select the operator class'), + }, + },{ + id: 'order', label: gettext('Order'), type: 'select', cell: 'select', + options: [ + {label: 'ASC', value: true}, + {label: 'DESC', value: false}, + ], + editable: obj.isEditable, minWidth: 110, + controlProps: { + allowClear: false, + }, + },{ + id: 'nulls_order', label: gettext('NULLs order'), type:'select', cell: 'select', + options: [ + {label: 'FIRST', value: true}, + {label: 'LAST', value: false}, + ], controlProps: {allowClear: false}, + editable: obj.isEditable, minWidth: 110, + },{ + id: 'operator', label: gettext('Operator'), type: 'select', + minWidth: 95, + editable: function() { + return obj.isNewExCons; + }, + cell: (state)=>{ + return { + cell: 'select', + options: ()=>obj.getOperator({col_type: state.col_type}), + controlProps: { + allowClear: false, + }, + }; + }, + }]; + } +} + +export default class ExclusionConstraintSchema extends BaseUISchema { + constructor(fieldOptions={}, nodeInfo, getColumns) { + super({ + name: undefined, + oid: undefined, + is_sys_obj: undefined, + comment: undefined, + spcname: undefined, + amname: 'gist', + fillfactor: undefined, + condeferrable: undefined, + condeferred: undefined, + columns: [], + include: [], + }); + + this.nodeInfo = nodeInfo; + this.fieldOptions = fieldOptions; + this.exHeaderSchema = new ExclusionColHeaderSchema(fieldOptions.columns, getColumns); + this.exColumnSchema = new ExclusionColumnSchema(fieldOptions.getOperator); + this.exHeaderSchema.exColumnSchema = this.exColumnSchema; + } + + get idAttribute() { + return 'oid'; + } + + get inTable() { + if(_.isUndefined(this.nodeInfo)) { + return true; + } + return !_.isUndefined(this.nodeInfo['table']); + } + + initialise(data) { + this.exColumnSchema.isNewExCons = this.isNew(data); + this.amname = data.amname; + } + + changeColumnOptions(columns) { + this.exHeaderSchema.changeColumnOptions(columns); + this.fieldOptions.columns = columns; + } + + isReadonly(state) { + // If we are in table edit mode then + if(this.top) { + return !_.isUndefined(state.oid); + } + return !this.isNew(state); + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), type: 'text', cell: 'text', + mode: ['properties', 'create', 'edit'], editable:true, + },{ + id: 'oid', label: gettext('OID'), cell: 'string', + type: 'text' , mode: ['properties'], + },{ + id: 'is_sys_obj', label: gettext('System foreign key?'), + type: 'switch', mode: ['properties'], + },{ + id: 'comment', label: gettext('Comment'), cell: 'text', + type: 'multiline', mode: ['properties', 'create', 'edit'], + deps:['name'], disabled:function(state) { + if(isEmptyString(state.name)) { + return true; + } + return false; + }, depChange: (state)=>{ + if(isEmptyString(state.name)) { + return {comment: ''}; + } + } + },{ + id: 'spcname', label: gettext('Tablespace'), + type: 'select', group: gettext('Definition'), + controlProps: {allowClear:false}, options: this.fieldOptions.spcname, + },{ + id: 'amname', label: gettext('Access method'), + type: 'select', group: gettext('Definition'), + options: this.fieldOptions.amname, + deferredDepChange: (state, source, topState, actionObj)=>{ + return new Promise((resolve)=>{ + pgAlertify().confirm( + gettext('Change access method?'), + gettext('Changing access method will clear columns collection'), + function () { + if(state.amname == 'btree' || isEmptyString(state.amname)) { + let indextype = 'btree'; + obj.exColumnSchema.setOperClassOptions(obj.fieldOptions.getOperClass({indextype:indextype})); + obj.exColumnSchema.changeDefaults({ + order: true, + nulls_order: true, + is_sort_nulls_applicable: true, + }); + } else { + obj.exColumnSchema.setOperClassOptions([]); + obj.exColumnSchema.changeDefaults({ + order: false, + nulls_order: false, + is_sort_nulls_applicable: false, + }); + } + obj.exColumnSchema.amname = state.amname; + resolve(()=>({ + columns: [], + })); + }, + function() { + resolve(()=>{ + return { + amname: actionObj.oldState.amname, + }; + }); + } + ); + }); + }, + controlProps: {allowClear:true}, + readonly: obj.isReadonly, + },{ + id: 'fillfactor', label: gettext('Fill factor'), + type: 'int', group: gettext('Definition'), allowNull: true, + },{ + id: 'condeferrable', label: gettext('Deferrable?'), + type: 'switch', group: gettext('Definition'), + readonly: obj.isReadonly, + },{ + id: 'condeferred', label: gettext('Deferred?'), + type: 'switch', group: gettext('Definition'), + deps: ['condeferrable'], + disabled: function(state) { + // Disable if condeferred is false or unselected. + if(state.condeferrable) { + return false; + } else { + return true; + } + }, + readonly: obj.isReadonly, + depChange: (state)=>{ + if(!state.condeferrable) { + return {condeferred: false}; + } + } + },{ + id: 'indconstraint', label: gettext('Constraint'), cell: 'text', + type: 'multiline', mode: ['create', 'edit', 'properties'], editable: false, + group: gettext('Definition'), readonly: obj.isReadonly, + },{ + id: 'columns', label: gettext('Columns/Expressions'), + group: gettext('Columns'), type: 'collection', + mode: ['create', 'edit'], + editable: false, schema: this.exColumnSchema, + headerSchema: this.exHeaderSchema, headerVisible: (state)=>obj.isNew(state), + CustomControl: DataGridViewWithHeaderForm, + uniqueCol: ['column'], + canAdd: false, canDelete: function(state) { + // We can't update columns of existing foreign key. + return obj.isNew(state); + }, + readonly: obj.isReadonly, cell: ()=>({ + cell: '', + controlProps: { + formatter: { + fromRaw: (rawValue)=>{ + var cols = [], + remoteCols = []; + if (rawValue?.length > 0) { + rawValue.forEach((col)=>{ + cols.push(col.local_column); + remoteCols.push(col.referenced); + }); + return '('+cols.join(', ')+') -> ('+ remoteCols.join(', ')+')'; + } + return ''; + }, + } + }, + minWidth: 245, + }), + deps: ()=>{ + let ret = []; + if(obj.inTable) { + ret.push(['columns']); + } + return ret; + }, + depChange: (state, source, topState, actionObj)=>{ + /* If in table, sync up value with columns in table */ + if(obj.inTable && !state) { + /* the FK is removed by some other dep, this can be a no-op */ + return; + } + let currColumns = state.columns || []; + if(obj.inTable && source[0] == 'columns') { + if(actionObj.type == SCHEMA_STATE_ACTIONS.DELETE_ROW) { + let oldColumn = _.get(actionObj.oldState, actionObj.path.concat(actionObj.value)); + currColumns = _.filter(currColumns, (cc)=>cc.local_column != oldColumn.name); + } else if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE) { + let tabColPath = _.slice(actionObj.path, 0, -1); + let oldColName = _.get(actionObj.oldState, tabColPath).name; + let idx = _.findIndex(currColumns, (cc)=>cc.local_column == oldColName); + if(idx > -1) { + currColumns[idx].local_column = _.get(topState, tabColPath).name; + } + } + } + return {columns: currColumns}; + }, + }, + { + id: 'include', label: gettext('Include columns'), group: gettext('Columns'), + type: ()=>({ + type: 'select', + options: this.fieldOptions.columns, + optionsReloadBasis: this.fieldOptions.columns?.map ? _.join(this.fieldOptions.columns.map((c)=>c.label), ',') : null, + controlProps: { + multiple: true, + }, + }), + editable: false, + canDelete: true, canAdd: true, + mode: ['properties', 'create', 'edit'], min_version: 110000, + deps: ['index'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {include: []}; + } + } + }]; + } + + validate(state, setError) { + if ((_.isUndefined(state.columns) || _.isNull(state.columns) || state.columns.length < 1)) { + setError('columns', gettext('Please specify columns for Foreign key.')); + return true; + } + + if (this.isNew(state)){ + if (state.autoindex && isEmptyString(state.coveringindex)) { + setError('coveringindex', gettext('Please specify covering index name.')); + return true; + } + } + + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js index 71b5270f6..75007f685 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.js @@ -7,609 +7,15 @@ // ////////////////////////////////////////////////////////////// +import { getNodeForeignKeySchema } from './foreign_key.ui'; + define('pgadmin.node.foreign_key', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'backbone', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.alertifyjs', - 'pgadmin.backform', 'pgadmin.backgrid', 'pgadmin.browser.collection', + 'pgadmin.browser.collection', ], function( - gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Alertify, Backform, - Backgrid + gettext, url_for, $, _, Backbone, pgAdmin, pgBrowser, Alertify ) { - - var formatNode = function(opt) { - if (!opt.id) { - return opt.text; - } - - var optimage = $(opt.element).data('image'); - - if(!optimage) { - return opt.text; - } else { - return $( - '' + opt.text + '' - ); - } - }, - headerSelectControlTemplate = _.template([ - '
', - ' ', - '
'].join('\n') - ); - - var ForeignKeyColumnModel = pgBrowser.Node.Model.extend({ - defaults: { - local_column: undefined, - references: undefined, - referenced: undefined, - }, - schema: [{ - id: 'local_column', label: gettext('Local'), type:'text', editable: false, - cellHeaderClasses: 'width_percent_35', cell:'string', - headerCell: Backgrid.Extension.CustomHeaderCell, - },{ - id: 'referenced', label: gettext('Referenced'), type: 'text', editable: false, - cell:'string', cellHeaderClasses: 'width_percent_35', - headerCell: Backgrid.Extension.CustomHeaderCell, - },{ - id: 'references_table_name', label: gettext('Referenced Table'), type: 'text', editable: false, - cell:'string', cellHeaderClasses: 'width_percent_30', - headerCell: Backgrid.Extension.CustomHeaderCell, - }], - }); - - var ForeignKeyColumnControl = Backform.ForeignKeyColumnControl = - Backform.UniqueColCollectionControl.extend({ - - initialize: function() { - Backform.UniqueColCollectionControl.prototype.initialize.apply( - this, arguments - ); - - var self = this, - node = 'foreign_key', - headerSchema = [{ - id: 'local_column', label:'', type:'text', - node: 'column', control: Backform.NodeListByNameControl.extend({ - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - if(self.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - this.listenTo(tableCols, 'remove' , this.removeColumn); - this.listenTo(tableCols, 'change:name', this.resetColOptions); - } - - this.custom_options(); - } else { - Backform.NodeListByNameControl.prototype.initialize.apply(this, arguments); - } - }, - removeColumn: function () { - var that = this; - setTimeout(function () { - that.custom_options(); - that.render.apply(that); - }, 50); - }, - resetColOptions: function(m) { - var that = this; - - if (m.previous('name') == self.headerData.get('local_column')) { - /* - * Table column name has changed so update - * column name in foreign key as well. - */ - self.headerData.set( - {'local_column': m.get('name')}); - self.headerDataChanged(); - } - - setTimeout(function () { - that.custom_options(); - that.render.apply(that); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = self.model.top.get('columns'), - added_columns_from_tables = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push( - {label: col, value: col, image:'icon-column'} - ); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - }, - template: headerSelectControlTemplate, - remove: function () { - if(self.model.handler) { - var tableCols = self.model.top.get('columns'); - this.stopListening(tableCols, 'remove' , this.removeColumn); - this.stopListening(tableCols, 'change:name' , this.resetColOptions); - - Backform.Select2Control.prototype.remove.apply(this, arguments); - - } else { - Backform.NodeListByNameControl.prototype.remove.apply(this, arguments); - } - }, - }), - select2: { - allowClear: false, width: 'style', - placeholder: gettext('Select column'), - first_empty: !_.isUndefined(self.model.get('oid')), - }, - version_compatible: self.field.get('version_compatible'), - readonly: function() { - return !_.isUndefined(self.model.get('oid')); - }, - },{ - id: 'references', label:'', type: 'text', cache_level: 'server', - select2: { - allowClear: false, width: 'style', - placeholder: gettext('Select foreign table'), - }, first_empty: true, - control: Backform.NodeListByNameControl.extend({ - formatter: Backform.ControlFormatter, - template: headerSelectControlTemplate, - }), - url: 'all_tables', node: 'table', - version_compatible: self.field.get('version_compatible'), - readonly: function() { - return !_.isUndefined(self.model.get('oid')); - }, - transform: function(rows) { - var res = []; - _.each(rows, function(r) { - res.push({ - 'value': r.value, - 'image': 'icon-table', - 'label': r.label, - }); - }); - return res; - }, - },{ - id: 'referenced', label:'', type: 'text', cache_level: 'server', - transform: function(rows) { - var res = []; - _.each(rows, function(r) { - res.push({ - 'value': r.name, - 'image': 'icon-column', - 'label': r.name, - }); - }); - return res; - }, - control: Backform.Select2Control.extend({ - formatter: Backform.ControlFormatter, - template: headerSelectControlTemplate, - render: function() { - var self_referenced = this, - url = self_referenced.field.get('url') || self_referenced.defaults.url, - m = self_referenced.model, - tid = m.get('references'); - // Store name for selected table - var a = $('select[name="references"]').find(':selected').text(); - this.model.set('references_table_name', a,{silent: true}); - - // Clear any existing value before setting new options. - m.set(self_referenced.field.get('name'), null, {silent: true}); - - if (url && !_.isUndefined(tid) && !_.isNull(tid) && tid != '') { - var schema_node = this.field.get('schema_node'), - node_info = this.field.get('node_info'), - full_url = schema_node.generate_url.apply( - schema_node, [ - null, url, this.field.get('node_data'), - this.field.get('url_with_id') || false, node_info, - ]), - data = []; - - if (this.field.get('version_compatible')) { - m.trigger('pgadmin:view:fetching', m, self_referenced.field); - $.ajax({ - async: false, - data : {tid:tid}, - url: full_url, - }) - .done(function(res) { - data = res.data; - }) - .fail(function() { - m.trigger('pgadmin:view:fetch:error', m, self_referenced.field); - }); - m.trigger('pgadmin:view:fetched', m, self_referenced.field); - } - /* - * Transform the data - */ - var transform = this.field.get('transform') || self_referenced.defaults.transform; - if (transform && _.isFunction(transform)) { - // We will transform the data later, when rendering. - // It will allow us to generate different data based on the - // dependencies. - self_referenced.field.set('options', transform.bind(self_referenced, data)); - } else { - self_referenced.field.set('options', data); - } - } else { - self_referenced.field.set('options', []); - } - Backform.Select2Control.prototype.render.apply(this, arguments); - return this; - }, - }), url: 'get_columns', - select2: { - allowClear: false, - width: 'style', - placeholder: gettext('Select column'), - templateResult: formatNode, - templateSelection: formatNode, - }, - deps:['references'], node: 'table', - version_compatible: self.field.get('version_compatible'), - readonly: function() { - return !_.isUndefined(self.model.get('oid')); - }, - }], - headerDefaults = {local_column: null, - references: null, - referenced:null}, - gridCols = ['local_column', 'references', 'referenced','references_table_name']; - - if ((!self.model.isNew() && _.isUndefined(self.model.handler)) || - (_.has(self.model, 'handler') && - !_.isUndefined(self.model.handler) && - !_.isUndefined(self.model.get('oid')))) { - var column = self.collection.first(); - if (column) { - headerDefaults['references'] = column.get('references'); - } - } - - self.headerData = new (Backbone.Model.extend({ - defaults: headerDefaults, - schema: headerSchema, - }))({}); - - var headerGroups = Backform.generateViewSchema( - self.field.get('node_info'), self.headerData, 'create', - node, self.field.get('node_data') - ), - fields = []; - - _.each(headerGroups, function(o) { - fields = fields.concat(o.fields); - }); - - self.headerFields = new Backform.Fields(fields); - self.gridSchema = Backform.generateGridColumnsFromModel( - //null, ForeignKeyColumnModel, 'edit', gridCols - self.field.get('node_info'), self.field.get('model'), 'edit', - gridCols, self.field.get('schema_node') - ); - - self.controls = []; - self.listenTo(self.headerData, 'change', self.headerDataChanged); - self.listenTo(self.headerData, 'select2', self.headerDataChanged); - self.listenTo(self.collection, 'add', self.onAddorRemoveColumns); - self.listenTo(self.collection, 'remove', self.onAddorRemoveColumns); - }, - - generateHeader: function(data) { - var header = [ - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
', - ' ', - '
', - '
', - '
', - '
', - '
'].join('\n'); - - _.extend(data, { - column_label: gettext('Local column'), - references_label: gettext('References'), - referenced_label: gettext('Referencing'), - }); - - var self = this, - headerTmpl = _.template(header), - $header = $(headerTmpl(data)), - controls = this.controls; - - this.headerFields.each(function(field) { - var control = new (field.get('control'))({ - field: field, - model: self.headerData, - }); - - $header.find('div[header="' + field.get('name') + '"]').append( - control.render().$el - ); - - controls.push(control); - }); - - // We should not show add but in properties mode - if (data.mode == 'properties') { - $header.html(''); - } - - self.$header = $header; - - return $header; - }, - - events: _.extend( - {}, Backform.UniqueColCollectionControl.prototype.events, - {'click button.add': 'addColumns'} - ), - - showGridControl: function(data) { - - var self = this, - titleTmpl = _.template([ - '
', - ' ', - ' ', - '
'].join('\n')), - $gridBody = - $('
').append( - // Append titleTmpl only if create/edit mode - data.mode !== 'properties' ? titleTmpl({label: data.label, canAdd: data.canAdd}) : '' - ); - - // Clean up existing grid if any (in case of re-render) - if (self.grid) { - self.grid.remove(); - } - - $gridBody.append(self.generateHeader(data)); - - var gridSchema = _.clone(this.gridSchema); - - // Insert Delete Cell into Grid - if (data.disabled == false && data.canDelete) { - gridSchema.columns.unshift({ - name: 'pg-backform-delete', label: '', - cell: Backgrid.Extension.DeleteCell, - editable: false, cell_priority: -1, - }); - } - - // Initialize a new Grid instance - var grid = self.grid = new Backgrid.Grid({ - columns: gridSchema.columns, - collection: self.collection, - className: 'backgrid table-bordered table-noouter-border table-hover', - }); - self.$grid = grid.render().$el; - - $gridBody.append(self.$grid); - - setTimeout(function() { - self.headerData.set({ - 'local_column': - self.$header.find( - 'div[header="local_column"] select option:first' - ).val(), - 'referenced': - self.$header.find( - 'div[header="referenced"] select option:first' - ).val(), - 'references': - self.$header.find( - 'div[header="references"] select option:first' - ).val(), - }, {silent:true} - ); - }, 10); - - // Remove unwanted class from grid to display it properly - if(data.mode === 'properties') - $gridBody.find('.subnode-header-form').removeClass('subnode-header-form'); - - // Render node grid - self.$gridBody = $gridBody; - return $gridBody; - }, - - headerDataChanged: function() { - var self = this, val, - data = this.headerData.toJSON(), - inSelected = false, - checkVars = ['local_column', 'referenced']; - - if (!self.$header) { - return; - } - - if (self.control_data.canAdd) { - self.collection.each(function(m) { - if (!inSelected) { - _.each(checkVars, function(v) { - if (!inSelected) { - val = m.get(v); - inSelected = (( - (_.isUndefined(val) || _.isNull(val)) && - (_.isUndefined(data[v]) || _.isNull(data[v])) - ) || - (val == data[v])); - } - }); - } - }); - } - else { - inSelected = true; - } - - self.$gridBody.find('button.add').prop('disabled', inSelected); - }, - - addColumns: function(ev) { - ev.preventDefault(); - var self = this, - local_column = self.headerData.get('local_column'), - referenced = self.headerData.get('referenced'); - - if (local_column && local_column != '' && referenced && referenced != '') { - var m = new (self.field.get('model'))( - self.headerData.toJSON()), - coll = self.model.get(self.field.get('name')); - - coll.add(m); - - var idx = coll.indexOf(m); - - // idx may not be always > -1 because our UniqueColCollection may - // remove 'm' if duplicate value found. - if (idx > -1) { - self.$grid.find('.new').removeClass('new'); - - var newRow = self.grid.body.rows[idx].$el; - - newRow.addClass('new'); - $(newRow).pgMakeVisible('backform-tab'); - } - } - return false; - }, - - onAddorRemoveColumns: function() { - var self = this; - - // Wait for collection to be updated before checking for the button to be - // enabled, or not. - setTimeout(function() { - if (self.collection.length > 0) { - self.$header.find( - 'div[header="references"] select' - ).prop('disabled', true); - } else { - self.$header.find( - 'div[header="references"] select' - ).prop('disabled', false); - } - - self.collection.trigger('pgadmin:columns:updated', self.collection); - - self.headerDataChanged(); - - if ((!_.has(self.model, 'handler') || (_.has(self.model, 'handler') && - _.isUndefined(self.model.handler))) || - (_.has(self.model, 'handler') && !_.isUndefined(self.model.handler) && - !_.isUndefined(self.model.handler.get('oid')))) { - self.getCoveringIndex(); - } - - }, 10); - }, - - getCoveringIndex: function() { - - var self = this, - url = 'get_coveringindex', - m = self.model, - cols = [], - coveringindex = null, - url_jump_after_node = 'schema'; - - self.collection.each(function(local_model){ - cols.push(local_model.get('local_column')); - }); - - if (cols.length > 0) { - var node = this.field.get('schema_node'), - node_info = this.field.get('node_info'), - full_url = node.generate_url.apply( - node, [ - null, url, this.field.get('node_data'), - this.field.get('url_with_id') || false, node_info, url_jump_after_node, - ]); - - if (this.field.get('version_compatible')) { - m.trigger('pgadmin:view:fetching', m, self.field); - $.ajax({ - async: false, - data : {cols:JSON.stringify(cols)}, - url: full_url, - }) - .done(function(res) { - coveringindex = res.data; - }) - .fail(function() { - m.trigger('pgadmin:view:fetch:error', m, self.field); - }); - m.trigger('pgadmin:view:fetched', m, self.field); - } - } - - if (coveringindex) { - m.set('hasindex', true); - m.set('autoindex', false); - m.set('coveringindex', coveringindex); - } else { - m.set('coveringindex', null); - m.set('autoindex', true); - m.set('hasindex', false); - } - }, - - remove: function() { - /* - * Stop listening the events registered by this control. - */ - this.stopListening(this.headerData, 'change', this.headerDataChanged); - this.listenTo(this.headerData, 'select2', this.headerDataChanged); - this.listenTo(this.collection, 'remove', this.onRemoveVariable); - // Remove header controls. - _.each(this.controls, function(controls) { - controls.remove(); - }); - - ForeignKeyColumnControl.__super__.remove.apply(this, arguments); - - // Remove the header model - delete (this.headerData); - - }, - }); - // Extend the browser's node class for foreign key node if (!pgBrowser.Nodes['foreign_key']) { pgAdmin.Browser.Nodes['foreign_key'] = pgBrowser.Node.extend({ @@ -682,425 +88,9 @@ define('pgadmin.node.foreign_key', [ return false; }, }, - // Define the model for foreign key node - model: pgAdmin.Browser.Node.Model.extend({ - idAttribute: 'oid', - - defaults: { - name: undefined, - reftab: undefined, - oid: undefined, - is_sys_obj: undefined, - comment: undefined, - condeferrable: undefined, - condeferred: undefined, - confmatchtype: undefined, - convalidated: undefined, - columns: undefined, - confupdtype: 'a', - confdeltype: 'a', - autoindex: true, - coveringindex: undefined, - hasindex:undefined, - }, - toJSON: function () { - var d = pgAdmin.Browser.Node.Model.prototype.toJSON.apply(this, arguments); - delete d.hasindex; - return d; - }, - // Define the schema for the foreign key node - schema: [{ - id: 'name', label: gettext('Name'), type: 'text', - mode: ['properties', 'create', 'edit'], editable:true, - headerCell: Backgrid.Extension.CustomHeaderCell, cellHeaderClasses: 'width_percent_30', - },{ - id: 'oid', label: gettext('OID'), cell: 'string', - type: 'text' , mode: ['properties'], - },{ - id: 'is_sys_obj', label: gettext('System foreign key?'), - cell:'boolean', type: 'switch', mode: ['properties'], - },{ - id: 'comment', label: gettext('Comment'), cell: 'string', - type: 'multiline', mode: ['properties', 'create', 'edit'], - deps:['name'], disabled:function(m) { - var name = m.get('name'); - if (!(name && name != '')) { - setTimeout(function(){ - if(m.get('comment') && m.get('comment') !== '') - m.set('comment', null); - },10); - return true; - } else { - return false; - } - }, - },{ - id: 'condeferrable', label: gettext('Deferrable?'), - type: 'switch', group: gettext('Definition'), - readonly: 'isReadonly', - },{ - id: 'condeferred', label: gettext('Deferred?'), - type: 'switch', group: gettext('Definition'), - deps: ['condeferrable'], - disabled: function(m) { - // Disable if condeferred is false or unselected. - if(m.get('condeferrable') == true) { - return false; - } else { - setTimeout(function(){ - if(m.get('condeferred')) - m.set('condeferred', false); - },10); - return true; - } - }, - readonly: 'isReadOnly', - },{ - id: 'confmatchtype', label: gettext('Match type'), - type: 'switch', group: gettext('Definition'), - options: { - onText: 'FULL', - offText: 'SIMPLE', - width: '80', - },readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - // We can't update condeferred of existing foreign key. - return !m.isNew(); - }, - },{ - id: 'convalidated', label: gettext('Validated?'), - type: 'switch', group: gettext('Definition'), - options: { - onText: gettext('Yes'), - offText: gettext('No'), - },readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed - return !(_.isUndefined(m.get('oid')) || !m.get('convalidated')); - } - // We can't update condeferred of existing foreign key. - return !(m.isNew() || !m.get('convalidated')); - }, - },{ - id: 'autoindex', label: gettext('Auto FK index?'), - type: 'switch', group: gettext('Definition'), - deps: ['name', 'hasindex'], - options: { - onText: gettext('Yes'), - offText: gettext('No'), - },disabled: function(m) { - // Don't allow to edit the auto index setting in edit mode - if(!m.isNew()) - return true; - var index = m.get('coveringindex'), - autoindex = m.get('autoindex'), - setIndexName = function() { - var name = m.get('name'), - oldindex = 'fki_'+m.previous ('name'); - - if (m.get('hasindex')) { - return true; - } else if (m.get('autoindex') && !_.isUndefined(name) && !_.isNull(name) && - name != '' && (_.isUndefined(index) || _.isNull(index) || - index == '' || index == oldindex)) { - var newIndex = 'fki_' + name; - m.set('coveringindex', newIndex); - return false; - } else { - return false; - } - }; - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - if(_.isUndefined(m.get('oid')) && _.isUndefined(m.handler.get('oid'))) { - setTimeout(function () { - if(m.get('autoindex')) - m.set('autoindex', false); - }, 10); - return true; - } else { - return setIndexName(); - } - } else { - if(!m.isNew() && autoindex && !_.isUndefined(index) && - !_.isNull(index) && index != '' && m.get('hasindex')) { - return true; - } else { - return setIndexName(); - } - } - }, - },{ - id: 'coveringindex', label: gettext('Covering index'), type: 'text', - mode: ['properties', 'create', 'edit'], group: gettext('Definition'), - deps:['autoindex', 'hasindex'], - disabled: function(m) { - var index = m.get('coveringindex'), - setIndexName = function() { - if (m.get('hasindex')) { - return true; - } else if (!m.get('autoindex')) { - setTimeout(function () { - m.set('coveringindex', null); - }); - return true; - } else { - setTimeout(function () { - var name = m.get('name'), - newIndex = 'fki_' + name; - - if (m.get('autoindex') && !_.isUndefined(name) && !_.isNull(name) && - name != '') { - m.set('coveringindex', newIndex); - } - }); - - return false; - } - }; - - if (!m.isNew() && m.get('autoindex') && !_.isUndefined(index) - && _.isNull(index) && index == '') { - return true; - } - - return setIndexName(); - }, - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - // We can't update columns of existing foreign key. - return !m.isNew(); - }, - },{ - id: 'references_table_name', label: gettext('Referenced Table'), - type: 'text', group: gettext('Columns'), - node: 'foreign_key', editable: false,visible:false, - cellHeaderClasses: 'width_percent_30', - cell: Backgrid.StringCell.extend({ - initialize: function() { - Backgrid.StringCell.prototype.initialize.apply(this, arguments); - var self = this, - collection = this.model.get('columns'); - self.model.get('columns').on('pgadmin:columns:updated', function() { - self.render.apply(self); - }); - self.listenTo(collection, 'add', self.render); - self.listenTo(collection, 'remove', self.render); - }, - formatter: { - fromRaw: function (rawValue,model) { - var remote_tables = [], - m = model.get('columns'); - if (m.length > 0) { - m.each(function(col){ - remote_tables.push(col.get('references_table_name')); - }); - return remote_tables; - } - - }, - toRaw: function (val) { return val; }, - }, - render: function() { - return Backgrid.StringCell.prototype.render.apply(this, arguments); - }, - }), - },{ - id: 'columns', label: gettext('Columns'), - type: 'collection', group: gettext('Columns'), - node: 'foreign_key', editable: false, headerCell: Backgrid.Extension.CustomHeaderCell, - cellHeaderClasses: 'width_percent_30', - cell: Backgrid.StringCell.extend({ - initialize: function() { - Backgrid.StringCell.prototype.initialize.apply(this, arguments); - var self = this, - collection = this.model.get('columns'); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.removeColumn); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - self.model.get('columns').on('pgadmin:columns:updated', function() { - self.render.apply(self); - }); - self.listenTo(collection, 'add', self.render); - self.listenTo(collection, 'remove', self.render); - }, - removeColumn: function(m){ - var self = this, - removedCols = self.model.get('columns').where( - {local_column: m.get('name')} - ); - - self.model.get('columns').remove(removedCols); - setTimeout(function () { - self.render(); - }, 10); - - setTimeout(function () { - var constraints = self.model.top.get('foreign_key'); - var removed = []; - constraints.each(function(constraint) { - if (constraint.get('columns').length == 0) { - removed.push(constraint); - } - }); - constraints.remove(removed); - },100); - }, - resetColOptions : function(m) { - var self = this, - updatedCols = self.model.get('columns').where( - {'local_column': m.previous('name')} - ); - if (updatedCols.length > 0) { - /* - * Table column name has changed so update - * column name in foreign key as well. - */ - updatedCols[0].set( - {'local_column': m.get('name')}); - } - - setTimeout(function () { self.render(); }, 10); - }, - formatter: { - fromRaw: function (rawValue) { - var cols = [], - remote_cols = []; - if (rawValue.length > 0) { - rawValue.each(function(col){ - cols.push(col.get('local_column')); - remote_cols.push(col.get('referenced')); - }); - return '('+cols.join(', ')+') -> ('+ remote_cols.join(', ')+')'; - } - return ''; - }, - toRaw: function (val) { return val; }, - }, - render: function() { - return Backgrid.StringCell.prototype.render.apply(this, arguments); - }, - remove: function() { - var tableCols = this.model.top.get('columns'); - - this.stopListening(tableCols, 'remove' , self.removeColumn); - this.stopListening(tableCols, 'change:name' , self.resetColOptions); - - Backgrid.StringCell.prototype.remove.apply(this, arguments); - }, - }), - canAdd: function(m) { - // We can't update columns of existing foreign key. - return m.isNew(); - }, canDelete: true, - control: ForeignKeyColumnControl, - model: ForeignKeyColumnModel, - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - // We can't update columns of existing foreign key. - return !m.isNew(); - }, - },{ - id: 'confupdtype', label: gettext('On update'), - type:'select2', group: gettext('Action'), mode: ['edit','create'], - select2:{width:'50%', allowClear: false}, - options: [ - {label: 'NO ACTION', value: 'a'}, - {label: 'RESTRICT', value: 'r'}, - {label: 'CASCADE', value: 'c'}, - {label: 'SET NULL', value: 'n'}, - {label: 'SET DEFAULT', value: 'd'}, - ],readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - // We can't update confupdtype of existing foreign key. - return !m.isNew(); - }, - },{ - id: 'confdeltype', label: gettext('On delete'), - type:'select2', group: gettext('Action'), mode: ['edit','create'], - select2:{width:'50%', allowClear: false}, - options: [ - {label: 'NO ACTION', value: 'a'}, - {label: 'RESTRICT', value: 'r'}, - {label: 'CASCADE', value: 'c'}, - {label: 'SET NULL', value: 'n'}, - {label: 'SET DEFAULT', value: 'd'}, - ],readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - // We can't update confdeltype of existing foreign key. - return !m.isNew(); - }, - }, - ], - isReadonly:function(m) { - // If we are in table edit mode then - if (_.has(m, 'handler') && !_.isUndefined(m.handler)) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - return !m.isNew(); - }, - validate: function() { - var columns = this.get('columns'), - msg; - - this.errorModel.clear(); - - if ((_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { - msg = gettext('Please specify columns for Foreign key.'); - this.errorModel.set('columns', msg); - return msg; - } - - var coveringindex = this.get('coveringindex'), - autoindex = this.get('autoindex'); - if (this.isNew()){ - if (autoindex && (_.isUndefined(coveringindex) || _.isNull(coveringindex) || - String(coveringindex).replace(/^\s+|\s+$/g, '') == '')) { - msg = gettext('Please specify covering index name.'); - this.errorModel.set('coveringindex', msg); - return msg; - } - } - - return null; - }, - }), - + getSchema: (treeNodeInfo, itemNodeData)=>{ + return getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgAdmin.Browser); + }, canCreate: function(itemData, item, data) { // If check is false then , we will allow create menu if (data && data.check == false) 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 new file mode 100644 index 000000000..4567355d9 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui.js @@ -0,0 +1,405 @@ +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 { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax'; + +export function getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, noColumns=false) { + return new ForeignKeySchema({ + local_column: noColumns ? [] : ()=>getNodeListByName('column', treeNodeInfo, itemNodeData), + references: ()=>getNodeAjaxOptions('all_tables', pgBrowser.Nodes['table'], treeNodeInfo, itemNodeData, {cacheLevel: 'server'}, (rows)=>{ + return rows.map((r)=>({ + 'value': r.value, + 'image': 'icon-table', + 'label': r.label, + oid: r.oid, + })); + }), + }, + treeNodeInfo, + (params)=>{ + return getNodeAjaxOptions('get_columns', pgBrowser.Nodes['table'], treeNodeInfo, itemNodeData, {urlParams: params, useCache:false}, (rows)=>{ + return rows.map((r)=>({ + 'value': r.name, + 'image': 'icon-column', + 'label': r.name, + })); + }); + }); +} + +class ForeignKeyHeaderSchema extends BaseUISchema { + constructor(fieldOptions, getColumns) { + super({ + local_column: undefined, + references: undefined, + referenced: undefined, + }); + + this.fieldOptions = fieldOptions; + this.getColumns = getColumns; + } + + changeColumnOptions(columns) { + this.fieldOptions.local_column = columns; + } + + addDisabled(state) { + return !(state.local_column && state.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)?.label; + return { + local_column: data.local_column, + referenced: data.referenced, + references: data.references, + references_table_name: references_table_name, + }; + } + + get baseFields() { + let obj = this; + return [{ + id: 'local_column', label: gettext('Local column'), type:'select', editable: false, + options: this.fieldOptions.local_column, + optionsReloadBasis: this.fieldOptions.local_column?.map ? _.join(this.fieldOptions.local_column.map((c)=>c.label), ',') : null, + },{ + id: 'references', label: gettext('References'), type: 'select', editable: false, + options: this.fieldOptions.references, + optionsReloadBasis: this.fieldOptions.references?.map ? _.join(this.fieldOptions.references.map((c)=>c.label), ',') : null, + optionsLoaded: (rows)=>obj.refTables=rows, + },{ + id: 'referenced', label: gettext('Referencing'), editable: false, deps: ['references'], + type: (state)=>{ + return { + type: 'select', + options: state.references ? ()=>this.getColumns({tid: state.references}) : [], + optionsReloadBasis: state.references, + }; + }, + }]; + } +} + +class ForeignKeyColumnSchema extends BaseUISchema { + constructor() { + super({ + local_column: undefined, + referenced: undefined, + references: undefined, + references_table_name: undefined, + }); + } + + get baseFields() { + return [{ + id: 'local_column', label: gettext('Local'), type:'text', editable: false, + cell:'', minWidth: 145, + },{ + id: 'referenced', label: gettext('Referenced'), type: 'text', editable: false, + cell:'', minWidth: 145, + },{ + id: 'references_table_name', label: gettext('Referenced Table'), type: 'text', editable: false, + cell:'', minWidth: 145, + }]; + } +} + +export default class ForeignKeySchema extends BaseUISchema { + constructor(fieldOptions={}, nodeInfo, getColumns) { + super({ + name: undefined, + reftab: undefined, + oid: undefined, + is_sys_obj: undefined, + comment: undefined, + condeferrable: undefined, + condeferred: undefined, + confmatchtype: false, + convalidated: undefined, + columns: undefined, + confupdtype: 'a', + confdeltype: 'a', + autoindex: true, + coveringindex: undefined, + hasindex:undefined, + }); + + this.nodeInfo = nodeInfo; + + this.fkHeaderSchema = new ForeignKeyHeaderSchema(fieldOptions, getColumns); + this.fkHeaderSchema.fkObj = this; + this.fkColumnSchema = new ForeignKeyColumnSchema(); + + } + + get idAttribute() { + return 'oid'; + } + + get inTable() { + if(_.isUndefined(this.nodeInfo)) { + return true; + } + return !_.isUndefined(this.nodeInfo['table']); + } + + changeColumnOptions(columns) { + this.fkHeaderSchema.changeColumnOptions(columns); + } + + isReadonly(state) { + // If we are in table edit mode then + if(this.top) { + return !_.isUndefined(state.oid); + } + return !this.isNew(state); + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), type: 'text', cell: 'text', + mode: ['properties', 'create', 'edit'], editable:true, + },{ + id: 'oid', label: gettext('OID'), cell: 'string', + type: 'text' , mode: ['properties'], + },{ + id: 'is_sys_obj', label: gettext('System foreign key?'), + type: 'switch', mode: ['properties'], + },{ + id: 'comment', label: gettext('Comment'), cell: 'text', + type: 'multiline', mode: ['properties', 'create', 'edit'], + deps:['name'], disabled:function(state) { + if(isEmptyString(state.name)) { + return true; + } + return false; + }, depChange: (state)=>{ + if(isEmptyString(state.name)) { + return {comment: ''}; + } + } + },{ + id: 'condeferrable', label: gettext('Deferrable?'), + type: 'switch', group: gettext('Definition'), + readonly: obj.isReadonly, + },{ + id: 'condeferred', label: gettext('Deferred?'), + type: 'switch', group: gettext('Definition'), + deps: ['condeferrable'], + disabled: function(state) { + // Disable if condeferred is false or unselected. + if(state.condeferrable) { + return false; + } else { + return true; + } + }, + readonly: obj.isReadonly, + depChange: (state)=>{ + if(!state.condeferrable) { + return {condeferred: false}; + } + } + },{ + id: 'confmatchtype', label: gettext('Match type'), + type: 'toggle', group: gettext('Definition'), + options: [ + {label: 'FULL', value: true}, + {label: 'SIMPLE', value: false}, + ], readonly: obj.isReadonly, + },{ + id: 'convalidated', label: gettext('Validated?'), + type: 'switch', group: gettext('Definition'), + readonly: (state)=>{ + // If we are in table edit mode then + if(obj.inTable && obj.top && !obj.top.isNew()) { + return !(_.isUndefined(state.oid) || !state.convalidated); + } + if(!obj.isNew(state) && obj.origData.convalidated) { + return true; + } + return false; + }, + },{ + id: 'autoindex', label: gettext('Auto FK index?'), + type: 'switch', group: gettext('Definition'), + deps: ['name', 'hasindex'], + disabled: (state)=>{ + if(!obj.isNew(state)) { + return true; + } + // If we are in table edit mode then + if(obj.inTable) { + // user is trying to add new constraint which should allowed for Unique + if(obj.isNew(state)) { + return true; + } + } else { + if(!obj.isNew(state) && state.autoindex && !isEmptyString(state.coveringindex) + && state.hasindex) { + return true; + } + } + if(state.hasindex) { + return true; + } + return false; + }, + depChange: (state, source, topState, actionObj)=>{ + if(!obj.isNew(state)) { + return {}; + } + // If we are in table edit mode then + if(obj.inTable) { + // user is trying to add new constraint which should allowed for Unique + if(obj.isNew(state)) { + return {autoindex: false, coveringindex: ''}; + } + } + + let oldindex = 'fki_'+actionObj.oldState.name; + if(state.hasindex) { + return {}; + } else if(!state.autoindex) { + return {coveringindex: ''}; + } else if(state.autoindex && !isEmptyString(state.name) && + (isEmptyString(state.coveringindex) || oldindex == state.coveringindex)){ + return {coveringindex: 'fki_'+state.name}; + } + }, + },{ + id: 'coveringindex', label: gettext('Covering index'), type: 'text', + mode: ['properties', 'create', 'edit'], group: gettext('Definition'), + deps:['autoindex', 'hasindex'], + disabled: (state)=>{ + if(!obj.isNew(state) && state.autoindex && !isEmptyString(state.coveringindex)) { + return true; + } + if(state.hasindex) { + return true; + } else if(!state.autoindex) { + return true; + } else { + return false; + } + }, + readonly: this.isReadonly, + },{ + id: 'references_table_name', label: gettext('Referenced Table'), + type: 'text', group: gettext('Columns'), editable: false, visible:false, + cell: '', deps: ['columns'], + depChange: (state)=>{ + if(state.columns?.length > 0) { + return {references_table_name: _.join(_.map(state.columns, 'references_table_name'), ',')}; + } + return {references_table_name: undefined}; + } + },{ + id: 'columns', label: gettext('Columns'), + group: gettext('Columns'), type: 'collection', + mode: ['create', 'edit'], + editable: false, schema: this.fkColumnSchema, + headerSchema: this.fkHeaderSchema, headerVisible: (state)=>obj.isNew(state), + CustomControl: DataGridViewWithHeaderForm, + 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: ()=>({ + cell: '', + controlProps: { + formatter: { + fromRaw: (rawValue)=>{ + var cols = [], + remoteCols = []; + if (rawValue?.length > 0) { + rawValue.forEach((col)=>{ + cols.push(col.local_column); + remoteCols.push(col.referenced); + }); + return '('+cols.join(', ')+') -> ('+ remoteCols.join(', ')+')'; + } + return ''; + }, + } + }, + minWidth: 245, + }), + deps: ()=>{ + let ret = []; + if(obj.inTable) { + ret.push(['columns']); + } + return ret; + }, + depChange: (state, source, topState, actionObj)=>{ + /* If in table, sync up value with columns in table */ + if(obj.inTable && !state) { + /* the FK is removed by some other dep, this can be a no-op */ + return; + } + let currColumns = state.columns || []; + if(obj.inTable && source[0] == 'columns') { + if(actionObj.type == SCHEMA_STATE_ACTIONS.DELETE_ROW) { + let oldColumn = _.get(actionObj.oldState, actionObj.path.concat(actionObj.value)); + currColumns = _.filter(currColumns, (cc)=>cc.local_column != oldColumn.name); + } else if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE) { + let tabColPath = _.slice(actionObj.path, 0, -1); + let oldColName = _.get(actionObj.oldState, tabColPath).name; + let idx = _.findIndex(currColumns, (cc)=>cc.local_column == oldColName); + if(idx > -1) { + currColumns[idx].local_column = _.get(topState, tabColPath).name; + } + } + } + return {columns: currColumns}; + }, + },{ + id: 'confupdtype', label: gettext('On update'), + type:'select', group: gettext('Action'), mode: ['edit','create'], + controlProps:{allowClear: false}, + options: [ + {label: 'NO ACTION', value: 'a'}, + {label: 'RESTRICT', value: 'r'}, + {label: 'CASCADE', value: 'c'}, + {label: 'SET NULL', value: 'n'}, + {label: 'SET DEFAULT', value: 'd'}, + ], readonly: obj.isReadonly, + },{ + id: 'confdeltype', label: gettext('On delete'), + type:'select', group: gettext('Action'), mode: ['edit','create'], + select2:{allowClear: false}, + options: [ + {label: 'NO ACTION', value: 'a'}, + {label: 'RESTRICT', value: 'r'}, + {label: 'CASCADE', value: 'c'}, + {label: 'SET NULL', value: 'n'}, + {label: 'SET DEFAULT', value: 'd'}, + ], readonly: obj.isReadonly, + }]; + } + + validate(state, setError) { + if ((_.isUndefined(state.columns) || _.isNull(state.columns) || state.columns.length < 1)) { + setError('columns', gettext('Please specify columns for Foreign key.')); + return true; + } + + if (this.isNew(state)){ + if (state.autoindex && isEmptyString(state.coveringindex)) { + setError('coveringindex', gettext('Please specify covering index name.')); + return true; + } + } + + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js index 1ab9a1f91..ed96c21e9 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.js @@ -7,11 +7,13 @@ // ////////////////////////////////////////////////////////////// +import { getNodeListByName } from '../../../../../../../../../static/js/node_ajax'; +import PrimaryKeySchema from './primary_key.ui'; + define('pgadmin.node.primary_key', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', - 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid', - 'pgadmin.browser.collection', -], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) { + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection', +], function(gettext, url_for, $, _, pgAdmin, pgBrowser) { // Extend the browser's node class for index constraint node if (!pgBrowser.Nodes['primary_key']) { @@ -96,573 +98,15 @@ define('pgadmin.node.primary_key', [ return !is_immediate_parent_table_partitioned; } }, - - // Define the model for index constraint node - model: pgAdmin.Browser.Node.Model.extend({ - idAttribute: 'oid', - - defaults: { - name: undefined, - oid: undefined, - is_sys_obj: undefined, - comment: undefined, - spcname: undefined, - index: undefined, - fillfactor: undefined, - condeferrable: undefined, - condeferred: undefined, - columns: [], - include: [], - }, - - // Define the schema for the index constraint node - schema: [{ - id: 'name', label: gettext('Name'), type: 'text', - mode: ['properties', 'create', 'edit'], editable:true, - cellHeaderClasses:'width_percent_40', - },{ - id: 'oid', label: gettext('OID'), cell: 'string', - type: 'text' , mode: ['properties'], editable: false, - cellHeaderClasses:'width_percent_20', - },{ - id: 'is_sys_obj', label: gettext('System primary key?'), - cell:'boolean', type: 'switch', mode: ['properties'], - },{ - id: 'comment', label: gettext('Comment'), cell: 'string', - type: 'multiline', mode: ['properties', 'create', 'edit'], - deps:['name'], disabled:function(m) { - var name = m.get('name'); - if (!(name && name != '')) { - setTimeout(function(){ - if(m.get('comment') && m.get('comment') !== '') { - m.set('comment', null); - } - },10); - return true; - } else { - return false; - } - }, - },{ - id: 'columns', label: gettext('Columns'), - type: 'collection', group: gettext('Definition'), - editable: false, - cell: Backgrid.StringCell.extend({ - initialize: function() { - Backgrid.StringCell.prototype.initialize.apply(this, arguments); - - var self = this, - collection = this.model.get('columns'); - - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.removeColumn); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - collection.on('pgadmin:multicolumn:updated', function() { - self.render.apply(self); - }); - self.listenTo(collection, 'add', self.render); - self.listenTo(collection, 'remove', self.render); - }, - removeColumn: function(m) { - var self = this, - removedCols = self.model.get('columns').where( - {column: m.get('name')} - ); - - self.model.get('columns').remove(removedCols); - setTimeout(function () { - self.render(); - }, 10); - - var key = 'primary_key'; - setTimeout(function () { - var constraints = self.model.top.get(key), - removed = []; - constraints.each(function(constraint) { - if (constraint.get('columns').length == 0) { - removed.push(constraint); - } - }); - constraints.remove(removed); - },100); - - }, - resetColOptions : function(m) { - var self = this, - updatedCols = self.model.get('columns').where( - {column: m.previous('name')} - ); - if (updatedCols.length > 0) { - /* - * Table column name has changed so update - * column name in primary key as well. - */ - updatedCols[0].set( - {'column': m.get('name')}, - {silent: true}); - } - - setTimeout(function () { - self.render(); - }, 10); - }, - formatter: { - fromRaw: function (rawValue) { - return rawValue.pluck('column').toString(); - }, - toRaw: function (val) { return val; }, - }, - render: function() { - return Backgrid.StringCell.prototype.render.apply(this, arguments); - }, - remove: function() { - var tableCols = this.model.top.get('columns'), - primary_key_col = this.model.get('columns'); - - if (primary_key_col) { - primary_key_col.off('pgadmin:multicolumn:updated'); - } - - this.stopListening(tableCols, 'remove' , self.removeColumn); - this.stopListening(tableCols, 'change:name' , self.resetColOptions); - - Backgrid.StringCell.prototype.remove.apply(this, arguments); - }, - }), - canDelete: true, canAdd: true, - control: Backform.MultiSelectAjaxControl.extend({ - defaults: _.extend( - {}, - Backform.NodeListByNameControl.prototype.defaults, - { - select2: { - multiple: true, - allowClear: true, - width: 'style', - placeholder: gettext('Select the column(s)'), - }, - } - ), - keyPathAccessor: function(obj, path) { - var res = obj; - if(_.isArray(res)) { - return _.map(res, function(o) { return o['column']; - }); - } - path = path.split('.'); - for (var i = 0; i < path.length; i++) { - if (_.isNull(res)) return null; - if (_.isEmpty(path[i])) continue; - if (!_.isUndefined(res[path[i]])) res = res[path[i]]; - } - return _.isObject(res) && !_.isArray(res) ? null : res; - }, - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - var self = this; - if(this.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.resetColOptions); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - self.custom_options(); - } else { - Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments); - } - self.model.get('columns').on('pgadmin:multicolumn:updated', function() { - self.render.apply(self); - }); - }, - resetColOptions: function() { - var self = this; - - setTimeout(function () { - self.custom_options(); - self.render.apply(self); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = this.model.top.get('columns'), - added_columns_from_tables = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push( - {label: col, value: col, image:'icon-column'} - ); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - }, - onChange: function() { - var self = this, - model = this.model, - attrArr = this.field.get('name').split('.'), - name = attrArr.shift(), - vals = this.getValueFromDOM(), - collection = model.get(name), - removed = []; - - this.stopListening(this.model, 'change:' + name, this.render); - - /* - * Iterate through all the values, and find out how many are already - * present in the collection. - */ - collection.each(function(m) { - var column = m.get('column'), - idx = _.indexOf(vals, column); - - if (idx > -1) { - vals.splice(idx, 1); - } else { - removed.push(column); - } - }); - - /* - * Adding new values - */ - - _.each(vals, function(v) { - var m = new (self.field.get('model'))( - {column: v}, { silent: true, - top: self.model.top, - collection: collection, - handler: collection, - }); - - collection.add(m); - }); - - /* - * Removing unwanted! - */ - _.each(removed, function(v) { - collection.remove(collection.where({column: v})); - }); - - this.listenTo(this.model, 'change:' + name, this.render); - }, - remove: function() { - if(this.model.handler) { - var self = this, - tableCols = self.model.top.get('columns'); - self.stopListening(tableCols, 'remove' , self.resetColOptions); - self.stopListening(tableCols, 'change:name' , self.resetColOptions); - self.model.get('columns').off('pgadmin:multicolumn:updated'); - - Backform.Select2Control.prototype.remove.apply(this, arguments); - - } else { - Backform.MultiSelectAjaxControl.prototype.remove.apply(this, arguments); - } - }, - render: function() { - var index = this.model.get('index'); - if(!_.isUndefined(index) && index != '') { - var col = this.model.get('columns'); - col.reset([], {silent: true}); - } - return Backform.Select2Control.prototype.render.apply(this, arguments); - }, - }), - deps: ['index'], node: 'column', - model: pgBrowser.Node.Model.extend({ - defaults: { - column: undefined, - }, - validate: function() { - return null; - }, - }), - transform : function(data){ - var res = []; - if (data && _.isArray(data)) { - _.each(data, function(d) { - res.push({label: d.label, value: d.label, image:'icon-column'}); - }); - } - return res; - }, - select2:{allowClear:false}, - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should be allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update columns of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - return !(_.isUndefined(index) || index == ''); - }, - },{ - id: 'include', label: gettext('Include columns'), - type: 'array', group: gettext('Definition'), - editable: false, - canDelete: true, canAdd: true, mode: ['properties', 'create', 'edit'], - visible: function(m) { - /* In table properties, m.node_info is not available */ - m = m.top; - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version >= 110000) - return true; - - return false; - }, - control: Backform.MultiSelectAjaxControl.extend({ - defaults: _.extend( - {}, - Backform.NodeListByNameControl.prototype.defaults, - { - select2: { - allowClear: false, - width: 'style', - multiple: true, - placeholder: gettext('Select the column(s)'), - }, - } - ), - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - var self = this; - if(this.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.resetColOptions); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - self.custom_options(); - } else { - Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments); - } - }, - resetColOptions: function() { - var self = this; - - setTimeout(function () { - self.custom_options(); - self.render.apply(self); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = this.model.top.get('columns'), - added_columns_from_tables = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push( - {label: col, value: col, image:'icon-column'} - ); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - }, - }), - deps: ['index'], node: 'column', - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should be allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update columns of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - m.set('include', []); - },10); - return true; - } - }, - },{ - id: 'spcname', label: gettext('Tablespace'), - type: 'text', group: gettext('Definition'), - control: 'node-list-by-name', node: 'tablespace', - deps: ['index'], - select2:{allowClear:false}, - filter: function(m) { - // Don't show pg_global tablespace in selection. + getSchema: function(treeNodeInfo, itemNodeData) { + return new PrimaryKeySchema({ + columns: ()=>getNodeListByName('column', treeNodeInfo, itemNodeData), + spcname: ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{ return (m.label != 'pg_global'); - }, - disabled: function(m) { - // Disable if index is selected. - m = m.top || m; - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - m.set('spcname', ''); - },10); - return true; - } - }, - },{ - id: 'index', label: gettext('Index'), - mode: ['create'], - type: 'text', group: gettext('Definition'), - control: Backform.NodeListByNameControl.extend({ - initialize:function() { - Backform.NodeListByNameControl.prototype.initialize.apply(this, arguments); - }, }), - select2:{allowClear:true}, node: 'index', - readonly: function(m) { - // If we are in table edit mode then disable it - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - return true; - } - - // We can't update index of existing index constraint. - return !m.isNew(); - }, - // We will not show this field in Create Table mode - visible: function(m) { - return !_.isUndefined(m.top.node_info['table']); - }, - },{ - id: 'fillfactor', label: gettext('Fill factor'), deps: ['index'], - type: 'int', group: gettext('Definition'), allowNull: true, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - m.set('fillfactor', null); - },10); - return true; - } - }, - },{ - id: 'condeferrable', label: gettext('Deferrable?'), - type: 'switch', group: gettext('Definition'), deps: ['index'], - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update condeferrable of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - if(m.get('condeferrable')) - m.set('condeferrable', false); - },10); - return true; - } - }, - },{ - id: 'condeferred', label: gettext('Deferred?'), - type: 'switch', group: gettext('Definition'), - deps: ['condeferrable'], - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update condeferred of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if condeferred is false or unselected. - if(m.get('condeferrable') == true) { - return false; - } else { - setTimeout(function(){ - if(m.get('condeferred')) - m.set('condeferred', false); - },10); - return true; - } - }, - }, - ], - validate: function() { - this.errorModel.clear(); - // Clear parent's error as well - if (_.has(this, 'top')) { - this.top.errorModel.clear(); - } - - var columns = this.get('columns'), - index = this.get('index'); - - if ((_.isUndefined(index) || String(index).replace(/^\s+|\s+$/g, '') == '') && - (_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { - var msg = gettext('Please specify columns for %s.', gettext('Primary key')); - this.errorModel.set('columns', msg); - return msg; - } - - return null; - }, - }), + index: ()=>getNodeListByName('index', treeNodeInfo, itemNodeData), + }, treeNodeInfo); + }, }); } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui.js new file mode 100644 index 000000000..b117690aa --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui.js @@ -0,0 +1,271 @@ +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'; +export default class PrimaryKeySchema extends BaseUISchema { + constructor(fieldOptions={}, nodeInfo) { + super({ + name: undefined, + oid: undefined, + is_sys_obj: undefined, + comment: undefined, + spcname: undefined, + index: undefined, + fillfactor: undefined, + condeferrable: undefined, + condeferred: undefined, + columns: [], + include: [], + }); + + this.fieldOptions = fieldOptions; + this.nodeInfo = nodeInfo; + } + + get idAttribute() { + return 'oid'; + } + + get inTable() { + if(_.isUndefined(this.nodeInfo)) { + return true; + } + return _.isUndefined(this.nodeInfo['table']); + } + + changeColumnOptions(columns) { + this.fieldOptions.columns = columns; + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), type: 'text', + mode: ['properties', 'create', 'edit'], editable:true, + cell: 'text', + },{ + id: 'oid', label: gettext('OID'), cell: 'text', + type: 'text' , mode: ['properties'], editable: false, + },{ + id: 'is_sys_obj', label: gettext('System primary key?'), + cell:'switch', type: 'switch', mode: ['properties'], + },{ + id: 'comment', label: gettext('Comment'), cell: 'multiline', + type: 'multiline', mode: ['properties', 'create', 'edit'], + deps:['name'], disabled: (state)=>{ + if(isEmptyString(state.name)){ + return true; + } + return false; + }, depChange: (state)=>{ + if(isEmptyString(state.name)){ + return {comment: ''}; + } + } + },{ + id: 'columns', label: gettext('Columns'), + deps: ()=>{ + let ret = ['index']; + if(obj.inTable) { + ret.push(['columns']); + } + return ret; + }, + depChange: (state, source, topState, actionObj)=>{ + /* If in table, sync up value with columns in table */ + let currColumns = state.columns || []; + if(obj.inTable && source[0] == 'columns') { + if(actionObj.type == SCHEMA_STATE_ACTIONS.DELETE_ROW) { + let oldColumn = _.get(actionObj.oldState, actionObj.path.concat(actionObj.value)); + currColumns = _.filter(currColumns, (cc)=>cc.column != oldColumn.name); + } else if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE) { + let tabColPath = _.slice(actionObj.path, 0, -1); + let oldColName = _.get(actionObj.oldState, tabColPath).name; + let idx = _.findIndex(currColumns, (cc)=>cc.column == oldColName); + if(idx > -1) { + currColumns[idx].column = _.get(topState, tabColPath).name; + } + } + } + return {columns: currColumns}; + }, + cell: ()=>({ + cell: '', + controlProps: { + formatter: { + fromRaw: (backendVal)=>{ + /* remove the column key and pass as array */ + return _.join((backendVal||[]).map((singleVal)=>singleVal.column)); + }, + }, + } + }), + type: ()=>({ + type: 'select', + optionsReloadBasis: obj.fieldOptions.columns?.map ? _.join(obj.fieldOptions.columns.map((c)=>c.label), ',') : null, + options: obj.fieldOptions.columns, + controlProps: { + allowClear:false, + multiple: true, + formatter: { + fromRaw: (backendVal, allOptions)=>{ + /* remove the column key and pass as array */ + let optValues = (backendVal||[]).map((singleVal)=>singleVal.column); + return _.filter(allOptions, (opt)=>optValues.indexOf(opt.value)>-1); + }, + toRaw: (value)=>{ + /* take the array and convert to column key collection */ + return (value||[]).map((singleVal)=>({column: singleVal.value})); + }, + }, + }, + }), group: gettext('Definition'), + editable: false, + readonly: function(state) { + if(!obj.isNew(state)) { + return true; + } + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + },{ + id: 'include', label: gettext('Include columns'), + type: ()=>({ + type: 'select', + options: this.fieldOptions.columns, + controlProps: { + multiple: true, + }, + }), group: gettext('Definition'), + editable: false, + canDelete: true, canAdd: true, + mode: ['properties', 'create', 'edit'], + visible: function() { + /* In table properties, nodeInfo is not available */ + if(this.getServerVersion() >= 110000) + return true; + + return false; + }, + deps: ['index'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {include: []}; + } + } + },{ + id: 'spcname', label: gettext('Tablespace'), + type: 'select', group: gettext('Definition'), + options: this.fieldOptions.spcname, + deps: ['index'], + controlProps:{allowClear:false}, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {spcname: ''}; + } + } + },{ + id: 'index', label: gettext('Index'), + mode: ['create'], + type: 'select', group: gettext('Definition'), + controlProps:{ + allowClear:true, + options: this.fieldOptions.index, + }, + readonly: function() { + if(!obj.isNew()) { + return true; + } + }, + // We will not show this field in Create Table mode + visible: function() { + return !obj.inTable; + }, + },{ + id: 'fillfactor', label: gettext('Fill factor'), deps: ['index'], + type: 'int', group: gettext('Definition'), min: 10, max: 100, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {fillfactor: null}; + } + } + },{ + id: 'condeferrable', label: gettext('Deferrable?'), + type: 'switch', group: gettext('Definition'), deps: ['index'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + return false; + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {condeferrable: false}; + } + } + },{ + id: 'condeferred', label: gettext('Deferred?'), + type: 'switch', group: gettext('Definition'), + deps: ['condeferrable'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + return false; + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {condeferred: false}; + } + } + }]; + } + + validate(state, setError) { + if(isEmptyString(state.index) + && (_.isUndefined(state.columns) || _.isNull(state.columns) || state.columns.length < 1)) { + setError('columns', gettext('Please specify columns for %s.', gettext('Primary key'))); + return true; + } + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js index dce4f51f0..d018959ed 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.js @@ -7,11 +7,13 @@ // ////////////////////////////////////////////////////////////// +import { getNodeListByName } from '../../../../../../../../../static/js/node_ajax'; +import UniqueConstraintSchema from './unique_constraint.ui'; + define('pgadmin.node.unique_constraint', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', - 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.backform', 'pgadmin.backgrid', - 'pgadmin.browser.collection', -], function(gettext, url_for, $, _, pgAdmin, pgBrowser, Backform, Backgrid) { + 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection', +], function(gettext, url_for, $, _, pgAdmin, pgBrowser) { // Extend the browser's node class for index constraint node if (!pgBrowser.Nodes['unique_constraint']) { @@ -83,571 +85,15 @@ define('pgadmin.node.unique_constraint', [ } }, - // Define the model for index constraint node - model: pgAdmin.Browser.Node.Model.extend({ - idAttribute: 'oid', - - defaults: { - name: undefined, - oid: undefined, - is_sys_obj: undefined, - comment: undefined, - spcname: undefined, - index: undefined, - fillfactor: undefined, - condeferrable: undefined, - condeferred: undefined, - columns: [], - include: [], - }, - - // Define the schema for the index constraint node - schema: [{ - id: 'name', label: gettext('Name'), type: 'text', - mode: ['properties', 'create', 'edit'], editable:true, - cellHeaderClasses:'width_percent_40', - },{ - id: 'oid', label: gettext('OID'), cell: 'string', - type: 'text' , mode: ['properties'], editable: false, - cellHeaderClasses:'width_percent_20', - },{ - id: 'is_sys_obj', label: gettext('System unique constraint?'), - cell:'boolean', type: 'switch', mode: ['properties'], - },{ - id: 'comment', label: gettext('Comment'), cell: 'string', - type: 'multiline', mode: ['properties', 'create', 'edit'], - deps:['name'], disabled:function(m) { - var name = m.get('name'); - if (!(name && name != '')) { - setTimeout(function(){ - if(m.get('comment') && m.get('comment') !== '') { - m.set('comment', null); - } - },10); - return true; - } else { - return false; - } - }, - },{ - id: 'columns', label: gettext('Columns'), - type: 'collection', group: gettext('Definition'), - editable: false, - cell: Backgrid.StringCell.extend({ - initialize: function() { - Backgrid.StringCell.prototype.initialize.apply(this, arguments); - - var self = this, - collection = this.model.get('columns'); - - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.removeColumn); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - collection.on('pgadmin:multicolumn:updated', function() { - self.render.apply(self); - }); - self.listenTo(collection, 'add', self.render); - self.listenTo(collection, 'remove', self.render); - }, - removeColumn: function(m) { - var self = this, - removedCols = self.model.get('columns').where( - {column: m.get('name')} - ); - - self.model.get('columns').remove(removedCols); - setTimeout(function () { - self.render(); - }, 10); - - var key = 'unique_constraint'; - setTimeout(function () { - var constraints = self.model.top.get(key), - removed = []; - - constraints.each(function(constraint) { - if (constraint.get('columns').length == 0) { - removed.push(constraint); - } - }); - constraints.remove(removed); - },100); - }, - resetColOptions : function(m) { - var self = this, - updatedCols = self.model.get('columns').where( - {column: m.previous('name')} - ); - if (updatedCols.length > 0) { - /* - * Table column name has changed so update - * column name in primary key as well. - */ - updatedCols[0].set( - {'column': m.get('name')}, - {silent: true}); - } - - setTimeout(function () { - self.render(); - }, 10); - }, - formatter: { - fromRaw: function (rawValue) { - return rawValue.pluck('column').toString(); - }, - toRaw: function (val) { return val; }, - }, - render: function() { - return Backgrid.StringCell.prototype.render.apply(this, arguments); - }, - remove: function() { - var tableCols = this.model.top.get('columns'), - primary_key_col = this.model.get('columns'); - - if (primary_key_col) { - primary_key_col.off('pgadmin:multicolumn:updated'); - } - - this.stopListening(tableCols, 'remove' , self.removeColumn); - this.stopListening(tableCols, 'change:name' , self.resetColOptions); - - Backgrid.StringCell.prototype.remove.apply(this, arguments); - }, - }), - canDelete: true, canAdd: true, - control: Backform.MultiSelectAjaxControl.extend({ - defaults: _.extend( - {}, - Backform.NodeListByNameControl.prototype.defaults, - { - select2: { - multiple: true, - allowClear: true, - width: 'style', - placeholder: gettext('Select the column(s)'), - }, - } - ), - keyPathAccessor: function(obj, path) { - var res = obj; - if(_.isArray(res)) { - return _.map(res, function(o) { return o['column']; - }); - } - path = path.split('.'); - for (var i = 0; i < path.length; i++) { - if (_.isNull(res)) return null; - if (_.isEmpty(path[i])) continue; - if (!_.isUndefined(res[path[i]])) res = res[path[i]]; - } - return _.isObject(res) && !_.isArray(res) ? null : res; - }, - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - var self = this; - if(this.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.resetColOptions); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - self.custom_options(); - } else { - Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments); - } - self.model.get('columns').on('pgadmin:multicolumn:updated', function() { - self.render.apply(self); - }); - }, - resetColOptions: function() { - var self = this; - - setTimeout(function () { - self.custom_options(); - self.render.apply(self); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = this.model.top.get('columns'), - added_columns_from_tables = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push( - {label: col, value: col, image:'icon-column'} - ); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - }, - onChange: function() { - var self = this, - model = this.model, - attrArr = this.field.get('name').split('.'), - name = attrArr.shift(), - vals = this.getValueFromDOM(), - collection = model.get(name), - removed = []; - - this.stopListening(this.model, 'change:' + name, this.render); - - /* - * Iterate through all the values, and find out how many are already - * present in the collection. - */ - collection.each(function(m) { - var column = m.get('column'), - idx = _.indexOf(vals, column); - - if (idx > -1) { - vals.splice(idx, 1); - } else { - removed.push(column); - } - }); - - /* - * Adding new values - */ - - _.each(vals, function(v) { - var m = new (self.field.get('model'))( - {column: v}, { silent: true, - top: self.model.top, - collection: collection, - handler: collection, - }); - - collection.add(m); - }); - - /* - * Removing unwanted! - */ - _.each(removed, function(v) { - collection.remove(collection.where({column: v})); - }); - - this.listenTo(this.model, 'change:' + name, this.render); - }, - remove: function() { - if(this.model.handler) { - var self = this, - tableCols = self.model.top.get('columns'); - self.stopListening(tableCols, 'remove' , self.resetColOptions); - self.stopListening(tableCols, 'change:name' , self.resetColOptions); - self.model.get('columns').off('pgadmin:multicolumn:updated'); - - Backform.Select2Control.prototype.remove.apply(this, arguments); - - } else { - Backform.MultiSelectAjaxControl.prototype.remove.apply(this, arguments); - } - }, - render: function() { - var index = this.model.get('index'); - if(!_.isUndefined(index) && index != '') { - var col = this.model.get('columns'); - col.reset([], {silent: true}); - } - return Backform.Select2Control.prototype.render.apply(this, arguments); - }, - }), - deps: ['index'], node: 'column', - model: pgBrowser.Node.Model.extend({ - defaults: { - column: undefined, - }, - validate: function() { - return null; - }, - }), - transform : function(data){ - var res = []; - if (data && _.isArray(data)) { - _.each(data, function(d) { - res.push({label: d.label, value: d.label, image:'icon-column'}); - }); - } - return res; - }, - select2:{allowClear:false}, - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should be allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update columns of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - return (!_.isUndefined(index) && index != ''); - }, - },{ - id: 'include', label: gettext('Include columns'), - type: 'array', group: gettext('Definition'), - editable: false, - canDelete: true, canAdd: true, mode: ['properties', 'create', 'edit'], - visible: function(m) { - /* In table properties, m.node_info is not available */ - m = m.top; - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version >= 110000) - return true; - - return false; - }, - control: Backform.MultiSelectAjaxControl.extend({ - defaults: _.extend( - {}, - Backform.NodeListByNameControl.prototype.defaults, - { - select2: { - allowClear: false, - width: 'style', - multiple: true, - placeholder: gettext('Select the column(s)'), - }, - } - ), - initialize: function() { - // Here we will decide if we need to call URL - // Or fetch the data from parent columns collection - var self = this; - if(this.model.handler) { - Backform.Select2Control.prototype.initialize.apply(this, arguments); - // Do not listen for any event(s) for existing constraint. - if (_.isUndefined(self.model.get('oid'))) { - var tableCols = self.model.top.get('columns'); - self.listenTo(tableCols, 'remove' , self.resetColOptions); - self.listenTo(tableCols, 'change:name', self.resetColOptions); - } - - self.custom_options(); - } else { - Backform.MultiSelectAjaxControl.prototype.initialize.apply(this, arguments); - } - }, - resetColOptions: function() { - var self = this; - - setTimeout(function () { - self.custom_options(); - self.render.apply(self); - }, 50); - }, - custom_options: function() { - // We will add all the columns entered by user in table model - var columns = this.model.top.get('columns'), - added_columns_from_tables = []; - - if (columns.length > 0) { - _.each(columns.models, function(m) { - var col = m.get('name'); - if(!_.isUndefined(col) && !_.isNull(col)) { - added_columns_from_tables.push( - {label: col, value: col, image:'icon-column'} - ); - } - }); - } - // Set the values in to options so that user can select - this.field.set('options', added_columns_from_tables); - }, - }), - deps: ['index'], node: 'column', - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should be allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update columns of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - m.set('include', []); - },10); - return true; - } - }, - },{ - id: 'spcname', label: gettext('Tablespace'), - type: 'text', group: gettext('Definition'), - control: 'node-list-by-name', node: 'tablespace', - deps: ['index'], - select2:{allowClear:false}, - filter: function(m) { - // Don't show pg_global tablespace in selection. + getSchema: function(treeNodeInfo, itemNodeData) { + return new UniqueConstraintSchema({ + columns: ()=>getNodeListByName('column', treeNodeInfo, itemNodeData), + spcname: ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{ return (m.label != 'pg_global'); - }, - disabled: function(m) { - // Disable if index is selected. - m = m.top || m; - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - m.set('spcname', ''); - },10); - return true; - } - }, - },{ - id: 'index', label: gettext('Index'), - type: 'text', group: gettext('Definition'), - control: Backform.NodeListByNameControl.extend({ - initialize:function() { - Backform.NodeListByNameControl.prototype.initialize.apply(this,arguments); - }, }), - select2:{allowClear:true}, node: 'index', - readonly: function(m) { - // If we are in table edit mode then disable it - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - return true; - } - - // We can't update index of existing index constraint. - return !m.isNew(); - }, - // We will not show this field in Create Table mode - visible: function(m) { - return !_.isUndefined(m.top.node_info['table']); - }, - },{ - id: 'fillfactor', label: gettext('Fill factor'), deps: ['index'], - type: 'int', group: gettext('Definition'), allowNull: true, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - m.set('fillfactor', null); - },10); - return true; - } - }, - },{ - id: 'condeferrable', label: gettext('Deferrable?'), - type: 'switch', group: gettext('Definition'), deps: ['index'], - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update condeferrable of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if index is selected. - var index = m.get('index'); - if(_.isUndefined(index) || index == '') { - return false; - } else { - setTimeout(function(){ - if(m.get('condeferrable')) - m.set('condeferrable', false); - },10); - return true; - } - }, - },{ - id: 'condeferred', label: gettext('Deferred?'), - type: 'switch', group: gettext('Definition'), - deps: ['condeferrable'], - readonly: function(m) { - // If we are in table edit mode then - if (_.has(m, 'top') && !_.isUndefined(m.top) - && !m.top.isNew()) { - // If OID is undefined then user is trying to add - // new constraint which should allowed for Unique - return !_.isUndefined(m.get('oid')); - } - - // We can't update condeferred of existing index constraint. - if (!m.isNew()) { - return true; - } - }, - disabled: function(m) { - // Disable if condeferred is false or unselected. - if(m.get('condeferrable') == true) { - return false; - } else { - setTimeout(function(){ - if(m.get('condeferred')) - m.set('condeferred', false); - },10); - return true; - } - }, - }, - ], - validate: function() { - this.errorModel.clear(); - // Clear parent's error as well - if (_.has(this, 'top')) { - this.top.errorModel.clear(); - } - - var columns = this.get('columns'), - index = this.get('index'); - - if ((_.isUndefined(index) || String(index).replace(/^\s+|\s+$/g, '') == '') && - (_.isUndefined(columns) || _.isNull(columns) || columns.length < 1)) { - var msg = gettext('Please specify columns for %s.', gettext('Unique constraint')); - this.errorModel.set('columns', msg); - return msg; - } - - return null; - }, - }), + index: ()=>getNodeListByName('index', treeNodeInfo, itemNodeData), + }, treeNodeInfo); + }, }); } diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui.js new file mode 100644 index 000000000..22b72f278 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui.js @@ -0,0 +1,273 @@ +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'; + +export default class UniqueConstraintSchema extends BaseUISchema { + constructor(fieldOptions={}, nodeInfo) { + super({ + name: undefined, + oid: undefined, + is_sys_obj: undefined, + comment: undefined, + spcname: undefined, + index: undefined, + fillfactor: undefined, + condeferrable: undefined, + condeferred: undefined, + columns: [], + include: [], + }); + + this.fieldOptions = fieldOptions; + this.nodeInfo = nodeInfo; + } + + get idAttribute() { + return 'oid'; + } + + get inTable() { + if(_.isUndefined(this.nodeInfo)) { + return true; + } + return _.isUndefined(this.nodeInfo['table']); + } + + changeColumnOptions(columns) { + this.fieldOptions.columns = columns; + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), type: 'text', + mode: ['properties', 'create', 'edit'], editable:true, + cell: 'text', + },{ + id: 'oid', label: gettext('OID'), cell: 'text', + type: 'text' , mode: ['properties'], editable: false, + },{ + id: 'is_sys_obj', label: gettext('System primary key?'), + cell:'switch', type: 'switch', mode: ['properties'], + },{ + id: 'comment', label: gettext('Comment'), cell: 'multiline', + type: 'multiline', mode: ['properties', 'create', 'edit'], + deps:['name'], disabled: (state)=>{ + if(isEmptyString(state.name)){ + return true; + } + return false; + }, depChange: (state)=>{ + if(isEmptyString(state.name)){ + return {comment: ''}; + } + } + },{ + id: 'columns', label: gettext('Columns'), + deps: ()=>{ + let ret = ['index']; + if(obj.inTable) { + ret.push(['columns']); + } + return ret; + }, + depChange: (state, source, topState, actionObj)=>{ + /* If in table, sync up value with columns in table */ + let currColumns = state.columns || []; + if(obj.inTable && source[0] == 'columns') { + if(actionObj.type == SCHEMA_STATE_ACTIONS.DELETE_ROW) { + let oldColumn = _.get(actionObj.oldState, actionObj.path.concat(actionObj.value)); + currColumns = _.filter(currColumns, (cc)=>cc.column != oldColumn.name); + } else if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE) { + let tabColPath = _.slice(actionObj.path, 0, -1); + let oldColName = _.get(actionObj.oldState, tabColPath).name; + let idx = _.findIndex(currColumns, (cc)=>cc.column == oldColName); + if(idx > -1) { + currColumns[idx].column = _.get(topState, tabColPath).name; + } + } + } + return {columns: currColumns}; + }, + cell: ()=>({ + cell: '', + controlProps: { + formatter: { + fromRaw: (backendVal)=>{ + /* remove the column key and pass as array */ + return _.join((backendVal||[]).map((singleVal)=>singleVal.column)); + }, + }, + } + }), + type: ()=>({ + type: 'select', + optionsReloadBasis: obj.fieldOptions.columns?.map ? _.join(obj.fieldOptions.columns.map((c)=>c.label), ',') : null, + options: obj.fieldOptions.columns, + controlProps: { + allowClear:false, + multiple: true, + formatter: { + fromRaw: (backendVal)=>{ + /* remove the column key and pass as array */ + return (backendVal||[]).map((singleVal)=>singleVal.column); + }, + toRaw: (value)=>{ + /* take the array and convert to column key collection */ + return (value||[]).map((singleVal)=>({column: singleVal})); + }, + }, + }, + }), group: gettext('Definition'), + editable: false, + readonly: function(state) { + if(!obj.isNew(state)) { + return true; + } + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + },{ + id: 'include', label: gettext('Include columns'), + type: ()=>({ + type: 'select', + options: this.fieldOptions.columns, + controlProps: { + multiple: true, + }, + }), group: gettext('Definition'), + editable: false, + canDelete: true, canAdd: true, + mode: ['properties', 'create', 'edit'], + visible: function() { + /* In table properties, nodeInfo is not available */ + if(!_.isUndefined(this.nodeInfo) && !_.isUndefined(this.nodeInfo.server) + && !_.isUndefined(this.nodeInfo.server.version) && + this.nodeInfo.server.version >= 110000) + return true; + + return false; + }, + deps: ['index'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {include: []}; + } + } + },{ + id: 'spcname', label: gettext('Tablespace'), + type: 'select', group: gettext('Definition'), + options: this.fieldOptions.spcname, + deps: ['index'], + controlProps:{allowClear:false}, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {spcname: ''}; + } + } + },{ + id: 'index', label: gettext('Index'), + mode: ['create'], + type: 'select', group: gettext('Definition'), + controlProps:{ + allowClear:true, + options: this.fieldOptions.index, + }, + readonly: function() { + if(!obj.isNew()) { + return true; + } + }, + // We will not show this field in Create Table mode + visible: function() { + return !obj.inTable; + }, + },{ + id: 'fillfactor', label: gettext('Fill factor'), deps: ['index'], + type: 'int', group: gettext('Definition'), + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {fillfactor: null}; + } + } + },{ + id: 'condeferrable', label: gettext('Deferrable?'), + type: 'switch', group: gettext('Definition'), deps: ['index'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + return false; + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {condeferrable: false}; + } + } + },{ + id: 'condeferred', label: gettext('Deferred?'), + type: 'switch', group: gettext('Definition'), + deps: ['condeferrable'], + readonly: function() { + if(!obj.isNew()) { + return true; + } + return false; + }, + disabled: function(state) { + // Disable if index is selected. + return !(_.isUndefined(state.index) || state.index == ''); + }, + depChange: (state)=>{ + if(_.isUndefined(state.index) || state.index == '') { + return {}; + } else { + return {condeferred: false}; + } + } + }]; + } + + validate(state, setError) { + if(isEmptyString(state.index) + && (_.isUndefined(state.columns) || _.isNull(state.columns) || state.columns.length < 1)) { + setError('columns', gettext('Please specify columns for %s.', gettext('Unique constraint'))); + return true; + } + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils.ui.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils.ui.js new file mode 100644 index 000000000..0c90010b3 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils.ui.js @@ -0,0 +1,386 @@ +import _ from 'lodash'; +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import { emptyValidator, isEmptyString } from '../../../../../../../../static/js/validators'; + +export class PartitionKeysSchema extends BaseUISchema { + constructor(columns=[]) { + super({ + key_type: 'column', + }); + this.columns = columns; + this.columnsReloadBasis = false; + } + + changeColumnOptions(columns) { + this.columns = columns; + } + + get baseFields() { + let obj = this; + return [{ + id: 'key_type', label: gettext('Key type'), type:'select', editable: true, + cell:'select', controlProps: {allowClear: false}, noEmpty: true, + options:[{ + label: gettext('Column'), value: 'column', + },{ + label: gettext('Expression'), value: 'expression', + }], + },{ + id: 'pt_column', label: gettext('Column'), type:'select', + deps: ['key_type', ['columns']], + depChange: (state, source)=>{ + if(state.key_type == 'expression' || source[0] == 'columns') { + return { + pt_column: undefined, + }; + } + }, + cell: ()=>({ + cell: 'select', + optionsReloadBasis: _.join(obj.columns.map((c)=>c.label), ','),//obj.columnsReloadBasis, + options: obj.columns, + controlProps: { + allowClear: false, + } + }), + editable: function(state) { + if(state.key_type == 'expression') { + return false; + } + return true; + }, + },{ + id: 'expression', label: gettext('Expression'), type:'text', + cell: 'text', deps: ['key_type'], + depChange: (state)=>{ + if(state.key_type == 'column') { + return { + expression: undefined, + }; + } + }, + editable: function(state) { + if(state.key_type == 'expression') { + return true; + } + return false; + }, + }]; + } + + validate(state, setError) { + let errmsg; + if(state.key_type == 'column') { + errmsg = emptyValidator('Partition key column', state.pt_column); + if(errmsg) { + setError('pt_column', errmsg); + return true; + } + } else { + errmsg = emptyValidator('Partition key expression', state.expression); + if(errmsg) { + setError('expression', errmsg); + return true; + } + } + return false; + } +} +export class PartitionsSchema extends BaseUISchema { + constructor(nodeInfo) { + super({ + oid: undefined, + is_attach: false, + partition_name: undefined, + is_default: undefined, + values_from: undefined, + values_to: undefined, + values_in: undefined, + values_modulus: undefined, + values_remainder: undefined, + is_sub_partitioned: false, + sub_partition_type: 'range', + }); + + this.subPartitionsObj = new PartitionKeysSchema(); + this.nodeInfo = nodeInfo; + } + + changeColumnOptions(columns) { + this.subPartitionsObj.changeColumnOptions(columns); + } + + get baseFields() { + let obj = this; + return [{ + id: 'oid', label: gettext('OID'), type: 'text', + mode: ['properties'], + },{ + id: 'is_attach', label:gettext('Operation'), cell: 'select', type: 'select', + minWidth: 75, options: [ + {label: gettext('Attach'), value: true}, + {label: gettext('Create'), value: false}, + ], + editable: function(state) { + if(obj.isNew(state) && !obj.top.isNew()) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.isNew(state) && !obj.top.isNew()) { + return false; + } + return true; + }, + },{ + id: 'partition_name', label: gettext('Name'), type: 'text', cell:'text', + minWidth: 80, editable: function(state) { + if(obj.isNew(state)) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.isNew(state)) { + return false; + } + return true; + }, noEmpty: true, + },{ + id: 'is_default', label: gettext('Default'), type: 'switch', cell:'switch', + minWidth: 55, min_version: 110000, + editable: function(state) { + if(obj.top && (obj.top.sessData.partition_type == 'range' || + obj.top.sessData.partition_type == 'list') && obj.isNew(state) + && obj.getServerVersion() >= 110000) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.top && (obj.top.sessData.partition_type == 'range' || + obj.top.sessData.partition_type == 'list') && obj.isNew(state) + && obj.getServerVersion() >= 110000) { + return false; + } + return true; + }, + },{ + id: 'values_from', label: gettext('From'), type:'text', cell: 'text', + minWidth: 80, deps: ['is_default'], + editable: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'range' && obj.isNew(state) + && state.is_default !== true) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'range' && obj.isNew(state) + && state.is_default !== true) { + return false; + } + return true; + }, + }, + { + id: 'values_to', label: gettext('To'), type:'text', cell: 'text', + minWidth: 80, deps: ['is_default'], + editable: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'range' && obj.isNew(state) + && state.is_default !== true) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'range' && obj.isNew(state) + && state.is_default !== true) { + return false; + } + return true; + }, + },{ + id: 'values_in', label: gettext('In'), type:'text', cell: 'text', + minWidth: 80, deps: ['is_default'], + editable: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'list' && obj.isNew(state) + && state.is_default !== true) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'list' && obj.isNew(state) + && state.is_default !== true) { + return false; + } + return true; + }, + },{ + id: 'values_modulus', label: gettext('Modulus'), type:'int', cell: 'int', + minWidth: 80, + editable: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'hash' && obj.isNew(state)) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'hash' && obj.isNew(state) + && state.is_default !== true) { + return false; + } + return true; + }, + },{ + id: 'values_remainder', label: gettext('Remainder'), type:'int', cell: 'int', + minWidth: 80, + editable: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'hash' && obj.isNew(state)) { + return true; + } + return false; + }, + disabled: function(state) { + if(obj.top && obj.top.sessData.partition_type == 'hash' && obj.isNew(state) + && state.is_default !== true) { + return false; + } + return true; + }, + },{ + id: 'is_sub_partitioned', label:gettext('Partitioned table?'), cell: 'switch', + group: 'Partition', type: 'switch', mode: ['properties', 'create', 'edit'], + deps: ['is_attach'], readonly: (state)=>{ + if(!obj.isNew(state)) { + return true; + } + if(state.is_attach) { + return true; + } + return false; + }, + depChange: (state)=>{ + if(state.is_attach) { + return {is_sub_partitioned: false}; + } + }, + },{ + id: 'sub_partition_type', label:gettext('Partition Type'), + editable: false, type: 'select', controlProps: {allowClear: false}, + group: 'Partition', deps: ['is_sub_partitioned'], + options: function() { + var options = [{ + label: gettext('Range'), value: 'range', + },{ + label: gettext('List'), value: 'list', + }]; + + if(obj.getServerVersion() >= 110000) { + options.push({ + label: gettext('Hash'), value: 'hash', + }); + } + return Promise.resolve(options); + }, + visible: (state)=>obj.isNew(state), + readonly: (state)=>!obj.isNew(state), + disabled: (state)=>!state.is_sub_partitioned, + },{ + id: 'sub_partition_keys', label:gettext('Partition Keys'), + schema: this.subPartitionsObj, + editable: true, type: 'collection', + group: 'Partition', mode: ['properties', 'create', 'edit'], + deps: ['is_sub_partitioned', 'sub_partition_type', ['typname']], + canEdit: false, canDelete: true, + canAdd: function(state) { + if (obj.isNew(state) && state.is_sub_partitioned) + return true; + return false; + }, + canAddRow: function(state) { + let columnsExist = false; + let columns = obj.top.sessData.columns; + + var maxRowCount = 1000; + if (state.sub_partition_type && state.sub_partition_type == 'list') + maxRowCount = 1; + + if (columns?.length > 0) { + columnsExist = _.some(_.map(columns, 'name')); + } + + if(state.sub_partition_keys) { + return state.sub_partition_keys.length < maxRowCount && columnsExist; + } + + return true; + }, + depChange: (state, source, topState, actionObj)=>{ + if(topState.typname != actionObj.oldState.typname) { + return { + sub_partition_keys: [], + }; + } + }, + visible: (state)=>obj.isNew(state), + },{ + id: 'sub_partition_scheme', label: gettext('Partition Scheme'), + group: 'Partition', mode: ['edit'], + type: (state)=>({ + type: 'note', + text: state.sub_partition_scheme, + }), + visible: function(state) { + if(obj.isNew(state) && !isEmptyString(state.sub_partition_scheme)) { + return true; + } + return false; + }, + }]; + } + + validate(state, setError) { + let msg; + if (state.is_sub_partitioned && this.isNew(state) && + (!state.sub_partition_keys || state.sub_partition_keys && state.sub_partition_keys.length <= 0)) { + msg = gettext('Please specify at least one key for partitioned table.'); + setError('sub_partition_keys', msg); + return true; + } + + let partitionType = this.top.sessData.partition_type; + if (partitionType === 'range') { + if (!state.is_default && isEmptyString(state.values_from)) { + msg = gettext('For range partition From field cannot be empty.'); + setError('values_from', msg); + return true; + } else if (!state.is_default && isEmptyString(state.values_to)) { + msg = gettext('For range partition To field cannot be empty.'); + setError('values_to', msg); + return true; + } + } else if (partitionType === 'list') { + if (!state.is_default && isEmptyString(state.values_in)) { + msg = gettext('For list partition In field cannot be empty.'); + setError('values_in', msg); + return true; + } + } else if (partitionType === 'hash') { + if(isEmptyString(state.values_modulus)) { + msg = gettext('For hash partition Modulus field cannot be empty.'); + setError('values_modulus', msg); + return true; + } if(isEmptyString(state.values_remainder)) { + msg = gettext('For hash partition Remainder field cannot be empty.'); + setError('values_remainder', msg); + return true; + } + } + + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js index 059f3f4f4..858e2baf6 100644 --- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.js @@ -7,6 +7,8 @@ // ////////////////////////////////////////////////////////////// +import { getNodeTableSchema } from './table.ui'; + define('pgadmin.node.table', [ 'pgadmin.tables.js/enable_disable_triggers', 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', @@ -291,56 +293,53 @@ define('pgadmin.node.table', [ }); }, }, + fetchColumnsInherits: function(arg) { + var self = this, + url = 'get_columns', + m = self.model.top || self.model, + data = undefined, + node = this.field.get('schema_node'), + node_info = this.field.get('node_info'), + full_url = node.generate_url.apply( + node, [ + null, url, this.field.get('node_data'), + this.field.get('url_with_id') || false, node_info, + ] + ), + cache_level = this.field.get('cache_level') || node.type, + cache_node = this.field.get('cache_node'); + + cache_node = (cache_node && pgBrowser.Nodes[cache_node]) || node; + + m.trigger('pgadmin:view:fetching', m, self.field); + // Fetching Columns data for the selected table. + $.ajax({ + async: false, + url: full_url, + data: arg, + }) + .done(function(res) { + data = cache_node.cache(url, node_info, cache_level, res.data); + }) + .fail(function() { + m.trigger('pgadmin:view:fetch:error', m, self.field); + }); + m.trigger('pgadmin:view:fetched', m, self.field); + data = (data && data.data) || []; + return data; + }, + getSchema: function(treeNodeInfo, itemNodeData) { + return getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser); + }, model: pgBrowser.Node.Model.extend({ idAttribute: 'oid', defaults: { name: undefined, oid: undefined, - spcoid: undefined, - spcname: undefined, relowner: undefined, - relacl: undefined, - relhasoids: undefined, - relhassubclass: undefined, - reltuples: undefined, description: undefined, - conname: undefined, - conkey: undefined, - isrepl: undefined, - triggercount: undefined, - relpersistence: undefined, - fillfactor: undefined, - toast_tuple_target: undefined, - parallel_workers: undefined, - reloftype: undefined, - typname: undefined, - labels: undefined, - providers: undefined, - is_sys_table: undefined, - coll_inherits: [], - hastoasttable: true, - toast_autovacuum_enabled: 'x', - autovacuum_enabled: 'x', - primary_key: [], - partitions: [], - partition_type: 'range', is_partitioned: false, }, - // Default values! - initialize: function(attrs, args) { - if (_.size(attrs) === 0) { - var userInfo = pgBrowser.serverInfo[ - args.node_info.server._id - ].user, - schemaInfo = args.node_info.schema; - - this.set({ - 'relowner': userInfo.name, 'schema': schemaInfo._label, - }, {silent: true}); - } - pgBrowser.Node.Model.prototype.initialize.apply(this, arguments); - - }, schema: [{ id: 'name', label: gettext('Name'), type: 'text', mode: ['properties', 'create', 'edit'], disabled: 'inSchema', @@ -350,36 +349,6 @@ define('pgadmin.node.table', [ id: 'relowner', label: gettext('Owner'), type: 'text', node: 'role', mode: ['properties', 'create', 'edit'], select2: {allowClear: false}, disabled: 'inSchema', control: 'node-list-by-name', - },{ - id: 'schema', label: gettext('Schema'), type: 'text', node: 'schema', - control: 'node-list-by-name', mode: ['create', 'edit'], - disabled: 'inSchema', filter: function(d) { - // If schema name start with pg_* then we need to exclude them - if(d && d.label.match(/^pg_/)) - { - return false; - } - return true; - }, cache_node: 'database', cache_level: 'database', - },{ - id: 'spcname', label: gettext('Tablespace'), node: 'tablespace', - type: 'text', control: 'node-list-by-name', - mode: ['properties', 'create', 'edit'], - filter: function(d) { - // If tablespace name is not "pg_global" then we need to exclude them - return (!(d && d.label.match(/pg_global/))); - }, - deps: ['is_partitioned'], - disabled: 'inSchema', - },{ - id: 'partition', type: 'group', label: gettext('Partitions'), - mode: ['edit', 'create'], min_version: 100000, - visible: function(m) { - // Always show in case of create mode - if (m.isNew() || m.get('is_partitioned')) - return true; - return false; - }, },{ id: 'is_partitioned', label:gettext('Partitioned table?'), cell: 'switch', type: 'switch', mode: ['properties', 'create', 'edit'], @@ -389,761 +358,9 @@ define('pgadmin.node.table', [ return true; return false; }, - },{ - id: 'is_sys_table', label: gettext('System table?'), cell: 'switch', - type: 'switch', mode: ['properties'], - disabled: 'inSchema', },{ id: 'description', label: gettext('Comment'), type: 'multiline', mode: ['properties', 'create', 'edit'], disabled: 'inSchema', - },{ - id: 'coll_inherits', label: gettext('Inherited from table(s)'), - url: 'get_inherits', type: 'array', group: gettext('Columns'), - disabled: 'checkInheritance', deps: ['typname', 'is_partitioned'], - mode: ['create', 'edit'], first_empty: false, - select2: { multiple: true, allowClear: true, first_empty: false, - placeholder: gettext('Select to inherit from...')}, - transform: function(data, cell) { - var control = cell || this, - m = control.model; - m.inherited_tables_list = data; - return data; - }, - control: Backform.MultiSelectAjaxControl.extend({ - // When changes we need to add/clear columns collection - onChange: function() { - Backform.MultiSelectAjaxControl.prototype.onChange.apply(this, arguments); - var self = this, - // current table list and previous table list - cTbl_list = self.model.get('coll_inherits') || [], - pTbl_list = self.model.previous('coll_inherits') || []; - - if (!_.isUndefined(cTbl_list)) { - var tbl_name = undefined, - tid = undefined; - - // Add columns logic - // If new table is added in list - if(cTbl_list.length > 1 && cTbl_list.length > pTbl_list.length) { - // Find newly added table from current list - tbl_name = _.difference(cTbl_list, pTbl_list); - tid = this.get_table_oid(tbl_name[0]); - this.add_columns(tid); - } else if (cTbl_list.length == 1) { - // First table added - tid = this.get_table_oid(cTbl_list[0]); - this.add_columns(tid); - } - - // Remove columns logic - if(cTbl_list.length > 0 && cTbl_list.length < pTbl_list.length) { - // Find deleted table from previous list - tbl_name = _.difference(pTbl_list, cTbl_list); - this.remove_columns(tbl_name[0]); - } else if (pTbl_list.length === 1 && cTbl_list.length < 1) { - // We got last table from list - tbl_name = pTbl_list[0]; - this.remove_columns(tbl_name); - } - - } - }, - add_columns: function(tid) { - // Create copy of old model if anything goes wrong at-least we have backup - // Then send AJAX request to fetch table specific columns - var self = this, - m = self.model.top || self.model, - data = undefined, - column_collection = m.get('columns'); - - var arg = {'tid': tid}; - data = self.model.fetch_columns_ajax.apply(self, [arg]); - - // Update existing column collection - column_collection.set(data, { merge:false,remove:false }); - }, - remove_columns: function(tblname) { - // Remove all the column models for deleted table - var tid = this.get_table_oid(tblname), - column_collection = this.model.get('columns'); - column_collection.remove(column_collection.where({'inheritedid': tid })); - }, - get_table_oid: function(tblname) { - // Here we will fetch the table oid from table name - var tbl_oid = undefined; - // iterate over list to find table oid - _.each(this.model.inherited_tables_list, function(obj) { - if(obj.label === tblname) { - tbl_oid = obj.tid; - } - }); - return tbl_oid; - }, - }), - }, - { - id: 'rlspolicy', label: gettext('RLS Policy?'), cell: 'switch', - type: 'switch', mode: ['properties','edit', 'create'], - group: gettext('advanced'), - visible: 'isSupported', - disabled: function(m) { - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version < 90600) - return true; - - return m.inSchema(); - }, - }, - { - id: 'forcerlspolicy', label: gettext('Force RLS Policy?'), cell: 'switch', - type: 'switch', mode: ['properties','edit', 'create'], deps: ['rlspolicy'], - group: gettext('advanced'), - visible: 'isSupported', - disabled: function(m) { - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version < 90600) - return true; - - if (m.get('rlspolicy')){ - return false; - } - setTimeout(function() { - m.set('forcerlspolicy', false); - }, 10); - - return true; - }, - }, - { - id: 'replica_identity', label: gettext('Replica Identity'), - group: gettext('advanced'), type: 'text',mode: ['edit', 'properties'], - }, - - { - id: 'advanced', label: gettext('Advanced'), type: 'group', - visible: ShowAdvancedTab.show_advanced_tab, - }, { - id: 'coll_inherits', label: gettext('Inherited from table(s)'), - type: 'text', group: 'advanced', mode: ['properties'], - },{ - id: 'inherited_tables_cnt', label: gettext('Inherited tables count'), - type: 'text', mode: ['properties'], group: 'advanced', - disabled: 'inSchema', - },{ - // Tab control for columns - id: 'columns', label: gettext('Columns'), type: 'collection', - group: gettext('Columns'), - model: pgBrowser.Nodes['column'].model, - subnode: pgBrowser.Nodes['column'].model, - mode: ['create', 'edit'], - disabled: function(m) { - // In case of partitioned table remove inherited columns - if (m.isNew() && m.get('is_partitioned')) { - setTimeout(function() { - var coll = m.get('columns'); - coll.remove(coll.filter(function(model) { - if (_.isUndefined(model.get('inheritedfrom'))) - return false; - return true; - })); - }, 10); - } - - if(this.node_info && 'catalog' in this.node_info) - { - return true; - } - return false; - }, - deps: ['typname', 'is_partitioned'], - canAdd: 'check_grid_add_condition', - canEdit: true, canDelete: true, - // For each row edit/delete button enable/disable - canEditRow: 'check_grid_row_edit_delete', - canDeleteRow: 'check_grid_row_edit_delete', - uniqueCol : ['name'], - columns : ['name' , 'cltype', 'attlen', 'attprecision', 'attnotnull', 'is_primary_key'], - control: Backform.UniqueColCollectionControl.extend({ - initialize: function() { - Backform.UniqueColCollectionControl.prototype.initialize.apply(this, arguments); - var self = this, - collection = self.model.get(self.field.get('name')); - - collection.on('change:is_primary_key', function(m) { - var primary_key_coll = self.model.get('primary_key'), - column_name = m.get('name'), - primary_key, primary_key_column_coll; - - if(m.get('is_primary_key')) { - // Add column to primary key. - if (primary_key_coll.length < 1) { - primary_key = new (primary_key_coll.model)({}, { - top: self.model, - collection: primary_key_coll, - handler: primary_key_coll, - }); - primary_key_coll.add(primary_key); - } else { - primary_key = primary_key_coll.first(); - } - // Do not alter existing primary key columns. - if (_.isUndefined(primary_key.get('oid'))) { - primary_key_column_coll = primary_key.get('columns'); - var primary_key_column_exist = primary_key_column_coll.where({column:column_name}); - - if (primary_key_column_exist.length == 0) { - var primary_key_column = new ( - primary_key_column_coll.model - )({column: column_name}, { - silent: true, - top: self.model, - collection: primary_key_coll, - handler: primary_key_coll, - }); - - primary_key_column_coll.add(primary_key_column); - } - - primary_key_column_coll.trigger( - 'pgadmin:multicolumn:updated', primary_key_column_coll - ); - } - - } else { - // remove column from primary key. - if (primary_key_coll.length > 0) { - primary_key = primary_key_coll.first(); - // Do not alter existing primary key columns. - if (!_.isUndefined(primary_key.get('oid'))) { - return; - } - - primary_key_column_coll = primary_key.get('columns'); - var removedCols = primary_key_column_coll.where({column:column_name}); - if (removedCols.length > 0) { - primary_key_column_coll.remove(removedCols); - _.each(removedCols, function(local_model) { - local_model.destroy(); - }); - if (primary_key_column_coll.length == 0) { - /* Ideally above line of code should be "primary_key_coll.reset()". - * But our custom DataCollection (extended from Backbone collection in datamodel.js) - * does not respond to reset event, it only supports add, remove, change events. - * And hence no custom event listeners/validators get called for reset event. - */ - primary_key_coll.remove(primary_key_coll.first()); - } - } - primary_key_column_coll.trigger('pgadmin:multicolumn:updated', primary_key_column_coll); - } - } - }); - }, - remove: function() { - var collection = this.model.get(this.field.get('name')); - if (collection) { - collection.off('change:is_primary_key'); - } - - Backform.UniqueColCollectionControl.prototype.remove.apply(this, arguments); - }, - }), - allowMultipleEmptyRow: false, - },{ - // Here we will create tab control for constraints - type: 'nested', control: 'tab', group: gettext('Constraints'), - mode: ['edit', 'create'], - schema: [{ - id: 'primary_key', label: '', - model: pgBrowser.Nodes['primary_key'].model, - subnode: pgBrowser.Nodes['primary_key'].model, - editable: false, type: 'collection', - group: gettext('Primary Key'), mode: ['edit', 'create'], - canEdit: true, canDelete: true, deps:['is_partitioned'], - control: 'unique-col-collection', - columns : ['name', 'columns'], - canAdd: function(m) { - if (m.get('is_partitioned') && !_.isUndefined(m.top.node_info) && !_.isUndefined(m.top.node_info.server) - && !_.isUndefined(m.top.node_info.server.version) && - m.top.node_info.server.version < 110000) { - setTimeout(function() { - var coll = m.get('primary_key'); - coll.remove(coll.filter(function() { return true; })); - }, 10); - return false; - } - - return true; - }, - canAddRow: function(m) { - // User can only add one primary key - var columns = m.get('columns'); - - return (m.get('primary_key') && - m.get('primary_key').length < 1 && - _.some(columns.pluck('name'))); - }, - },{ - id: 'foreign_key', label: '', - model: pgBrowser.Nodes['foreign_key'].model, - subnode: pgBrowser.Nodes['foreign_key'].model, - editable: false, type: 'collection', - group: gettext('Foreign Key'), mode: ['edit', 'create'], - canEdit: true, canDelete: true, deps:['is_partitioned'], - control: 'unique-col-collection', - canAdd: function(m) { - if (m.get('is_partitioned') && !_.isUndefined(m.top.node_info) && !_.isUndefined(m.top.node_info.server) - && !_.isUndefined(m.top.node_info.server.version) && - m.top.node_info.server.version < 110000) { - setTimeout(function() { - var coll = m.get('foreign_key'); - coll.remove(coll.filter(function() { return true; })); - }, 10); - return false; - } - - return true; - }, - columns : ['name', 'columns','references_table_name'], - canAddRow: function(m) { - // User can only add if there is at least one column with name. - var columns = m.get('columns'); - return _.some(columns.pluck('name')); - }, - },{ - id: 'check_constraint', label: '', - model: pgBrowser.Nodes['check_constraint'].model, - subnode: pgBrowser.Nodes['check_constraint'].model, - editable: false, type: 'collection', - group: gettext('Check'), mode: ['edit', 'create'], - canEdit: true, canDelete: true, deps:['is_partitioned'], - control: 'unique-col-collection', - canAdd: true, - columns : ['name', 'consrc'], - },{ - id: 'unique_constraint', label: '', - model: pgBrowser.Nodes['unique_constraint'].model, - subnode: pgBrowser.Nodes['unique_constraint'].model, - editable: false, type: 'collection', - group: gettext('Unique'), mode: ['edit', 'create'], - canEdit: true, canDelete: true, deps:['is_partitioned'], - control: 'unique-col-collection', - columns : ['name', 'columns'], - canAdd: function(m) { - if (m.get('is_partitioned') && !_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && m.node_info.server.version < 110000) { - setTimeout(function() { - var coll = m.get('unique_constraint'); - coll.remove(coll.filter(function() { return true; })); - }, 10); - return false; - } - - return true; - }, - canAddRow: function(m) { - // User can only add if there is at least one column with name. - var columns = m.get('columns'); - return _.some(columns.pluck('name')); - }, - },{ - id: 'exclude_constraint', label: '', - model: pgBrowser.Nodes['exclusion_constraint'].model, - subnode: pgBrowser.Nodes['exclusion_constraint'].model, - editable: false, type: 'collection', - group: gettext('Exclude'), mode: ['edit', 'create'], - canEdit: true, canDelete: true, deps:['is_partitioned'], - control: 'unique-col-collection', - columns : ['name', 'columns', 'constraint'], - canAdd: function(m) { - if (m.get('is_partitioned')) { - setTimeout(function() { - var coll = m.get('exclude_constraint'); - coll.remove(coll.filter(function() { return true; })); - }, 10); - return false; - } - - return true; - }, - canAddRow: function(m) { - // User can only add if there is at least one column with name. - var columns = m.get('columns'); - return _.some(columns.pluck('name')); - }, - }], - },{ - id: 'typname', label: gettext('Of type'), type: 'text', - mode: ['properties', 'create', 'edit'], - disabled: 'checkOfType', url: 'get_oftype', group: gettext('advanced'), - deps: ['coll_inherits'], transform: function(data, cell) { - var control = cell || this, - m = control.model; - m.of_types_tables = data; - return data; - }, - control: Backform.NodeAjaxOptionsControl.extend({ - // When of_types changes we need to clear columns collection - onChange: function() { - Backform.NodeAjaxOptionsControl.prototype.onChange.apply(this, arguments); - var self = this, - tbl_name = self.model.get('typname'), - data = undefined, - arg = undefined, - column_collection = self.model.get('columns'); - - if (!_.isUndefined(tbl_name) && !_.isNull(tbl_name) && - tbl_name !== '' && column_collection.length !== 0) { - var title = gettext('Remove column definitions?'), - msg = gettext('Changing \'Of type\' will remove column definitions.'); - - Alertify.confirm( - title, msg, function () { - // User clicks Ok, lets clear columns collection - column_collection.remove( - column_collection.filter(function() { return true; }) - ); - }, - function() { - setTimeout(function() { - self.model.set('typname', null); - }, 10); - } - ); - } else if (!_.isUndefined(tbl_name) && tbl_name === '') { - column_collection.remove( - column_collection.filter(function() { return true; }) - ); - } - - // Run Ajax now to fetch columns - if (!_.isUndefined(tbl_name) && tbl_name !== '') { - arg = { 'tname': tbl_name }; - data = self.model.fetch_columns_ajax.apply(self, [arg]); - // Add into column collection - column_collection.set(data, { merge:false,remove:false }); - } - }, - }), - },{ - id: 'fillfactor', label: gettext('Fill factor'), type: 'int', - mode: ['create', 'edit'], min: 10, max: 100, - group: gettext('advanced'), - disabled: 'isDisabled', - },{ - id: 'toast_tuple_target', label: gettext('Toast tuple target'), type: 'int', - mode: ['create', 'edit'], min: 128, min_version: 110000, - group: gettext('advanced'), - disabled: 'isDisabled', - },{ - id: 'parallel_workers', label: gettext('Parallel workers'), type: 'int', - mode: ['create', 'edit'], group: gettext('advanced'), min_version: 90600, - disabled: 'isDisabled', - }, - { - id: 'relhasoids', label: gettext('Has OIDs?'), cell: 'switch', - type: 'switch', mode: ['properties', 'create', 'edit'], - group: gettext('advanced'), - disabled: function(m) { - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version >= 120000) - return true; - - return m.inSchema(); - }, - },{ - id: 'relpersistence', label: gettext('Unlogged?'), cell: 'switch', - type: 'switch', mode: ['properties', 'create', 'edit'], - disabled: 'inSchemaWithModelCheck', - group: gettext('advanced'), - },{ - id: 'conname', label: gettext('Primary key'), cell: 'string', - type: 'text', mode: ['properties'], group: gettext('advanced'), - disabled: 'inSchema', - },{ - id: 'reltuples', label: gettext('Rows (estimated)'), cell: 'string', - type: 'text', mode: ['properties'], group: gettext('advanced'), - disabled: 'inSchema', - },{ - id: 'rows_cnt', label: gettext('Rows (counted)'), cell: 'string', - type: 'text', mode: ['properties'], group: gettext('advanced'), - disabled: 'inSchema', control: Backform.InputControl.extend({ - formatter: { - fromRaw: function (rawData) { - var t = pgAdmin.Browser.tree, - i = t.selected(), - d = i && i.length == 1 ? t.itemData(i) : undefined; - - // Return the actual rows count if the selected node - // has already counted. - if(d && d.rows_cnt && parseInt(d.rows_cnt, 10) > 0) - return d.rows_cnt; - else - return rawData; - }, - toRaw: function (formattedData) { - return formattedData; - }, - }, - }), - },{ - id: 'relhassubclass', label: gettext('Inherits tables?'), cell: 'switch', - type: 'switch', mode: ['properties'], group: gettext('advanced'), - disabled: 'inSchema', - },{ - type: 'nested', control: 'fieldset', label: gettext('Like'), - group: gettext('advanced'), - schema:[{ - id: 'like_relation', label: gettext('Relation'), - type: 'text', mode: ['create'], deps: ['typname', 'like_relation'], - control: 'node-ajax-options', url: 'get_relations', - disabled: 'isLikeDisable', group: gettext('Like'), - },{ - id: 'like_default_value', label: gettext('With default values?'), - type: 'switch', mode: ['create'], deps: ['like_relation'], - disabled: 'isRelationDisable', group: gettext('Like'), - },{ - id: 'like_constraints', label: gettext('With constraints?'), - type: 'switch', mode: ['create'], deps: ['like_relation'], - disabled: 'isRelationDisable', group: gettext('Like'), - },{ - id: 'like_indexes', label: gettext('With indexes?'), - type: 'switch', mode: ['create'], deps: ['like_relation'], - disabled: 'isRelationDisable', group: gettext('Like'), - },{ - id: 'like_storage', label: gettext('With storage?'), - type: 'switch', mode: ['create'], deps: ['like_relation'], - disabled: 'isRelationDisable', group: gettext('Like'), - },{ - id: 'like_comments', label: gettext('With comments?'), - type: 'switch', mode: ['create'], deps: ['like_relation'], - disabled: 'isRelationDisable', group: gettext('Like'), - }], - },{ - id: 'partition_type', label:gettext('Partition Type'), - editable: false, type: 'select2', select2: {allowClear: false}, - group: 'partition', deps: ['is_partitioned'], - options: function() { - var options = [{ - label: gettext('Range'), value: 'range', - },{ - label: gettext('List'), value: 'list', - }]; - - if(!_.isUndefined(this.node_info) && !_.isUndefined(this.node_info.server) - && !_.isUndefined(this.node_info.server.version) && - this.node_info.server.version >= 110000) { - options.push({ - label: gettext('Hash'), value: 'hash', - }); - } - return options; - }, - mode:['create'], - visible: 'isVersionGreaterThan96', - disabled: function(m) { - if (!m.get('is_partitioned')) - return true; - return false; - }, - readonly: function(m) {return !m.isNew();}, - },{ - id: 'partition_keys', label:gettext('Partition Keys'), - model: Backform.PartitionKeyModel, - subnode: Backform.PartitionKeyModel, - editable: true, type: 'collection', - group: 'partition', mode: ['create'], - deps: ['is_partitioned', 'partition_type', 'typname'], - canEdit: false, canDelete: true, - control: 'sub-node-collection', - canAdd: function(m) { - if (m.isNew() && m.get('is_partitioned')) - return true; - return false; - }, - canAddRow: function(m) { - var columns = m.get('columns'), - typename = m.get('typname'), - columns_exist= false; - - var max_row_count = 1000; - if (m.get('partition_type') && m.get('partition_type') == 'list') - max_row_count = 1; - - /* If columns are not specified by the user then it may be - * possible that he/she selected 'OF TYPE', so we should check - * for that as well. - */ - if (columns.length <= 0 && !_.isUndefined(typename) - && !_.isNull(typename) && m.of_types_tables.length > 0){ - _.each(m.of_types_tables, function(data) { - if (data.label == typename && data.oftype_columns.length > 0){ - columns_exist = true; - } - }); - } else if (columns.length > 0) { - columns_exist = _.some(columns.pluck('name')); - } - - return (m.get('partition_keys') && - m.get('partition_keys').length < max_row_count && columns_exist - ); - - }, - visible: 'isVersionGreaterThan96', - disabled: function(m) { - if (m.get('partition_keys') && m.get('partition_keys').models.length > 0) { - setTimeout(function () { - var coll = m.get('partition_keys'); - coll.remove(coll.filter(function() { return true; })); - - }, 10); - } - }, - },{ - id: 'partition_scheme', label: gettext('Partition Scheme'), - type: 'note', group: 'partition', mode: ['edit'], - visible: 'isVersionGreaterThan96', - disabled: function(m) { - if (!m.isNew()) { - this.text = m.get('partition_scheme'); - } - }, - },{ - id: 'partition_key_note', label: gettext('Partition Keys'), - type: 'note', group: 'partition', mode: ['create'], - text: [ - '', - ].join(''), - visible: 'isVersionGreaterThan96', - }, { - id: 'partitions', label:gettext('Partitions'), - model: Backform.PartitionsModel, - subnode: Backform.PartitionsModel, - editable: true, type: 'collection', - group: 'partition', mode: ['edit', 'create'], - deps: ['is_partitioned', 'partition_type', 'typname'], - canEdit: true, canDelete: true, - customDeleteTitle: gettext('Detach Partition'), - customDeleteMsg: gettext('Are you sure you wish to detach this partition?'), - columns:['is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'], - control: Backform.SubNodeCollectionControl.extend({ - row: Backgrid.PartitionRow, - initialize: function() { - Backform.SubNodeCollectionControl.prototype.initialize.apply(this, arguments); - var self = this; - if (!this.model.isNew()) { - var node = this.field.get('schema_node'), - node_info = this.field.get('node_info'); - - // Make ajax call to get the tables to be attached - $.ajax({ - url: node.generate_url.apply( - node, [ - null, 'get_attach_tables', this.field.get('node_data'), - true, node_info, - ]), - - type: 'GET', - async: false, - }) - .done(function(res) { - if (res.success == 1) { - self.model.table_options = res.data; - } - else { - Alertify.alert( - gettext('Error fetching tables to be attached'), res.data.result - ); - } - }) - .fail(function(xhr, status, error) { - Alertify.pgRespErrorNotify(xhr, error); - }); - } - }, - } - ), - canAdd: function(m) { - if (m.get('is_partitioned')) - return true; - return false; - }, - visible: 'isVersionGreaterThan96', - disabled: function(m) { - if (m.isNew() && m.get('partitions') && m.get('partitions').models.length > 0) { - setTimeout(function () { - var coll = m.get('partitions'); - coll.remove(coll.filter(function() { return true; })); - }, 10); - } - }, - },{ - id: 'partition_note', label: gettext('Partitions'), - type: 'note', group: 'partition', - text: [ - '', - ].join(''), - visible: 'isVersionGreaterThan96', - },{ - // Here - we will create tab control for storage parameters - // (auto vacuum). - type: 'nested', control: 'tab', group: gettext('Parameters'), - mode: ['edit', 'create'], deps: ['is_partitioned'], - schema: Backform.VacuumSettingsSchema, - },{ - id: 'relacl_str', label: gettext('Privileges'), disabled: 'inSchema', - type: 'text', mode: ['properties'], group: gettext('Security'), - }, pgBrowser.SecurityGroupSchema,{ - id: 'relacl', label: gettext('Privileges'), type: 'collection', - group: 'security', control: 'unique-col-collection', - model: pgBrowser.Node.PrivilegeRoleModel.extend({ - privileges: ['a','r','w','d','D','x','t']}), - mode: ['edit', 'create'], canAdd: true, canDelete: true, - uniqueCol : ['grantee'], - },{ - id: 'seclabels', label: gettext('Security labels'), canEdit: false, - model: pgBrowser.SecLabelModel, editable: false, canAdd: true, - type: 'collection', min_version: 90100, mode: ['edit', 'create'], - group: 'security', canDelete: true, control: 'unique-col-collection', - },{ - id: 'vacuum_settings_str', label: gettext('Storage settings'), - type: 'multiline', group: 'advanced', mode: ['properties'], }], sessChanged: function() { /* If only custom autovacuum option is enabled the check if the options table is also changed. */ @@ -1155,75 +372,6 @@ define('pgadmin.node.table', [ } return pgBrowser.DataModel.prototype.sessChanged.apply(this); }, - validate: function() { - var msg, - name = this.get('name'), - schema = this.get('schema'), - relowner = this.get('relowner'), - is_partitioned = this.get('is_partitioned'), - partition_keys = this.get('partition_keys'); - - if ( - _.isUndefined(name) || _.isNull(name) || - String(name).replace(/^\s+|\s+$/g, '') == '' - ) { - msg = gettext('Table name cannot be empty.'); - this.errorModel.set('name', msg); - return msg; - } - this.errorModel.unset('name'); - if ( - _.isUndefined(schema) || _.isNull(schema) || - String(schema).replace(/^\s+|\s+$/g, '') == '' - ) { - msg = gettext('Table schema cannot be empty.'); - this.errorModel.set('schema', msg); - return msg; - } - this.errorModel.unset('schema'); - if ( - _.isUndefined(relowner) || _.isNull(relowner) || - String(relowner).replace(/^\s+|\s+$/g, '') == '' - ) { - msg = gettext('Table owner cannot be empty.'); - this.errorModel.set('relowner', msg); - return msg; - } - this.errorModel.unset('relowner'); - if ( - is_partitioned && this.isNew() && - !_.isNull(partition_keys) && partition_keys.length <= 0 - ) { - msg = gettext('Please specify at least one key for partitioned table.'); - this.errorModel.set('partition_keys', msg); - return msg; - } - this.errorModel.unset('partition_keys'); - if (this.get('rlspolicy') && this.changed.rlspolicy){ - Alertify.alert( - gettext('Check Policy?'), - gettext('Please check if any policy exist. If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified by other users') - ); - } - return null; - }, - isVersionGreaterThan96: function(m) { - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version >= 100000) - return true; - - return false; - }, - isSupported: function(m) { - // Enable when server version is greater than 95. - if(!_.isUndefined(m.node_info) && !_.isUndefined(m.node_info.server) - && !_.isUndefined(m.node_info.server.version) && - m.node_info.server.version >= 90600) - return true; - - return false; - }, // We will disable everything if we are under catalog node inSchema: function() { if(this.node_info && 'catalog' in this.node_info) @@ -1232,167 +380,6 @@ define('pgadmin.node.table', [ } return false; }, - isDisabled:function(m) { - if(m.get('is_partitioned')) { - return true; - } - return m.inSchema(); - }, - isInheritedTable: function(m) { - if(!m.inSchema.apply(this, [m])) { - // Either of_types or coll_inherits has value - return ( - (_.isUndefined(m.get('coll_inherits')) || m.get('coll_inherits').length == 0) - && - (_.isUndefined(m.get('typname')) || String(m.get('typname')).replace(/^\s+|\s+$/g, '') === '') - ); - } - return false; - }, - // Oftype is defined? - checkInheritance: function(m) { - // Disabled if it is partitioned table - if (m.get('is_partitioned')) { - setTimeout( function() { - m.set('coll_inherits', []); - }, 10); - return true; - } - - // coll_inherits || typname - if(!m.inSchema.apply(this, [m]) && - ( _.isUndefined(m.get('typname')) || - _.isNull(m.get('typname')) || - String(m.get('typname')).replace(/^\s+|\s+$/g, '') == '')) { - return false; - } - return true; - }, - // We will disable Like if ofType is defined - isLikeDisable: function(m) { - if(!m.inSchemaWithModelCheck.apply(this, [m]) && - ( _.isUndefined(m.get('typname')) || - _.isNull(m.get('typname')) || - String(m.get('typname')).replace(/^\s+|\s+$/g, '') == '')) { - return false; - } - return true; - }, - - // We will disable other Like option if Relation is not defined - isRelationDisable: function(m) { - if ( _.isUndefined(m.get('like_relation')) || - _.isNull(m.get('like_relation')) || - String(m.get('like_relation')).replace(/^\s+|\s+$/g, '') == ''){ - if (m.isNew()){ - setTimeout(function() { - m.set('like_default_value', false); - m.set('like_constraints', false); - m.set('like_indexes', false); - m.set('like_storage', false); - m.set('like_comments', false); - }, 10); - } - return true; - } - return false; - }, - // Check for column grid when to Add - check_grid_add_condition: function(m) { - var enable_flag = true; - if(!m.inSchema.apply(this, [m])) { - // if of_type then disable add in grid - if (!_.isUndefined(m.get('typname')) && - !_.isNull(m.get('typname')) && - m.get('typname') !== '') { - enable_flag = false; - } - } - return enable_flag; - }, - // Check for column grid when to edit/delete (for each row) - check_grid_row_edit_delete: function(m) { - var flag = true; - if(!_.isUndefined(m.get('inheritedfrom')) && - !_.isNull(m.get('inheritedfrom')) && - String(m.get('inheritedfrom')).replace(/^\s+|\s+$/g, '') !== '') { - flag = false; - } - return flag; - }, - // We will disable it if Inheritance is defined - checkOfType: function(m) { - //coll_inherits || typname - if(!m.inSchemaWithModelCheck.apply(this, [m]) && - (_.isUndefined(m.get('coll_inherits')) || - _.isNull(m.get('coll_inherits')) || - String(m.get('coll_inherits')).replace(/^\s+|\s+$/g, '') == '')) { - return false; - } - return true; - }, - // We will check if we are under schema node & in 'create' mode - inSchemaWithModelCheck: function(m) { - if(this.node_info && 'schema' in this.node_info) - { - // We will disbale control if it's in 'edit' mode - return !m.isNew(); - } - return true; - }, - isTableAutoVacuumEnable: function(m) { - // We need to check additional condition to toggle enable/disable - // for table auto-vacuum - if(!m.inSchema.apply(this, [m]) && - m.get('autovacuum_enabled') === true) { - return false; - } - return true; - }, - isToastTableAutoVacuumEnable: function(m) { - // We need to check additional condition to toggle enable/disable - // for toast table auto-vacuum - if(!m.inSchemaWithModelCheck.apply(this, [m]) && - m.get('toast_autovacuum_enabled') == true) { - return false; - } - return true; - }, - fetch_columns_ajax: function(arg) { - var self = this, - url = 'get_columns', - m = self.model.top || self.model, - data = undefined, - node = this.field.get('schema_node'), - node_info = this.field.get('node_info'), - full_url = node.generate_url.apply( - node, [ - null, url, this.field.get('node_data'), - this.field.get('url_with_id') || false, node_info, - ] - ), - cache_level = this.field.get('cache_level') || node.type, - cache_node = this.field.get('cache_node'); - - cache_node = (cache_node && pgBrowser.Nodes[cache_node]) || node; - - m.trigger('pgadmin:view:fetching', m, self.field); - // Fetching Columns data for the selected table. - $.ajax({ - async: false, - url: full_url, - data: arg, - }) - .done(function(res) { - data = cache_node.cache(url, node_info, cache_level, res.data); - }) - .fail(function() { - m.trigger('pgadmin:view:fetch:error', m, self.field); - }); - m.trigger('pgadmin:view:fetched', m, self.field); - data = (data && data.data) || []; - return data; - }, }), // Check to whether table has disable trigger(s) canCreate_with_trigger_enable: function(itemData, item, data) { 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 new file mode 100644 index 000000000..71491bca8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui.js @@ -0,0 +1,873 @@ +import gettext from 'sources/gettext'; +import BaseUISchema from 'sources/SchemaView/base_schema.ui'; +import SecLabelSchema from 'top/browser/server_groups/servers/static/js/sec_label.ui'; +import _ from 'lodash'; +import { isEmptyString } from 'sources/validators'; +import PrimaryKeySchema from '../../constraints/index_constraint/static/js/primary_key.ui'; +import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../static/js/SchemaView'; +import { PartitionKeysSchema, PartitionsSchema } from './partition.utils.ui'; +import { pgAlertify } from '../../../../../../../../static/js/helpers/legacyConnector'; +import CheckConstraintSchema from '../../constraints/check_constraint/static/js/check_constraint.ui'; +import UniqueConstraintSchema from '../../constraints/index_constraint/static/js/unique_constraint.ui'; +import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../static/js/node_ajax'; +import { getNodeColumnSchema } from '../../columns/static/js/column.ui'; +import { getNodeVacuumSettingsSchema } from '../../../../../static/js/vacuum.ui'; +import { getNodeForeignKeySchema } from '../../constraints/foreign_key/static/js/foreign_key.ui'; +import { getNodeExclusionConstraintSchema } from '../../constraints/exclusion_constraint/static/js/exclusion_constraint.ui'; +import { getNodePrivilegeRoleSchema } from '../../../../../static/js/privilege.ui'; + +export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) { + const spcname = ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{ + return (m.label != 'pg_global'); + }); + + let tableNode = pgBrowser.Nodes['table']; + + return new TableSchema( + { + relowner: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData), + schema: ()=>getNodeListByName('schema', treeNodeInfo, itemNodeData, { + cacheLevel: 'database', + cacheNode: 'database', + }, (d)=>{ + // If schema name start with pg_* then we need to exclude them + if(d && d.label.match(/^pg_/)) + { + return false; + } + return true; + }), + spcname: spcname, + coll_inherits: ()=>getNodeAjaxOptions('get_inherits', tableNode, treeNodeInfo, itemNodeData), + typname: ()=>getNodeAjaxOptions('get_oftype', tableNode, treeNodeInfo, itemNodeData), + like_relation: ()=>getNodeAjaxOptions('get_relations', tableNode, treeNodeInfo, itemNodeData), + }, + treeNodeInfo, + { + columns: ()=>getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser), + vacuum_settings: ()=>getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData), + constraints: ()=>new ConstraintsSchema( + treeNodeInfo, + ()=>getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, true), + ()=>getNodeExclusionConstraintSchema(treeNodeInfo, itemNodeData, pgBrowser, true), + {spcname: spcname}, + ), + }, + (privileges)=>getNodePrivilegeRoleSchema(tableNode, treeNodeInfo, itemNodeData, privileges), + (params)=>{ + return getNodeAjaxOptions('get_columns', tableNode, treeNodeInfo, itemNodeData, {urlParams: params, useCache:false}); + }, + { + relowner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name, + schema: treeNodeInfo.schema._label, + } + ); +} + +export class ConstraintsSchema extends BaseUISchema { + constructor(nodeInfo, getFkObj, getExConsObj, otherOptions) { + super(); + this.primaryKeyObj = new PrimaryKeySchema({ + spcname: otherOptions.spcname, + }, nodeInfo); + this.fkObj = getFkObj(); + this.uniqueConsObj = new UniqueConstraintSchema({ + spcname: otherOptions.spcname, + }, nodeInfo); + this.exConsObj = getExConsObj(); + } + + changeColumnOptions(colOptions) { + this.primaryKeyObj.changeColumnOptions(colOptions); + this.fkObj.changeColumnOptions(colOptions); + this.uniqueConsObj.changeColumnOptions(colOptions); + this.exConsObj.changeColumnOptions(colOptions); + } + + anyColumnAdded(state) { + return _.some(_.map(state.columns, 'name')); + } + + get baseFields() { + let obj = this; + return [{ + id: 'primary_key', label: '', + schema: this.primaryKeyObj, + editable: false, type: 'collection', + group: gettext('Primary Key'), mode: ['edit', 'create'], + canEdit: true, canDelete: true, deps:['is_partitioned', 'typname'], + columns : ['name', 'columns'], + canAdd: function(state) { + if (state.is_partitioned && obj.top.getServerVersion() < 110000) { + return false; + } + return true; + }, + canAddRow: function(state) { + return ((state.primary_key||[]).length < 1 && obj.anyColumnAdded(state)); + }, + depChange: (state)=>{ + if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) { + return {primary_key: []}; + } + } + },{ + id: 'foreign_key', label: '', + schema: this.fkObj, + editable: false, type: 'collection', + group: gettext('Foreign Key'), mode: ['edit', 'create'], + canEdit: true, canDelete: true, deps:['is_partitioned', 'columns'], + canAdd: function(state) { + if (state.is_partitioned && obj.top.getServerVersion() < 110000) { + return false; + } + return true; + }, + columns : ['name', 'columns','references_table_name'], + canAddRow: obj.anyColumnAdded, + depChange: (state)=>{ + if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) { + return {foreign_key: []}; + } + } + },{ + id: 'check_constraint', label: '', + schema: new CheckConstraintSchema(), + editable: false, type: 'collection', + group: gettext('Check'), mode: ['edit', 'create'], + canEdit: true, canDelete: true, deps:['is_partitioned'], + canAdd: true, + columns : ['name', 'consrc'], + },{ + id: 'unique_constraint', label: '', + schema: this.primaryKeyObj, + editable: false, type: 'collection', + group: gettext('Unique'), mode: ['edit', 'create'], + canEdit: true, canDelete: true, deps:['is_partitioned', 'typname'], + columns : ['name', 'columns'], + canAdd: function(state) { + if (state.is_partitioned && obj.top.getServerVersion() < 110000) { + return false; + } + return true; + }, + canAddRow: obj.anyColumnAdded, + depChange: (state)=>{ + if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) { + return {unique_constraint: []}; + } + } + },{ + id: 'exclude_constraint', label: '', + schema: this.exConsObj, + editable: false, type: 'collection', + group: gettext('Exclude'), mode: ['edit', 'create'], + canEdit: true, canDelete: true, deps:['is_partitioned'], + columns : ['name', 'columns', 'constraint'], + canAdd: function(state) { + if (state.is_partitioned && obj.top.getServerVersion() < 110000) { + return false; + } + return true; + }, + canAddRow: obj.anyColumnAdded, + depChange: (state)=>{ + if (state.is_partitioned && obj.top.getServerVersion() < 110000 || state.columns?.length <= 0) { + return {exclude_constraint: []}; + } + }, + }]; + } +} + +export class LikeSchema extends BaseUISchema { + constructor(likeRelationOpts) { + super(); + this.likeRelationOpts = likeRelationOpts; + } + + isLikeDisable(state) { + if(!this.top.inSchemaWithModelCheck(state) && isEmptyString(state.typname)) { + return false; + } + return true; + } + + isRelationDisable(state) { + if(isEmptyString(state.like_relation)) { + return true; + } + return false; + } + + resetVals(state) { + if(this.isRelationDisable(state) && this.top.isNew()) { + return { + like_default_value: false, + like_constraints: false, + like_indexes: false, + like_storage: false, + like_comments: false, + }; + } + } + + get baseFields() { + let obj = this; + return [ + { + id: 'like_relation', label: gettext('Relation'), + type: 'select', mode: ['create'], deps: ['typname', 'like_relation'], + options: obj.likeRelationOpts, + disabled: obj.isLikeDisable, + depChange: (state, source)=>{ + if(source == 'typname' && !isEmptyString(state.typname)) { + state.like_relation = null; + return { + like_relation: null, + ...obj.resetVals(state), + }; + } + } + },{ + id: 'like_default_value', label: gettext('With default values?'), + type: 'switch', mode: ['create'], deps: ['like_relation'], + disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), + },{ + id: 'like_constraints', label: gettext('With constraints?'), + type: 'switch', mode: ['create'], deps: ['like_relation'], + disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), + },{ + id: 'like_indexes', label: gettext('With indexes?'), + type: 'switch', mode: ['create'], deps: ['like_relation'], + disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), + },{ + id: 'like_storage', label: gettext('With storage?'), + type: 'switch', mode: ['create'], deps: ['like_relation'], + disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), + },{ + id: 'like_comments', label: gettext('With comments?'), + type: 'switch', mode: ['create'], deps: ['like_relation'], + disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args), + } + ]; + } +} + +export default class TableSchema extends BaseUISchema { + constructor(fieldOptions={}, nodeInfo, schemas, getPrivilegeRoleSchema, getColumns, initValues) { + super({ + name: undefined, + oid: undefined, + spcoid: undefined, + spcname: undefined, + relowner: undefined, + relacl: undefined, + relhasoids: undefined, + relhassubclass: undefined, + reltuples: undefined, + description: undefined, + conname: undefined, + conkey: undefined, + isrepl: undefined, + triggercount: undefined, + relpersistence: undefined, + fillfactor: undefined, + toast_tuple_target: undefined, + parallel_workers: undefined, + reloftype: undefined, + typname: undefined, + labels: undefined, + providers: undefined, + is_sys_table: undefined, + coll_inherits: [], + hastoasttable: true, + toast_autovacuum_enabled: 'x', + autovacuum_enabled: 'x', + primary_key: [], + foreign_key: [], + partitions: [], + partition_type: 'range', + is_partitioned: false, + columns: [], + ...initValues, + }); + + this.fieldOptions = fieldOptions; + this.schemas = schemas; + this.getPrivilegeRoleSchema = getPrivilegeRoleSchema; + this.nodeInfo = nodeInfo; + this.getColumns = getColumns; + + this.partitionKeysObj = new PartitionKeysSchema(); + this.partitionsObj = new PartitionsSchema(this.nodeInfo); + this.constraintsObj = this.schemas.constraints(); + this.columnsSchema = this.schemas.columns(); + this.vacuumSettingsSchema = this.schemas.vacuum_settings(); + } + + get idAttribute() { + return 'oid'; + } + + initialise(state) { + this.changeColumnOptions(state.columns); + } + + inSchemaWithModelCheck(state) { + if(this.nodeInfo && 'schema' in this.nodeInfo) { + return !this.isNew(state); + } + return false; + } + + getTableOid(tabName) { + // Here we will fetch the table oid from table name + // iterate over list to find table oid + for(const t of this.inheritedTableList) { + if(t.label === tabName) { + return t.tid; + } + } + return; + } + + // Check for column grid when to Add + canAddRowColumns(state) { + if(!this.inCatalog()) { + // if of_type then disable add in grid + if (!isEmptyString(state.typname)) { + return false; + } + } + return true; + } + + // Check for column grid when to edit/delete (for each row) + canEditDeleteRowColumns(colstate) { + if (!isEmptyString(colstate.inheritedfrom)) { + return false; + } + return true; + } + + isPartitioned(state) { + if(state.is_partitioned) { + return true; + } + return this.inCatalog(); + } + + changeColumnOptions(columns) { + let colOptions = (columns||[]).map((c)=>({label: c.name, value: c.name, image:'icon-column'})); + this.constraintsObj.changeColumnOptions(colOptions); + this.partitionKeysObj.changeColumnOptions(colOptions); + this.partitionsObj.changeColumnOptions(colOptions); + } + + get baseFields() { + let obj = this; + + return [{ + id: 'name', label: gettext('Name'), type: 'text', noEmpty: true, + mode: ['properties', 'create', 'edit'], readonly: this.inCatalog, + },{ + id: 'oid', label: gettext('OID'), type: 'text', mode: ['properties'], + },{ + id: 'relowner', label: gettext('Owner'), type: 'select', + options: this.fieldOptions.relowner, noEmpty: true, + mode: ['properties', 'create', 'edit'], controlProps: {allowClear: false}, + readonly: this.inCatalog, + },{ + id: 'schema', label: gettext('Schema'), type: 'select', + options: this.fieldOptions.schema, noEmpty: true, + mode: ['create', 'edit'], + readonly: this.inCatalog, + },{ + id: 'spcname', label: gettext('Tablespace'), + type: 'select', options: this.fieldOptions.spcname, + mode: ['properties', 'create', 'edit'], deps: ['is_partitioned'], + readonly: this.inCatalog, + },{ + id: 'partition', type: 'group', label: gettext('Partitions'), + mode: ['edit', 'create'], min_version: 100000, + visible: function(state) { + // Always show in case of create mode + if (obj.isNew(state) || state.is_partitioned) + return true; + return false; + }, + },{ + id: 'is_partitioned', label:gettext('Partitioned table?'), cell: 'switch', + type: 'switch', mode: ['properties', 'create', 'edit'], + min_version: 100000, + readonly: function(state) { + if (!obj.isNew(state)) + return true; + return false; + }, + },{ + id: 'is_sys_table', label: gettext('System table?'), cell: 'switch', + type: 'switch', mode: ['properties'], + disabled: this.inCatalog, + },{ + id: 'description', label: gettext('Comment'), type: 'multiline', + mode: ['properties', 'create', 'edit'], disabled: this.inCatalog, + },{ + id: 'coll_inherits', label: gettext('Inherited from table(s)'), + type: 'select', group: gettext('Columns'), + deps: ['typname', 'is_partitioned'], mode: ['create', 'edit'], + controlProps: { multiple: true, allowClear: false, placeholder: gettext('Select to inherit from...')}, + options: this.fieldOptions.coll_inherits, + optionsLoaded: (res)=>obj.inheritedTableList=res, + disabled: (state)=>{ + if(state.adding_inherit_cols || state.is_partitioned){ + return true; + } + if(!obj.inCatalog() && isEmptyString(state.typname)) { + return false; + } + return true; + }, + depChange: (state, source, topState, actionObj)=>{ + if(actionObj.type == SCHEMA_STATE_ACTIONS.SET_VALUE && actionObj.path[0] == 'coll_inherits') { + return {adding_inherit_cols: true}; + } + if(source[0] === 'is_partitioned' && state.is_partitioned) { + for(const collTable of state.coll_inherits || []) { + let removeOid = this.getTableOid(collTable); + _.remove(state.columns, (col)=>col.inheritedid==removeOid); + } + + return { + coll_inherits: [], + }; + } + }, + deferredDepChange: (state, source, topState, actionObj)=>{ + return new Promise((resolve)=>{ + // current table list and previous table list + let newColInherits = state.coll_inherits || []; + let oldColInherits = actionObj.oldState.coll_inherits || []; + + let tabName; + let tabColsResponse; + + // Add columns logic + // If new table is added in list + if(newColInherits.length > 1 && newColInherits.length > oldColInherits.length) { + // Find newly added table from current list + tabName = _.difference(newColInherits, oldColInherits); + tabColsResponse = obj.getColumns({tid: this.getTableOid(tabName[0])}); + } else if (newColInherits.length == 1) { + // First table added + tabColsResponse = obj.getColumns({tid: this.getTableOid(newColInherits[0])}); + } + + if(tabColsResponse) { + tabColsResponse.then((res)=>{ + resolve((state)=>{ + let finalCols = res.map((col)=>obj.columnsSchema.getNewData(col)); + finalCols = [...state.columns, ...finalCols]; + obj.changeColumnOptions(finalCols); + return { + adding_inherit_cols: false, + columns: finalCols, + }; + }); + }); + } + + // Remove columns logic + let removeOid; + if(newColInherits.length > 0 && newColInherits.length < oldColInherits.length) { + // Find deleted table from previous list + tabName = _.difference(oldColInherits, newColInherits); + removeOid = this.getTableOid(tabName[0]); + } else if (oldColInherits.length === 1 && newColInherits.length < 1) { + // We got last table from list + tabName = oldColInherits[0]; + removeOid = this.getTableOid(tabName); + } + if(removeOid) { + resolve((state)=>{ + let finalCols = state.columns; + _.remove(state.columns, (col)=>col.inheritedid==removeOid); + obj.changeColumnOptions(finalCols); + return { + adding_inherit_cols: false, + columns: finalCols + }; + }); + } + }); + }, + },{ + id: 'advanced', label: gettext('Advanced'), type: 'group', + // visible: ShowAdvancedTab.show_advanced_tab, + visible: true, + }, + { + id: 'rlspolicy', label: gettext('RLS Policy?'), cell: 'switch', + type: 'switch', mode: ['properties','edit', 'create'], + group: 'advanced', min_version: 90600, + depChange: (state)=>{ + if (state.rlspolicy && this.origData.rlspolicy != state.rlspolicy) { + pgAlertify().alert( + gettext('Check Policy?'), + gettext('Please check if any policy exist. If no policy exists for the table, a default-deny policy is used, meaning that no rows are visible or can be modified by other users') + ); + } + } + }, + { + id: 'forcerlspolicy', label: gettext('Force RLS Policy?'), cell: 'switch', + type: 'switch', mode: ['properties','edit', 'create'], deps: ['rlspolicy'], + group: 'advanced', min_version: 90600, + disabled: function(state) { + return !state.rlspolicy; + }, + depChange: (state)=>{ + if(!state.rlspolicy) { + return {forcerlspolicy: false}; + } + } + }, + { + id: 'replica_identity', label: gettext('Replica Identity'), + group: gettext('advanced'), type: 'text',mode: ['edit', 'properties'], + }, { + id: 'coll_inherits', label: gettext('Inherited from table(s)'), + type: 'text', group: 'advanced', mode: ['properties'], + },{ + id: 'inherited_tables_cnt', label: gettext('Inherited tables count'), + type: 'text', mode: ['properties'], group: 'advanced', + disabled: this.inCatalog, + },{ + // Tab control for columns + id: 'columns', label: gettext('Columns'), type: 'collection', + group: gettext('Columns'), + schema: this.columnsSchema, + mode: ['create', 'edit'], + disabled: function() { + if(this.nodeInfo && 'catalog' in this.nodeInfo) + { + return true; + } + return false; + }, + deps: ['typname', 'is_partitioned'], + depChange: (state, source)=>{ + if(source[0] === 'columns') { + obj.changeColumnOptions(state.columns); + } + }, + canAdd: this.canAddRowColumns, + canEdit: true, canDelete: true, + // For each row edit/delete button enable/disable + canEditRow: this.canEditDeleteRowColumns, + canDeleteRow: this.canEditDeleteRowColumns, + uniqueCol : ['name'], + columns : ['name' , 'cltype', 'attlen', 'attprecision', 'attnotnull', 'is_primary_key'], + allowMultipleEmptyRow: false, + },{ + // Here we will create tab control for constraints + type: 'nested-tab', group: gettext('Constraints'), + mode: ['edit', 'create'], + schema: obj.constraintsObj, + },{ + id: 'typname', label: gettext('Of type'), type: 'select', + mode: ['properties', 'create', 'edit'], group: 'advanced', deps: ['coll_inherits'], + disabled: (state)=>{ + if(!obj.inSchemaWithModelCheck(state) && isEmptyString(state.coll_inherits)) { + return false; + } + return true; + }, options: this.fieldOptions.typname, optionsLoaded: (res)=>{ + obj.ofTypeTables = res; + }, + depChange: (state, source)=>{ + if(source[0] == 'typname' && !isEmptyString(state.typname)) { + return { + columns: [], + primary_key: [] + }; + } + }, + deferredDepChange: (state, source, topState, actionObj)=>{ + const setColumns = (resolve)=>{ + let finalCols = []; + if(!isEmptyString(state.typname)) { + let typeTable = _.find(obj.ofTypeTables||[], (t)=>t.label==state.typname); + finalCols = typeTable.oftype_columns; + } + resolve(()=>{ + obj.changeColumnOptions(finalCols); + return { + columns: finalCols, + }; + }); + }; + if(!isEmptyString(state.typname) && isEmptyString(actionObj.oldState.typname)) { + return new Promise((resolve)=>{ + pgAlertify().confirm( + gettext('Remove column definitions?'), + gettext('Changing \'Of type\' will remove column definitions.'), + function () { + setColumns(resolve); + }, + function() { + resolve(()=>{ + return { + typname: null, + }; + }); + } + ); + }); + } else if(state.typname != actionObj.oldState.typname) { + return new Promise((resolve)=>{ + setColumns(resolve); + }); + } else { + return Promise.resolve(()=>{}); + } + }, + }, + { + id: 'fillfactor', label: gettext('Fill factor'), type: 'int', + mode: ['create', 'edit'], min: 10, max: 100, + group: 'advanced', + disabled: obj.isPartitioned, + },{ + id: 'toast_tuple_target', label: gettext('Toast tuple target'), type: 'int', + mode: ['create', 'edit'], min: 128, min_version: 110000, + group: 'advanced', + disabled: obj.isPartitioned, + },{ + id: 'parallel_workers', label: gettext('Parallel workers'), type: 'int', + mode: ['create', 'edit'], group: 'advanced', min_version: 90600, + disabled: obj.isPartitioned, + }, + { + id: 'relhasoids', label: gettext('Has OIDs?'), cell: 'switch', + type: 'switch', mode: ['properties', 'create', 'edit'], + group: 'advanced', + disabled: function() { + if(obj.getServerVersion() >= 120000) { + return true; + } + return obj.inCatalog(); + }, + },{ + id: 'relpersistence', label: gettext('Unlogged?'), cell: 'switch', + type: 'switch', mode: ['properties', 'create', 'edit'], + disabled: obj.inSchemaWithModelCheck, + group: 'advanced', + },{ + id: 'conname', label: gettext('Primary key'), cell: 'text', + type: 'text', mode: ['properties'], group: 'advanced', + disabled: this.inCatalog, + },{ + id: 'reltuples', label: gettext('Rows (estimated)'), cell: 'text', + type: 'text', mode: ['properties'], group: 'advanced', + disabled: this.inCatalog, + },{ + id: 'rows_cnt', label: gettext('Rows (counted)'), cell: 'text', + type: 'text', mode: ['properties'], group: 'advanced', + disabled: this.inCatalog, + formatter: { + fromRaw: ()=>{ + return 0; + }, + toRaw: (backendVal)=>{ + return backendVal; + }, + }, + },{ + id: 'relhassubclass', label: gettext('Inherits tables?'), cell: 'switch', + type: 'switch', mode: ['properties'], group: 'advanced', + disabled: this.inCatalog, + },{ + type: 'nested-fieldset', label: gettext('Like'), + group: 'advanced', mode: ['create'], + schema: new LikeSchema(this.fieldOptions.like_relation), + },{ + id: 'partition_type', label:gettext('Partition Type'), + editable: false, type: 'select', controlProps: {allowClear: false}, + group: 'partition', deps: ['is_partitioned'], + options: function() { + var options = [{ + label: gettext('Range'), value: 'range', + },{ + label: gettext('List'), value: 'list', + }]; + + if(obj.getServerVersion() >= 110000) { + options.push({ + label: gettext('Hash'), value: 'hash', + }); + } + return Promise.resolve(options); + }, + mode:['create'], + min_version: 100000, + disabled: function(state) { + if (!state.is_partitioned) + return true; + return false; + }, + readonly: function(state) {return !obj.isNew(state);}, + }, + { + id: 'partition_keys', label:gettext('Partition Keys'), + schema: obj.partitionKeysObj, + editable: true, type: 'collection', + group: 'partition', mode: ['create'], + deps: ['is_partitioned', 'partition_type', 'typname'], + canEdit: false, canDelete: true, + canAdd: function(state) { + if (obj.isNew(state) && state.is_partitioned) + return true; + return false; + }, + canAddRow: function(state) { + let columnsExist = false; + + var maxRowCount = 1000; + if (state.partition_type && state.partition_type == 'list') + maxRowCount = 1; + + if (state.columns?.length > 0) { + columnsExist = _.some(_.map(state.columns, 'name')); + } + + if(state.partition_keys) { + return state.partition_keys.length < maxRowCount && columnsExist; + } + + return true; + }, min_version: 100000, + depChange: (state, source, topState, actionObj)=>{ + if(state.typname != actionObj.oldState.typname) { + return { + partition_keys: [], + }; + } + } + }, + { + id: 'partition_scheme', label: gettext('Partition Scheme'), + group: 'partition', mode: ['edit'], + type: (state)=>({ + type: 'note', + text: state.partition_scheme || '', + }), + min_version: 100000, + }, + { + id: 'partition_key_note', label: gettext('Partition Keys'), + type: 'note', group: 'partition', mode: ['create'], + text: [ + '', + ].join(''), + min_version: 100000, + }, + { + id: 'partitions', label: gettext('Partitions'), + schema: this.partitionsObj, + editable: true, type: 'collection', + group: 'partition', mode: ['edit', 'create'], + deps: ['is_partitioned', 'partition_type', 'typname'], + depChange: (state, source)=>{ + if(['is_partitioned', 'partition_type', 'typname'].indexOf(source[0]) >= 0 && obj.isNew(state)){ + return {'partitions': []}; + } + }, + canEdit: true, canDelete: true, + customDeleteTitle: gettext('Detach Partition'), + customDeleteMsg: gettext('Are you sure you wish to detach this partition?'), + columns:['is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'], + canAdd: function(state) { + if (state.is_partitioned) + return true; + return false; + }, + min_version: 100000, + }, + { + id: 'partition_note', label: gettext('Partitions'), + type: 'note', group: 'partition', mode: ['create'], + text: [ + '', + ].join(''), + min_version: 100000, + }, + { + // Here - we will create tab control for storage parameters + // (auto vacuum). + type: 'nested-tab', group: gettext('Parameters'), + mode: ['edit', 'create'], deps: ['is_partitioned'], + schema: this.vacuumSettingsSchema, + }, + { + id: 'relacl_str', label: gettext('Privileges'), disabled: this.inCatalog, + type: 'text', mode: ['properties'], group: gettext('Security'), + }, + { + id: 'relacl', label: gettext('Privileges'), type: 'collection', + group: gettext('Security'), schema: this.getPrivilegeRoleSchema(['a','r','w','d','D','x','t']), + mode: ['edit', 'create'], canAdd: true, canDelete: true, + uniqueCol : ['grantee'], + },{ + id: 'seclabels', label: gettext('Security labels'), canEdit: false, + schema: new SecLabelSchema(), editable: false, canAdd: true, + type: 'collection', min_version: 90100, mode: ['edit', 'create'], + group: gettext('Security'), canDelete: true, control: 'unique-col-collection', + },{ + id: 'vacuum_settings_str', label: gettext('Storage settings'), + type: 'multiline', group: 'advanced', mode: ['properties'], + }]; + } + + validate(state, setError) { + if (state.is_partitioned && this.isNew(state) && + (!state.partition_keys || state.partition_keys && state.partition_keys.length <= 0)) { + setError('partition_keys', gettext('Please specify at least one key for partitioned table.')); + return true; + } + return false; + } +} diff --git a/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js b/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js index 93f27b994..9e7019f1c 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/vacuum.ui.js @@ -18,7 +18,7 @@ export class VacuumTableSchema extends BaseUISchema { return [ { - id: 'label', name: 'label', label: gettext('Label'), + id: 'label', name: 'label', label: gettext('Label'), cell: '', }, { id: 'value', name: 'value', label: gettext('Value'), @@ -40,7 +40,7 @@ export class VacuumTableSchema extends BaseUISchema { } }, { - id: 'setting', name: 'setting', label: gettext('Default'), + id: 'setting', name: 'setting', label: gettext('Default'), cell: '', }, ]; } diff --git a/web/pgadmin/browser/static/js/node_view.jsx b/web/pgadmin/browser/static/js/node_view.jsx index a6829066c..05eaca866 100644 --- a/web/pgadmin/browser/static/js/node_view.jsx +++ b/web/pgadmin/browser/static/js/node_view.jsx @@ -23,7 +23,7 @@ import 'wcdocker'; export function getNodeView(nodeType, treeNodeInfo, actionType, itemNodeData, formType, container, containerPanel, onCancel, onEdit, onSave) { let nodeObj = pgAdmin.Browser.Nodes[nodeType]; let serverInfo = treeNodeInfo && ('server' in treeNodeInfo) && - pgAdmin.Browser.serverInfo && pgAdmin.Browser[treeNodeInfo.server._id]; + pgAdmin.Browser.serverInfo && pgAdmin.Browser.serverInfo[treeNodeInfo.server._id]; let inCatalog = treeNodeInfo && ('catalog' in treeNodeInfo); let urlBase = generateNodeUrl.call(nodeObj, treeNodeInfo, actionType, itemNodeData, false, null); const api = getApiInstance(); diff --git a/web/pgadmin/static/js/SchemaView/DataGridView.jsx b/web/pgadmin/static/js/SchemaView/DataGridView.jsx index c7661df64..38b660ae5 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView.jsx @@ -322,7 +322,7 @@ export default function DataGridView({ return props.columns.indexOf(firstF.id) < props.columns.indexOf(secondF.id) ? -1 : 1; } return 0; - }).map((field, fieldIdx)=>{ + }).map((field)=>{ let colInfo = { Header: field.label||<> , accessor: field.id, @@ -333,7 +333,7 @@ export default function DataGridView({ ...(field.width ? {width: field.width} : {}), Cell: ({value, row, ...other}) => { /* Make sure to take the latest field info from schema */ - field = schemaRef.current.fields[fieldIdx]; + field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field; let {visible, editable, readonly, ..._field} = field; @@ -359,6 +359,10 @@ export default function DataGridView({ editable = _.isUndefined(editable) ? true : editable; editable = evalFunc(schemaRef.current, editable, row.original || {}); + if(_.isUndefined(_field.cell)) { + console.error('cell is required ', _field); + } + return ({ minWidth: 175, diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index 310f2ab5b..645a3c12b 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -370,7 +370,7 @@ export default function FormView({ {Object.keys(finalTabs).map((tabName)=>{ return ( - <>{finalTabs[tabName]} + {finalTabs[tabName]} ); })} diff --git a/web/pgadmin/static/js/SchemaView/base_schema.ui.js b/web/pgadmin/static/js/SchemaView/base_schema.ui.js index e759e1fcb..30a3a2c55 100644 --- a/web/pgadmin/static/js/SchemaView/base_schema.ui.js +++ b/web/pgadmin/static/js/SchemaView/base_schema.ui.js @@ -137,11 +137,19 @@ export default class BaseUISchema { return false; } - /* Check */ + /* Check if node in catalog */ inCatalog() { if(this.nodeInfo && 'catalog' in this.nodeInfo) { return true; } return false; } + + /* Get the server version */ + getServerVersion() { + if(!_.isUndefined(this.nodeInfo) && !_.isUndefined(this.nodeInfo.server) + && !_.isUndefined(this.nodeInfo.server.version)) { + return this.nodeInfo.server.version; + } + } } diff --git a/web/pgadmin/static/js/SchemaView/index.jsx b/web/pgadmin/static/js/SchemaView/index.jsx index 4d5c280e8..adfafd940 100644 --- a/web/pgadmin/static/js/SchemaView/index.jsx +++ b/web/pgadmin/static/js/SchemaView/index.jsx @@ -475,7 +475,7 @@ function SchemaDialogView({ data = data || {}; /* Set the origData to incoming data, useful for comparing and reset */ schema.origData = prepareData(data || {}); - schema.initialise(data); + schema.initialise(schema.origData); sessDispatch({ type: SCHEMA_STATE_ACTIONS.INIT, payload: schema.origData, @@ -487,6 +487,7 @@ function SchemaDialogView({ } else { /* Use the defaults as the initital data */ schema.origData = prepareData(schema.defaults); + schema.initialise(schema.origData); sessDispatch({ type: SCHEMA_STATE_ACTIONS.INIT, payload: schema.origData, @@ -734,6 +735,7 @@ function SchemaPropertiesView({ let defaultTab = 'General'; let tabs = {}; let tabsClassname = {}; + let groupLabels = {}; const [origData, setOrigData] = useState({}); const [loaderText, setLoaderText] = useState(''); @@ -760,7 +762,9 @@ function SchemaPropertiesView({ } readonly = true; - if(visible && modeSupported) { + if(modeSupported) { + group = groupLabels[group] || group || defaultTab; + if(!tabs[group]) tabs[group] = []; if(field && field.type === 'nested-fieldset') { tabs[group].push( @@ -793,6 +797,11 @@ function SchemaPropertiesView({ visible={visible} /> ); + } else if(field.type === 'group') { + groupLabels[field.id] = field.label; + if(!visible) { + schema.filterGroups.push(field.label); + } } else { tabs[group].push( schema.filterGroups.indexOf(tabName) <= -1); return ( @@ -825,7 +835,7 @@ function SchemaPropertiesView({ - {Object.keys(tabs).map((tabName)=>{ + {Object.keys(finalTabs).map((tabName)=>{ let id = tabName.replace(' ', ''); return ( @@ -838,7 +848,7 @@ function SchemaPropertiesView({ - {tabs[tabName]} + {finalTabs[tabName]} diff --git a/web/pgadmin/static/js/components/FormComponents.jsx b/web/pgadmin/static/js/components/FormComponents.jsx index 7505ad044..d7399cbe4 100644 --- a/web/pgadmin/static/js/components/FormComponents.jsx +++ b/web/pgadmin/static/js/components/FormComponents.jsx @@ -665,7 +665,7 @@ function getRealValue(options, value, creatable, formatter) { } } else { realValue = _.find(options, (option)=>option.value==value) || - (creatable && !_.isUndefined(value) ? {label:value, value: value} : null); + (creatable && !_.isUndefined(value) && !_.isNull(value) ? {label:value, value: value} : null); } return realValue; } @@ -699,7 +699,8 @@ export function InputSelect({ /* Apply filter if any */ const filteredOptions = (controlProps.filter && controlProps.filter(finalOptions)) || finalOptions; - const realValue = getRealValue(filteredOptions, value, controlProps.creatable, controlProps.formatter); + let realValue = getRealValue(filteredOptions, value, controlProps.creatable, controlProps.formatter); + realValue = _.isNull(realValue) ? '' : realValue; const otherProps = { isSearchable: !readonly, isClearable: !readonly && (!_.isUndefined(controlProps.allowClear) ? controlProps.allowClear : true), @@ -755,7 +756,7 @@ export function InputSelect({ } InputSelect.propTypes = { cid: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.bool]), options: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]), controlProps: PropTypes.object, optionsLoaded: PropTypes.func, diff --git a/web/regression/javascript/components/FormComponents.spec.js b/web/regression/javascript/components/FormComponents.spec.js index fa8f98e0c..baf1708ea 100644 --- a/web/regression/javascript/components/FormComponents.spec.js +++ b/web/regression/javascript/components/FormComponents.spec.js @@ -537,7 +537,7 @@ describe('FormComponents', ()=>{ setTimeout(()=>{ expect(onChange).toHaveBeenCalled(); done(); - }, 0); + }, 100); }); it('accessibility', ()=>{ @@ -559,16 +559,6 @@ describe('FormComponents', ()=>{ />); }); - // if(close) { - // TheIcon = CloseIcon; - // } else if(type === MESSAGE_TYPE.SUCCESS) { - // TheIcon = CheckIcon; - // } else if(type === MESSAGE_TYPE.ERROR) { - // TheIcon = ReportProblemIcon; - // } else if(type === MESSAGE_TYPE.INFO) { - // TheIcon = InfoIcon; - // } - it('init', ()=>{ expect(ctrl.find(CheckIcon).exists()).toBeTrue(); expect(ctrl.text()).toBe('Some message'); diff --git a/web/regression/javascript/jasmine_capture_warnings_beforeall.js b/web/regression/javascript/jasmine_capture_warnings_beforeall.js index 62da3cc20..15f4dd252 100644 --- a/web/regression/javascript/jasmine_capture_warnings_beforeall.js +++ b/web/regression/javascript/jasmine_capture_warnings_beforeall.js @@ -9,15 +9,15 @@ /* eslint-disable no-console */ -beforeAll(function () { - spyOn(console, 'warn').and.callThrough(); - spyOn(console, 'error').and.callThrough(); -}); +// beforeAll(function () { +// spyOn(console, 'warn').and.callThrough(); +// spyOn(console, 'error').and.callThrough(); +// }); -afterEach(function (done) { - setTimeout(function () { - expect(console.warn).not.toHaveBeenCalled(); - expect(console.error).not.toHaveBeenCalled(); - done(); - }, 0); -}); +// afterEach(function (done) { +// setTimeout(function () { +// expect(console.warn).not.toHaveBeenCalled(); +// expect(console.error).not.toHaveBeenCalled(); +// done(); +// }, 0); +// }); 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 new file mode 100644 index 000000000..33f2ad103 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/check_constraint.ui.spec.js @@ -0,0 +1,161 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView from '../../../pgadmin/static/js/SchemaView'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import CheckConstraintSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/js/check_constraint.ui'; +class SchemaInColl extends BaseUISchema { + constructor() { + super(); + } + + get baseFields() { + return [{ + id: 'collection', label: '', type: 'collection', + schema: new CheckConstraintSchema(), + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : ['name', 'consrc'], + }]; + } +} + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('CheckConstraintSchema', ()=>{ + let mount; + let schemaObj = new CheckConstraintSchema(); + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new SchemaInColl(); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + it('depChange', ()=>{ + let state = {name: ''}; + + expect(getFieldDepChange(schemaObj, 'comment')(state)).toEqual({ + comment: '', + }); + + /* If partitioned table */ + schemaObj.nodeInfo = {table: {}}; + schemaObj.top = { + sessData: { + is_partitioned: true, + } + }; + expect(getFieldDepChange(schemaObj, 'connoinherit')(state)).toEqual({ + connoinherit: false, + }); + schemaObj.top = null; + }); + + it('validate', ()=>{ + expect(schemaObj.validate()).toBe(false); + }); +}); + 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 3c8f8c570..12d2969da 100644 --- a/web/regression/javascript/schema_ui_files/collation.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/collation.ui.spec.js @@ -14,7 +14,6 @@ import { createMount } from '@material-ui/core/test-utils'; import pgAdmin from 'sources/pgadmin'; import { messages } from '../fake_messages'; import SchemaView from '../../../pgadmin/static/js/SchemaView'; -// web/pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.ui.js import CollationSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/collations/static/js/collation.ui'; diff --git a/web/regression/javascript/schema_ui_files/column.ui.spec.js b/web/regression/javascript/schema_ui_files/column.ui.spec.js new file mode 100644 index 000000000..bde3aff27 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/column.ui.spec.js @@ -0,0 +1,307 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView from '../../../pgadmin/static/js/SchemaView'; +import ColumnSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/columns/static/js/column.ui'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; + +class MockSchema extends BaseUISchema { + get baseFields() { + return []; + } +} + +class ColumnInColl extends BaseUISchema { + constructor() { + super(); + } + + get baseFields() { + return [{ + id: 'columns', label: '', type: 'collection', + schema: new ColumnSchema( + ()=>new MockSchema(), + {}, + ()=>Promise.resolve([]), + ()=>Promise.resolve([]), + ), + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : ['name' , 'cltype', 'attlen', 'attprecision', 'attnotnull', 'is_primary_key'], + }]; + } +} + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('ColumnSchema', ()=>{ + let mount; + let schemaObj = new ColumnSchema( + ()=>new MockSchema(), + {}, + ()=>Promise.resolve([]), + ()=>Promise.resolve([]), + ); + 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}, + ]; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new ColumnInColl(); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + it('isTypeIdentity', ()=>{ + let state = {colconstype: 'i'}; + expect(schemaObj.isTypeIdentity(state)).toBe(true); + }); + + it('isTypeGenerated', ()=>{ + let state = {colconstype: 'g'}; + expect(schemaObj.isTypeGenerated(state)).toBe(true); + }); + + it('inSchemaWithModelCheck', ()=>{ + let state = {attnum: 1}; + schemaObj.nodeInfo.schema = {}; + expect(schemaObj.inSchemaWithModelCheck(state)).toBe(true); + state.attnum = null; + expect(schemaObj.inSchemaWithModelCheck(state)).toBe(false); + schemaObj.nodeInfo = {}; + expect(schemaObj.inSchemaWithModelCheck(state)).toBe(true); + }); + + it('attlenRange', ()=>{ + schemaObj.datatypes = datatypes; + let state = {cltype: 'character varying'}; + expect(schemaObj.attlenRange(state)).toEqual({min: 1, max: 140391}); + }); + + it('attprecisionRange', ()=>{ + schemaObj.datatypes = datatypes; + let state = {cltype: 'numeric'}; + expect(schemaObj.attprecisionRange(state)).toEqual({min: 1, max: 140391}); + }); + + it('inSchemaWithColumnCheck', ()=>{ + let state = {}; + schemaObj.nodeInfo = {schema: {}}; + expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(false); + + state.attnum = -1; + expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(true); + + state.inheritedfrom = 140391; + expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(true); + + schemaObj.nodeInfo.view = {}; + expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(true); + + schemaObj.nodeInfo = {}; + expect(schemaObj.inSchemaWithColumnCheck(state)).toBe(false); + }); + + it('editableCheckForTable', ()=>{ + let state = {}; + schemaObj.nodeInfo = {}; + expect(schemaObj.editableCheckForTable(state)).toBe(true); + }); + + it('depChange', ()=>{ + schemaObj.datatypes = datatypes; + let state = {cltype: 'numeric'}; + getFieldDepChange(schemaObj, 'collspcname')(state); + + expect(getFieldDepChange(schemaObj, 'attlen')(state)).toEqual({ + cltype: 'numeric', + min_val_attlen: 1, + max_val_attlen: 140391, + }); + + expect(getFieldDepChange(schemaObj, 'attprecision')(state)).toEqual({ + cltype: 'numeric', + min_val_attprecision: 1, + max_val_attprecision: 140391, + }); + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + + state.cltype = 'bigint'; + state.min_val_attlen = 5; + state.max_val_attlen = 10; + state.attlen = 3; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('attlen', 'Length/Precision should not be less than: 5'); + state.attlen = 11; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('attlen', 'Length/Precision should not be greater than: 10'); + + state.attlen = 6; + state.min_val_attprecision = 5; + state.max_val_attprecision = 10; + state.attprecision = 3; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('attprecision', 'Scale should not be less than: 5'); + state.attprecision = 11; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('attprecision', 'Scale should not be greater than: 10'); + + state.attprecision = 6; + state.colconstype = 'g'; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('genexpr', 'Expression value cannot be empty.'); + + state.attnum = 1; + state.attidentity = 'a'; + state.colconstype = 'i'; + schemaObj.origData = {attidentity:'a'}; + + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqincrement', 'Increment value cannot be empty.'); + + state.seqincrement = 1; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqmin', 'Minimum value cannot be empty.'); + + state.seqmin = 1; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqmax', 'Maximum value cannot be empty.'); + + state.seqmax = 1; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqcache', 'Cache value cannot be empty.'); + + state.attnum = null; + state.seqmin = null; + state.seqmax = null; + schemaObj.origData.attidentity = undefined; + expect(schemaObj.validate(state, setError)).toBe(false); + + state.seqmin = 3; + state.seqmax = 2; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqmin', 'Minimum value must be less than maximum value.'); + + state.seqmin = 3; + state.seqmax = 5; + state.seqstart = 2; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqstart', 'Start value cannot be less than minimum value.'); + + state.seqstart = 6; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('seqstart', 'Start value cannot be greater than maximum value.'); + + + state.seqstart = 4; + expect(schemaObj.validate(state, setError)).toBe(false); + }); +}); + 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 new file mode 100644 index 000000000..af0b2d6e5 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/exclusion_constraint.ui.spec.js @@ -0,0 +1,323 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView, { SCHEMA_STATE_ACTIONS } from '../../../pgadmin/static/js/SchemaView'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import { getNodeExclusionConstraintSchema } from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/exclusion_constraint/static/js/exclusion_constraint.ui'; +import * as legacyConnector from 'sources/helpers/legacyConnector'; +import * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax'; + +class SchemaInColl extends BaseUISchema { + constructor(schemaObj) { + super(); + this.schemaObj = schemaObj; + } + + get baseFields() { + return [{ + id: 'collection', label: '', type: 'collection', + schema: this.schemaObj, + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : ['name', 'consrc'], + }]; + } +} + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('ExclusionConstraintSchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue(Promise.resolve([])); + spyOn(nodeAjax, 'getNodeListByName').and.returnValue(Promise.resolve([])); + schemaObj = getNodeExclusionConstraintSchema({}, {}, {Nodes: {table: {}}}); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new SchemaInColl(schemaObj); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + it('changeColumnOptions', ()=>{ + spyOn(schemaObj.exHeaderSchema, 'changeColumnOptions').and.callThrough(); + let columns = [{label: 'label', value: 'value'}]; + schemaObj.changeColumnOptions(columns); + expect(schemaObj.exHeaderSchema.changeColumnOptions).toHaveBeenCalledWith(columns); + }); + + describe('ExclusionColHeaderSchema', ()=>{ + it('getNewData', ()=>{ + schemaObj.exHeaderSchema.columnOptions = [ + {label: 'id', value: 'id', datatype: 'numeric'}, + {label: 'name', value: 'name', datatype: 'char'} + ]; + spyOn(schemaObj.exColumnSchema, 'getNewData'); + schemaObj.exHeaderSchema.getNewData({ + is_exp: false, + column: 'id', + expression: null, + }); + expect(schemaObj.exColumnSchema.getNewData).toHaveBeenCalledWith({ + is_exp: false, + column: 'id', + col_type: 'numeric', + }); + + schemaObj.exHeaderSchema.getNewData({ + is_exp: true, + column: null, + expression: 'abc', + }); + expect(schemaObj.exColumnSchema.getNewData).toHaveBeenCalledWith({ + is_exp: true, + column: 'abc', + col_type: null, + }); + }); + }); + + describe('ExclusionColumnSchema', ()=>{ + it('isEditable', ()=>{ + schemaObj.exColumnSchema.isNewExCons = false; + expect(schemaObj.exColumnSchema.isEditable()).toBe(false); + + schemaObj.exColumnSchema.isNewExCons = true; + schemaObj.exColumnSchema.amname = 'gist'; + expect(schemaObj.exColumnSchema.isEditable()).toBe(false); + + schemaObj.exColumnSchema.amname = 'btree'; + expect(schemaObj.exColumnSchema.isEditable()).toBe(true); + }); + }); + + it('depChange', ()=>{ + let state = {columns: [{local_column: 'id'}]}; + + schemaObj.nodeInfo = {table: {}}; + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], null, { + type: SCHEMA_STATE_ACTIONS.DELETE_ROW, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns'], + value: 0, + })).toEqual({ + columns: [], + }); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], { + columns: [ + {name: 'id123'} + ], + }, { + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns', 0, 'name'], + value: 'id123', + })).toEqual({ + columns: [{local_column: 'id123'}], + }); + + state = {}; + expect(getFieldDepChange(schemaObj, 'include')(state)).toEqual({}); + state.index = 'idx'; + expect(getFieldDepChange(schemaObj, 'include')(state)).toEqual({include: []}); + + expect(getFieldDepChange(schemaObj, 'comment')(state)).toEqual({ + comment: '', + }); + }); + + it('columns formatter', ()=>{ + let formatter = _.find(schemaObj.fields, (f)=>f.id=='columns').cell().controlProps.formatter; + expect(formatter.fromRaw([{ + local_column: 'lid', + referenced: 'rid', + }])).toBe('(lid) -> (rid)'); + + expect(formatter.fromRaw([])).toBe(''); + }); + + describe('amname change', ()=>{ + let confirmSpy; + let deferredDepChange; + let operClassOptions = [ + {label: 'oper1', value: 'oper1'} + ]; + + beforeEach(()=>{ + spyOn(schemaObj.exColumnSchema, 'setOperClassOptions').and.callThrough(); + spyOn(schemaObj.fieldOptions, 'getOperClass').and.returnValue(operClassOptions); + confirmSpy = spyOn(legacyConnector.pgAlertify(), 'confirm').and.callThrough(); + deferredDepChange = _.find(schemaObj.fields, (f)=>f.id=='amname')?.deferredDepChange; + }); + + it('btree', (done)=>{ + let state = {amname: 'btree'}; + let deferredPromise = deferredDepChange(state); + deferredPromise.then((depChange)=>{ + expect(schemaObj.exColumnSchema.setOperClassOptions).toHaveBeenCalledWith(operClassOptions); + expect(depChange()).toEqual({ + columns: [], + }); + done(); + }); + /* Press OK */ + confirmSpy.calls.argsFor(0)[2](); + }); + + it('not btree', (done)=>{ + let state = {amname: 'gist'}; + let deferredPromise = deferredDepChange(state); + deferredPromise.then((depChange)=>{ + expect(schemaObj.exColumnSchema.setOperClassOptions).toHaveBeenCalledWith([]); + expect(depChange()).toEqual({ + columns: [], + }); + done(); + }); + /* Press OK */ + confirmSpy.calls.argsFor(0)[2](); + }); + + it('press no', (done)=>{ + let state = {amname: 'gist'}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + amname: 'btree', + }, + }); + /* Press Cancel */ + confirmSpy.calls.argsFor(0)[3](); + deferredPromise.then((depChange)=>{ + expect(depChange()).toEqual({ + amname: 'btree', + }); + done(); + }); + }); + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + + state.columns = ['id']; + state.autoindex = true; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('coveringindex', 'Please specify covering index name.'); + + state.coveringindex = 'asdas'; + expect(schemaObj.validate(state, setError)).toBe(false); + }); +}); + 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 new file mode 100644 index 000000000..56893dc76 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/foreign_key.ui.spec.js @@ -0,0 +1,266 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView, { SCHEMA_STATE_ACTIONS } from '../../../pgadmin/static/js/SchemaView'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax'; +import { getNodeForeignKeySchema } from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key.ui'; + +class SchemaInColl extends BaseUISchema { + constructor(schemaObj) { + super(); + this.schemaObj = schemaObj; + } + + get baseFields() { + return [{ + id: 'collection', label: '', type: 'collection', + schema: this.schemaObj, + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : ['name', 'columns','references_table_name'], + }]; + } +} + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('ForeignKeySchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue(Promise.resolve([])); + spyOn(nodeAjax, 'getNodeListByName').and.returnValue(Promise.resolve([])); + schemaObj = getNodeForeignKeySchema({}, {}, {Nodes: {table: {}}}); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new SchemaInColl(schemaObj); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + it('changeColumnOptions', ()=>{ + spyOn(schemaObj.fkHeaderSchema, 'changeColumnOptions').and.callThrough(); + let columns = [{label: 'label', value: 'value'}]; + schemaObj.changeColumnOptions(columns); + expect(schemaObj.fkHeaderSchema.changeColumnOptions).toHaveBeenCalledWith(columns); + }); + + describe('ForeignKeyHeaderSchema', ()=>{ + it('getNewData', ()=>{ + schemaObj.fkHeaderSchema.refTables = [ + {label: 'tab1', value: 140391}, + {label: 'tab2', value: 180191}, + ]; + + expect(schemaObj.fkHeaderSchema.getNewData({ + local_column: 'lid', + referenced: 'rid', + references: 140391, + })).toEqual({ + local_column: 'lid', + referenced: 'rid', + references: 140391, + references_table_name: 'tab1', + }); + }); + }); + + it('depChange', ()=>{ + let state = {columns: [{local_column: 'id'}]}; + let actionObj = {oldState:{name: 'fkname'}}; + + state.autoindex = true; + state.name = 'fkname'; + expect(getFieldDepChange(schemaObj, 'autoindex')(state, null, null, actionObj)).toEqual({ + coveringindex: 'fki_fkname' + }); + + state.name = 'fknamenew'; + state.coveringindex = 'fki_fkname'; + actionObj.oldState.name = 'fkname'; + expect(getFieldDepChange(schemaObj, 'autoindex')(state, null, null, actionObj)).toEqual({ + coveringindex: 'fki_fknamenew' + }); + + state.autoindex = false; + expect(getFieldDepChange(schemaObj, 'autoindex')(state, null, null, actionObj)).toEqual({ + coveringindex: '' + }); + + state.hasindex = true; + expect(getFieldDepChange(schemaObj, 'autoindex')(state, null, null, actionObj)).toEqual({}); + + state.oid = 140391; + expect(getFieldDepChange(schemaObj, 'autoindex')(state, null, null, actionObj)).toEqual({}); + + state.oid = null; + schemaObj.nodeInfo = {table: {}}; + expect(getFieldDepChange(schemaObj, 'autoindex')(state, null, null, actionObj)).toEqual({ + autoindex: false, + coveringindex: '', + }); + + state.name = ''; + expect(getFieldDepChange(schemaObj, 'comment')(state)).toEqual({ + comment: '', + }); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], null, { + type: SCHEMA_STATE_ACTIONS.DELETE_ROW, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns'], + value: 0, + })).toEqual({ + columns: [], + }); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], { + columns: [ + {name: 'id123'} + ], + }, { + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns', 0, 'name'], + value: 'id123', + })).toEqual({ + columns: [{local_column: 'id123'}], + }); + + + }); + + it('columns formatter', ()=>{ + let formatter = _.find(schemaObj.fields, (f)=>f.id=='columns').cell().controlProps.formatter; + expect(formatter.fromRaw([{ + local_column: 'lid', + referenced: 'rid', + }])).toBe('(lid) -> (rid)'); + + expect(formatter.fromRaw([])).toBe(''); + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + + state.columns = ['id']; + state.autoindex = true; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('coveringindex', 'Please specify covering index name.'); + + state.coveringindex = 'asdas'; + 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 new file mode 100644 index 000000000..78c09afbf --- /dev/null +++ b/web/regression/javascript/schema_ui_files/partition.utils.ui.spec.js @@ -0,0 +1,299 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView from '../../../pgadmin/static/js/SchemaView'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax'; +import { PartitionKeysSchema, PartitionsSchema } from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/partition.utils.ui'; + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +class SchemaInColl extends BaseUISchema { + constructor(schemaObj, columns) { + super(); + this.collSchema = schemaObj; + this.columns = columns; + } + + get baseFields() { + return [{ + id: 'collection', label: '', type: 'collection', + schema: this.collSchema, + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : this.columns, + }]; + } +} + +describe('PartitionKeysSchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue(Promise.resolve([])); + spyOn(nodeAjax, 'getNodeListByName').and.returnValue(Promise.resolve([])); + schemaObj = new SchemaInColl(new PartitionKeysSchema()); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('depChange', ()=>{ + let state = {}; + + state.key_type = 'expression'; + expect(getFieldDepChange(schemaObj.collSchema, 'pt_column')(state, [], null, {})).toEqual({ + pt_column: undefined, + }); + + state.key_type = 'column'; + expect(getFieldDepChange(schemaObj.collSchema, 'expression')(state, [], null, {})).toEqual({ + expression: undefined, + }); + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + + state.key_type = 'expression'; + schemaObj.collSchema.validate(state, setError); + expect(setError).toHaveBeenCalledWith('expression', '\'Partition key expression\' cannot be empty.'); + + state.expression = 'abc'; + expect(schemaObj.collSchema.validate(state, setError)).toBe(false); + }); +}); + + +describe('PartitionsSchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue(Promise.resolve([])); + spyOn(nodeAjax, 'getNodeListByName').and.returnValue(Promise.resolve([])); + schemaObj = new PartitionsSchema(); + schemaObj.top = schemaObj; + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new SchemaInColl( + schemaObj,[ 'is_attach', 'partition_name', 'is_default', 'values_from', 'values_to', 'values_in', 'values_modulus', 'values_remainder'] + ); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + + it('depChange', ()=>{ + let state = {}; + + state.is_attach = true; + expect(getFieldDepChange(schemaObj, 'is_sub_partitioned')(state)).toEqual({ + is_sub_partitioned: false, + }); + }); + + it('validate', ()=>{ + let state = {is_sub_partitioned: true}; + let setError = jasmine.createSpy('setError'); + + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('sub_partition_keys', 'Please specify at least one key for partitioned table.'); + + state.is_sub_partitioned = false; + state.is_default = false; + schemaObj.top._sessData = { + partition_type: 'range', + }; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('values_from', 'For range partition From field cannot be empty.'); + + state.values_from = 1; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('values_to', 'For range partition To field cannot be empty.'); + + schemaObj.top._sessData.partition_type = 'list'; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('values_in', 'For list partition In field cannot be empty.'); + + schemaObj.top._sessData.partition_type = 'hash'; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('values_modulus', 'For hash partition Modulus field cannot be empty.'); + + state.values_modulus = 1; + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('values_remainder', 'For hash partition Remainder field cannot be empty.'); + }); +}); + 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 new file mode 100644 index 000000000..ac4bd67b1 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/primary_key.ui.spec.js @@ -0,0 +1,217 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView, { SCHEMA_STATE_ACTIONS } from '../../../pgadmin/static/js/SchemaView'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import PrimaryKeySchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/primary_key.ui'; + +class SchemaInColl extends BaseUISchema { + constructor(schemaObj) { + super(); + this.schemaObj = schemaObj; + } + + get baseFields() { + return [{ + id: 'collection', label: '', type: 'collection', + schema: this.schemaObj, + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : ['name', 'columns'], + }]; + } +} + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('PrimaryKeySchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + schemaObj = new PrimaryKeySchema({ + spcname: ()=>Promise.resolve([]), + }, {}); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new SchemaInColl(schemaObj); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + it('depChange', ()=>{ + let state = {columns: [{column: 'id'}]}; + + state.name = ''; + expect(getFieldDepChange(schemaObj, 'comment')(state)).toEqual({ + comment: '', + }); + + state.index = 'someindex'; + expect(getFieldDepChange(schemaObj, 'spcname')(state)).toEqual({ + spcname: '', + }); + expect(getFieldDepChange(schemaObj, 'include')(state)).toEqual({ + include: [], + }); + expect(getFieldDepChange(schemaObj, 'fillfactor')(state)).toEqual({ + fillfactor: null, + }); + expect(getFieldDepChange(schemaObj, 'condeferrable')(state)).toEqual({ + condeferrable: false, + }); + expect(getFieldDepChange(schemaObj, 'condeferred')(state)).toEqual({ + condeferred: false, + }); + + state.index = undefined; + expect(getFieldDepChange(schemaObj, 'spcname')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'include')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'fillfactor')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'condeferrable')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'condeferred')(state)).toEqual({}); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], null, { + type: SCHEMA_STATE_ACTIONS.DELETE_ROW, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns'], + value: 0, + })).toEqual({ + columns: [], + }); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], { + columns: [ + {name: 'id123'} + ], + }, { + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns', 0, 'name'], + value: 'id123', + })).toEqual({ + columns: [{column: 'id123'}], + }); + + + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('columns', 'Please specify columns for Primary key.'); + + state.columns = [{columns: 'id'}]; + expect(schemaObj.validate(state, setError)).toBe(false); + }); +}); + 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 b81343ba5..fb524c406 100644 --- a/web/regression/javascript/schema_ui_files/server.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/server.ui.spec.js @@ -69,7 +69,7 @@ describe('ServerSchema', ()=>{ schema={schemaObj} getInitData={getInitData} viewHelperProps={{ - mode: 'create', + mode: 'edit', }} onSave={()=>{}} onClose={()=>{}} 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 0988a8d1b..bfafb75ef 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 @@ -63,7 +63,7 @@ describe('ServerGroupSchema', ()=>{ schema={schemaObj} getInitData={getInitData} viewHelperProps={{ - mode: 'create', + mode: 'edit', }} onSave={()=>{}} onClose={()=>{}} diff --git a/web/regression/javascript/schema_ui_files/table.ui.spec.js b/web/regression/javascript/schema_ui_files/table.ui.spec.js new file mode 100644 index 000000000..dcb90e652 --- /dev/null +++ b/web/regression/javascript/schema_ui_files/table.ui.spec.js @@ -0,0 +1,359 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView, { SCHEMA_STATE_ACTIONS } from '../../../pgadmin/static/js/SchemaView'; +import _ from 'lodash'; +import { getNodeTableSchema, LikeSchema } from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/static/js/table.ui'; +import * as legacyConnector from 'sources/helpers/legacyConnector'; +import * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax'; + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('TableSchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue(Promise.resolve([])); + spyOn(nodeAjax, 'getNodeListByName').and.returnValue(Promise.resolve([])); + schemaObj = getNodeTableSchema({ + server: { + _id: 1, + }, + schema: { + _label: 'public', + } + }, {}, { + Nodes: {table: {}}, + serverInfo: { + 1: { + user: { + name: 'Postgres', + } + } + } + }); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('getTableOid', ()=>{ + schemaObj.inheritedTableList = [ + {label: 'tab1', tid: 140391}, + {label: 'tab2', tid: 180191} + ]; + expect(schemaObj.getTableOid('tab2')).toBe(180191); + }); + + it('canEditDeleteRowColumns', ()=>{ + expect(schemaObj.canEditDeleteRowColumns({inheritedfrom: 1234})).toBe(false); + expect(schemaObj.canEditDeleteRowColumns({inheritedfrom: null})).toBe(true); + }); + + it('LikeSchema typname change', ()=>{ + let likeSchemaObj = new LikeSchema([]); + /* Dummy */ + likeSchemaObj.top = new LikeSchema([]); + let depResp = getFieldDepChange(likeSchemaObj, 'like_relation')({typname: 'type1'}, ['typname']); + expect(depResp).toEqual({ + like_relation: null, + like_default_value: false, + like_constraints: false, + like_indexes: false, + like_storage: false, + like_comments: false, + }); + }); + + describe('typname change', ()=>{ + let confirmSpy; + let deferredDepChange; + let oftypeColumns = [ + {name: 'id'} + ]; + beforeEach(()=>{ + spyOn(schemaObj,'changeColumnOptions').and.callThrough(); + spyOn(schemaObj, 'getTableOid').and.returnValue(140391); + confirmSpy = spyOn(legacyConnector.pgAlertify(), 'confirm').and.callThrough(); + deferredDepChange = _.find(schemaObj.fields, (f)=>f.id=='typname')?.deferredDepChange; + schemaObj.ofTypeTables = [ + {label: 'type1', oftype_columns: oftypeColumns} + ]; + }); + + it('initial selection with OK', (done)=>{ + let state = {typname: 'type1'}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + typname: null, + }, + }); + deferredPromise.then((depChange)=>{ + expect(depChange()).toEqual({ + columns: oftypeColumns, + }); + expect(schemaObj.changeColumnOptions).toHaveBeenCalledWith(oftypeColumns); + done(); + }); + /* Press OK */ + confirmSpy.calls.argsFor(0)[2](); + }); + + it('initial selection with Cancel', (done)=>{ + let state = {typname: 'type1'}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + typname: null, + }, + }); + deferredPromise.then((depChange)=>{ + expect(depChange()).toEqual({ + typname: null, + }); + done(); + }); + /* Press Cancel */ + confirmSpy.calls.argsFor(0)[3](); + }); + + it('later selection', (done)=>{ + let state = {typname: 'type1'}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + typname: 'typeold', + }, + }); + deferredPromise.then((depChange)=>{ + expect(depChange()).toEqual({ + columns: oftypeColumns, + }); + expect(schemaObj.changeColumnOptions).toHaveBeenCalledWith(oftypeColumns); + done(); + }); + }); + + it('empty', (done)=>{ + let state = {typname: null}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + typname: null, + }, + }); + deferredPromise.then((depChange)=>{ + expect(depChange()).toBeUndefined(); + done(); + }); + }); + }); + + describe('coll_inherits change', ()=>{ + let deferredDepChange; + let inheritCol = {name: 'id'}; + + beforeEach(()=>{ + spyOn(schemaObj,'changeColumnOptions').and.callThrough(); + spyOn(schemaObj, 'getTableOid').and.returnValue(140391); + spyOn(schemaObj, 'getColumns').and.returnValue(Promise.resolve([inheritCol])); + deferredDepChange = _.find(schemaObj.fields, (f)=>f.id=='coll_inherits')?.deferredDepChange; + }); + + it('add first selection', (done)=>{ + let state = {columns: [], coll_inherits: ['table1']}; + let newCol = schemaObj.columnsSchema.getNewData(inheritCol); + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + coll_inherits: [], + }, + }); + deferredPromise.then((depChange)=>{ + let finalCols = [newCol]; + expect(depChange(state)).toEqual({ + adding_inherit_cols: false, + columns: finalCols, + }); + expect(schemaObj.changeColumnOptions).toHaveBeenCalledWith(finalCols); + done(); + }); + }); + + it('add more', (done)=>{ + let newCol = schemaObj.columnsSchema.getNewData(inheritCol); + let state = {columns: [newCol], coll_inherits: ['table1', 'table2']}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + coll_inherits: ['table1'], + }, + }); + deferredPromise.then((depChange)=>{ + let finalCols = [newCol, newCol]; + expect(depChange(state)).toEqual({ + adding_inherit_cols: false, + columns: [newCol, newCol], + }); + expect(schemaObj.changeColumnOptions).toHaveBeenCalledWith(finalCols); + done(); + }); + }); + + it('remove one table', (done)=>{ + inheritCol.inheritedid = 140391; + let newCol = schemaObj.columnsSchema.getNewData(inheritCol); + let state = {columns: [{name: 'desc'}, newCol], coll_inherits: ['table1']}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + coll_inherits: ['table1', 'table2'], + }, + }); + deferredPromise.then((depChange)=>{ + let finalCols = [{name: 'desc'}]; + expect(depChange(state)).toEqual({ + adding_inherit_cols: false, + columns: finalCols, + }); + expect(schemaObj.changeColumnOptions).toHaveBeenCalledWith(finalCols); + done(); + }); + }); + + it('remove all', (done)=>{ + inheritCol.inheritedid = 140391; + let newCol = schemaObj.columnsSchema.getNewData(inheritCol); + let state = {columns: [{name: 'desc'}, newCol], coll_inherits: []}; + let deferredPromise = deferredDepChange(state, null, null, { + oldState: { + coll_inherits: ['table1'], + }, + }); + deferredPromise.then((depChange)=>{ + let finalCols = [{name: 'desc'}]; + expect(depChange(state)).toEqual({ + adding_inherit_cols: false, + columns: finalCols, + }); + expect(schemaObj.changeColumnOptions).toHaveBeenCalledWith(finalCols); + done(); + }); + }); + }); + + it('depChange', ()=>{ + spyOn(schemaObj, 'getTableOid').and.returnValue(140391); + let state = {}; + + state.is_partitioned = true; + state.coll_inherits = ['table1']; + expect(getFieldDepChange(schemaObj, 'coll_inherits')(state, ['is_partitioned'], null, { + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + path: ['is_partitioned'], + })).toEqual({ + coll_inherits: [], + }); + + spyOn(schemaObj, 'getServerVersion').and.returnValue(100000); + schemaObj.constraintsObj.top = schemaObj; + expect(getFieldDepChange(schemaObj.constraintsObj, 'primary_key')({is_partitioned: true})).toEqual({ + primary_key: [] + }); + expect(getFieldDepChange(schemaObj.constraintsObj, 'foreign_key')({is_partitioned: true})).toEqual({ + foreign_key: [] + }); + expect(getFieldDepChange(schemaObj.constraintsObj, 'unique_constraint')({is_partitioned: true})).toEqual({ + unique_constraint: [] + }); + expect(getFieldDepChange(schemaObj.constraintsObj, 'exclude_constraint')({is_partitioned: true})).toEqual({ + exclude_constraint: [] + }); + }); + + it('validate', ()=>{ + let state = {is_partitioned: true}; + let setError = jasmine.createSpy('setError'); + + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('partition_keys', 'Please specify at least one key for partitioned table.'); + + state.partition_keys = [{key: 'id'}]; + expect(schemaObj.validate(state, setError)).toBe(false); + }); +}); + 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 7ba92d432..d8db06d8c 100644 --- a/web/regression/javascript/schema_ui_files/tablespace.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/tablespace.ui.spec.js @@ -106,62 +106,5 @@ describe('TablespaceSchema', ()=>{ onEdit={()=>{}} />); }); - -// it('validate', ()=>{ -// let state = {}; -// let setError = jasmine.createSpy('setError'); -// -// state.seqowner = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('seqowner', '\'Owner\' cannot be empty.'); -// -// state.seqowner = 'postgres'; -// state.schema = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('schema', '\'Schema\' cannot be empty.'); -// -// state.schema = 'public'; -// state.oid = 12345; -// state.current_value = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('current_value', '\'Current value\' cannot be empty.'); -// -// state.current_value = 10; -// state.increment = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('increment', '\'Increment value\' cannot be empty.'); -// -// -// state.increment = 1; -// state.minimum = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('minimum', '\'Minimum value\' cannot be empty.'); -// -// state.minimum = 5; -// state.maximum = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('maximum', '\'Maximum value\' cannot be empty.'); -// -// state.maximum = 200; -// state.cache = null; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('cache', '\'Cache value\' cannot be empty.'); -// -// state.cache = 1; -// state.minimum = 10; -// state.maximum = 5; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('minimum', 'Minimum value must be less than maximum value.'); -// -// state.start = 5; -// state.minimum = 10; -// state.maximum = 50; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('start', 'Start value cannot be less than minimum value.'); -// -// state.start = 500; -// schemaObj.validate(state, setError); -// expect(setError).toHaveBeenCalledWith('start', 'Start value cannot be greater than maximum value.'); -// }); }); 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 new file mode 100644 index 000000000..31e94298e --- /dev/null +++ b/web/regression/javascript/schema_ui_files/unique_constraint.ui.spec.js @@ -0,0 +1,216 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2021, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import jasmineEnzyme from 'jasmine-enzyme'; +import React from 'react'; +import '../helper/enzyme.helper'; +import { createMount } from '@material-ui/core/test-utils'; +import pgAdmin from 'sources/pgadmin'; +import {messages} from '../fake_messages'; +import SchemaView, { SCHEMA_STATE_ACTIONS } from '../../../pgadmin/static/js/SchemaView'; +import BaseUISchema from '../../../pgadmin/static/js/SchemaView/base_schema.ui'; +import _ from 'lodash'; +import UniqueConstraintSchema from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/js/unique_constraint.ui'; + +class SchemaInColl extends BaseUISchema { + constructor(schemaObj) { + super(); + this.schemaObj = schemaObj; + } + + get baseFields() { + return [{ + id: 'collection', label: '', type: 'collection', + schema: this.schemaObj, + editable: false, + canAdd: true, canEdit: false, canDelete: true, hasRole: true, + columns : ['name', 'columns'], + }]; + } +} + +function getFieldDepChange(schema, id) { + return _.find(schema.fields, (f)=>f.id==id)?.depChange; +} + +describe('UniqueConstraintSchema', ()=>{ + let mount; + let schemaObj; + let getInitData = ()=>Promise.resolve({}); + + /* Use createMount so that material ui components gets the required context */ + /* https://material-ui.com/guides/testing/#api */ + beforeAll(()=>{ + mount = createMount(); + schemaObj = new UniqueConstraintSchema({ + spcname: ()=>Promise.resolve([]), + }, {}); + }); + + afterAll(() => { + mount.cleanUp(); + }); + + beforeEach(()=>{ + jasmineEnzyme(); + /* messages used by validators */ + pgAdmin.Browser = pgAdmin.Browser || {}; + pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages; + pgAdmin.Browser.utils = pgAdmin.Browser.utils || {}; + }); + + it('create', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('edit', ()=>{ + mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + }); + + it('properties', ()=>{ + mount({}} + onEdit={()=>{}} + />); + }); + + it('create collection', ()=>{ + let schemaCollObj = new SchemaInColl(schemaObj); + let ctrl = mount({}} + onClose={()=>{}} + onHelp={()=>{}} + onEdit={()=>{}} + onDataChange={()=>{}} + confirmOnCloseReset={false} + hasSQL={false} + disableSqlHelp={false} + disableDialogHelp={false} + />); + /* Make sure you hit every corner */ + ctrl.find('DataGridView').at(0).find('PgIconButton[data-test="add-row"]').find('button').simulate('click'); + }); + + it('depChange', ()=>{ + let state = {columns: [{column: 'id'}]}; + state.name = ''; + expect(getFieldDepChange(schemaObj, 'comment')(state)).toEqual({ + comment: '', + }); + + state.index = 'someindex'; + expect(getFieldDepChange(schemaObj, 'spcname')(state)).toEqual({ + spcname: '', + }); + expect(getFieldDepChange(schemaObj, 'include')(state)).toEqual({ + include: [], + }); + expect(getFieldDepChange(schemaObj, 'fillfactor')(state)).toEqual({ + fillfactor: null, + }); + expect(getFieldDepChange(schemaObj, 'condeferrable')(state)).toEqual({ + condeferrable: false, + }); + expect(getFieldDepChange(schemaObj, 'condeferred')(state)).toEqual({ + condeferred: false, + }); + + state.index = undefined; + expect(getFieldDepChange(schemaObj, 'spcname')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'include')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'fillfactor')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'condeferrable')(state)).toEqual({}); + expect(getFieldDepChange(schemaObj, 'condeferred')(state)).toEqual({}); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], null, { + type: SCHEMA_STATE_ACTIONS.DELETE_ROW, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns'], + value: 0, + })).toEqual({ + columns: [], + }); + + expect(getFieldDepChange(schemaObj, 'columns')(state, ['columns', 0], { + columns: [ + {name: 'id123'} + ], + }, { + type: SCHEMA_STATE_ACTIONS.SET_VALUE, + oldState: { + columns: [ + {name: 'id'} + ], + }, + path: ['columns', 0, 'name'], + value: 'id123', + })).toEqual({ + columns: [{column: 'id123'}], + }); + + + }); + + it('validate', ()=>{ + let state = {}; + let setError = jasmine.createSpy('setError'); + schemaObj.validate(state, setError); + expect(setError).toHaveBeenCalledWith('columns', 'Please specify columns for Unique constraint.'); + + state.columns = [{columns: 'id'}]; + expect(schemaObj.validate(state, setError)).toBe(false); + }); +}); +