Port Index node to react. Fixes #6661
parent
1b7a77f5cb
commit
e3992527fb
|
@ -7,6 +7,9 @@
|
|||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import IndexSchema, { getColumnSchema } from './index.ui';
|
||||
import { getNodeAjaxOptions, getNodeListByName } from 'pgbrowser/node_ajax';
|
||||
|
||||
define('pgadmin.node.index', [
|
||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
||||
'backbone', 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.alertifyjs',
|
||||
|
@ -32,191 +35,6 @@ define('pgadmin.node.index', [
|
|||
});
|
||||
}
|
||||
|
||||
// Node-Ajax-Cell with Deps
|
||||
var NodeAjaxOptionsDepsCell = Backgrid.Extension.NodeAjaxOptionsCell.extend({
|
||||
initialize: function() {
|
||||
Backgrid.Extension.NodeAjaxOptionsCell.prototype.initialize.apply(this, arguments);
|
||||
Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments);
|
||||
},
|
||||
dependentChanged: function () {
|
||||
var model = this.model,
|
||||
column = this.column,
|
||||
editable = this.column.get('editable'),
|
||||
input = this.$el.find('select').first();
|
||||
|
||||
var is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
|
||||
if (is_editable) {
|
||||
this.$el.addClass('editable');
|
||||
input.prop('disabled', false);
|
||||
} else {
|
||||
this.$el.removeClass('editable');
|
||||
input.prop('disabled', true);
|
||||
}
|
||||
|
||||
this.delegateEvents();
|
||||
return this;
|
||||
},
|
||||
remove: Backgrid.Extension.DependentCell.prototype.remove,
|
||||
});
|
||||
|
||||
|
||||
// Model to create column collection control
|
||||
var ColumnModel = pgAdmin.Browser.Node.Model.extend({
|
||||
defaults: {
|
||||
colname: undefined,
|
||||
collspcname: undefined,
|
||||
op_class: undefined,
|
||||
sort_order: false,
|
||||
nulls: false,
|
||||
is_sort_nulls_applicable: true,
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
id: 'colname', label: gettext('Column'), cell: 'node-list-by-name',
|
||||
type: 'text', disabled: 'inSchema', readonly: 'isEditMode', editable: function(m) {
|
||||
// Header cell then skip
|
||||
if (m instanceof Backbone.Collection) {
|
||||
return false;
|
||||
}
|
||||
return !(m.inSchemaWithModelCheck.apply(this, arguments));
|
||||
},
|
||||
control: 'node-list-by-name', node: 'column',
|
||||
},{
|
||||
id: 'collspcname', label: gettext('Collation'),
|
||||
cell: NodeAjaxOptionsDepsCell,
|
||||
type: 'text', disabled: 'inSchema', readonly: 'isEditMode', editable: function(m) {
|
||||
// Header cell then skip
|
||||
if (m instanceof Backbone.Collection) {
|
||||
return false;
|
||||
}
|
||||
return !(m.inSchemaWithModelCheck.apply(this, arguments));
|
||||
},
|
||||
control: 'node-ajax-options', url: 'get_collations', node: 'index',
|
||||
url_jump_after_node: 'schema',
|
||||
},{
|
||||
id: 'op_class', label: gettext('Operator class'),
|
||||
cell: NodeAjaxOptionsDepsCell, tags: true,
|
||||
type: 'text', disabled: 'checkAccessMethod',
|
||||
editable: function(m) {
|
||||
// Header cell then skip
|
||||
if (m instanceof Backbone.Collection || m.inSchemaWithModelCheck.apply(this, arguments)) {
|
||||
return false;
|
||||
}
|
||||
return !(m.checkAccessMethod.apply(this, arguments));
|
||||
},
|
||||
control: 'node-ajax-options', url: 'get_op_class', node: 'index',
|
||||
url_jump_after_node: 'schema',
|
||||
deps: ['amname'], transform: function(data, control) {
|
||||
/* We need to extract data from collection according
|
||||
* to access method selected by user if not selected
|
||||
* send btree related op_class options
|
||||
*/
|
||||
var amname = control.model.top.get('amname'),
|
||||
options = data['btree'];
|
||||
|
||||
if(_.isUndefined(amname))
|
||||
return options;
|
||||
|
||||
_.each(data, function(v, k) {
|
||||
if(amname === k) {
|
||||
options = v;
|
||||
}
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},{
|
||||
id: 'sort_order', label: gettext('Sort order'),
|
||||
cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch',
|
||||
editable: function(m) {
|
||||
// Header cell then skip
|
||||
if (m instanceof Backbone.Collection) {
|
||||
return false;
|
||||
} else if (m.inSchemaWithModelCheck.apply(this, arguments)) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
deps: ['amname'],
|
||||
options: {
|
||||
'onText': 'DESC', 'offText': 'ASC',
|
||||
},
|
||||
},{
|
||||
id: 'nulls', label: gettext('NULLs'),
|
||||
cell: Backgrid.Extension.TableChildSwitchCell, type: 'switch',
|
||||
editable: function(m) {
|
||||
// Header cell then skip
|
||||
if (m instanceof Backbone.Collection) {
|
||||
return false;
|
||||
} else if (m.inSchemaWithModelCheck.apply(this, arguments)) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
deps: ['amname', 'sort_order'],
|
||||
options: {
|
||||
'onText': 'FIRST', 'offText': 'LAST',
|
||||
},
|
||||
},
|
||||
],
|
||||
validate: function() {
|
||||
this.errorModel.clear();
|
||||
|
||||
if (_.isUndefined(this.get('colname'))
|
||||
|| String(this.get('colname')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
var msg = gettext('Column Name cannot be empty.');
|
||||
this.errorModel.set('colname', msg);
|
||||
return msg;
|
||||
}
|
||||
},
|
||||
// We will check if we are under schema node
|
||||
inSchema: function() {
|
||||
if(this.node_info && 'catalog' in this.node_info) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isEditMode: function(m) {
|
||||
return !m.top.isNew();
|
||||
},
|
||||
// We will check if we are under schema node & in 'create' mode
|
||||
inSchemaWithModelCheck: function(m) {
|
||||
if(m.top.node_info && 'schema' in m.top.node_info) {
|
||||
// We will disable control if it's in 'edit' mode
|
||||
return !m.top.isNew();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// We will check if we are under schema node and added condition
|
||||
checkAccessMethod: function(m) {
|
||||
//Access method is empty or btree then do not disable field
|
||||
var parent_model = m.top;
|
||||
if(_.isUndefined(parent_model.get('amname')) ||
|
||||
_.isNull(parent_model.get('amname')) ||
|
||||
String(parent_model.get('amname')).replace(/^\s+|\s+$/g, '') == '' ||
|
||||
parent_model.get('amname') === 'btree') {
|
||||
// We need to set nulls to true if sort_order is set to desc
|
||||
// nulls first is default for desc
|
||||
if(m.get('sort_order') == true && m.previous('sort_order') == false) {
|
||||
setTimeout(function() { m.set('nulls', true); }, 10);
|
||||
}
|
||||
}
|
||||
else {
|
||||
m.set('is_sort_nulls_applicable', false);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
if (!pgBrowser.Nodes['index']) {
|
||||
pgAdmin.Browser.Nodes['index'] = pgBrowser.Node.extend({
|
||||
parent_type: ['table', 'view', 'mview', 'partition'],
|
||||
|
@ -291,253 +109,11 @@ define('pgadmin.node.index', [
|
|||
},{
|
||||
id: 'oid', label: gettext('OID'), cell: 'string',
|
||||
type: 'int', readonly: true, mode: ['properties'],
|
||||
},{
|
||||
id: 'spcname', label: gettext('Tablespace'), cell: 'string',
|
||||
control: 'node-list-by-name', node: 'tablespace',
|
||||
select2: {'allowClear': true},
|
||||
type: 'text', mode: ['properties', 'create', 'edit'],
|
||||
disabled: 'inSchema', filter: function(d) {
|
||||
// If tablespace name is not "pg_global" then we need to exclude them
|
||||
if(d && d.label.match(/pg_global/))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},{
|
||||
id: 'amname', label: gettext('Access Method'), cell: 'string',
|
||||
type: 'text', mode: ['properties', 'create', 'edit'],
|
||||
disabled: 'inSchema', readonly: 'isEditMode', url: 'get_access_methods',
|
||||
url_jump_after_node: 'schema',
|
||||
group: gettext('Definition'), select2: {'allowClear': true},
|
||||
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');
|
||||
if (current_am != previous_am && self.model.get('columns').length !== 0) {
|
||||
var msg = gettext('Changing access method will clear columns collection');
|
||||
Alertify.confirm(msg, function () {
|
||||
// User clicks Ok, lets clear collection
|
||||
var column_collection = self.model.get('columns'),
|
||||
col_length = column_collection.length;
|
||||
for (var i=(col_length-1);i>=0;i--) {
|
||||
column_collection.remove(column_collection.models[i]);
|
||||
}
|
||||
}, function() {
|
||||
// User clicks Cancel set previous value again in combo box
|
||||
setTimeout(function(){
|
||||
self.model.set('amname', previous_am);
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
},{
|
||||
id: 'columns_csv', label: gettext('Columns'), cell: 'string',
|
||||
type: 'text', disabled: 'inSchema', mode: ['properties'],
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'include', label: gettext('Include columns'),
|
||||
type: 'array', group: gettext('Definition'),
|
||||
editable: false,
|
||||
canDelete: true, canAdd: true, mode: ['properties'],
|
||||
disabled: 'inSchema', readonly: 'isEditMode',
|
||||
visible: function(m) {
|
||||
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)'),
|
||||
},
|
||||
}
|
||||
),
|
||||
}),
|
||||
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;
|
||||
},
|
||||
node:'column',
|
||||
},{
|
||||
id: 'fillfactor', label: gettext('Fill factor'), cell: 'string',
|
||||
type: 'int', disabled: 'inSchema', mode: ['create', 'edit', 'properties'],
|
||||
min: 10, max:100, group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisunique', label: gettext('Unique?'), cell: 'string',
|
||||
type: 'switch', disabled: 'inSchema', readonly: 'isEditMode',
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisclustered', label: gettext('Clustered?'), cell: 'string',
|
||||
type: 'switch', disabled: 'inSchema',
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisvalid', label: gettext('Valid?'), cell: 'string',
|
||||
type: 'switch', mode: ['properties'],
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisprimary', label: gettext('Primary?'), cell: 'string',
|
||||
type: 'switch', mode: ['properties'],
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'is_sys_idx', label: gettext('System index?'), cell: 'string',
|
||||
type: 'switch', mode: ['properties'],
|
||||
},{
|
||||
id: 'isconcurrent', label: gettext('Concurrent build?'), cell: 'string',
|
||||
type: 'switch', disabled: 'inSchema', readonly: 'isEditMode',
|
||||
mode: ['create', 'edit'], group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indconstraint', label: gettext('Constraint'), cell: 'string',
|
||||
type: 'text', disabled: 'inSchema', readonly: 'isEditMode', mode: ['create', 'edit'],
|
||||
control: 'sql-field', visible: true, group: gettext('Definition'),
|
||||
},{
|
||||
id: 'columns', label: gettext('Columns'), type: 'collection', deps: ['amname'],
|
||||
group: gettext('Definition'), model: ColumnModel, mode: ['edit', 'create'],
|
||||
canAdd: function(m) {
|
||||
// We will disable it if it's in 'edit' mode
|
||||
return m.isNew();
|
||||
},
|
||||
canEdit: false,
|
||||
canDelete: function(m) {
|
||||
// We will disable it if it's in 'edit' mode
|
||||
return m.isNew();
|
||||
},
|
||||
control: 'unique-col-collection', uniqueCol : ['colname'],
|
||||
columns: ['colname', 'op_class', 'sort_order', 'nulls', 'collspcname'],
|
||||
},{
|
||||
id: 'include', label: gettext('Include columns'),
|
||||
type: 'array', group: gettext('Definition'),
|
||||
editable: false,
|
||||
canDelete: true, canAdd: true, mode: ['edit', 'create'],
|
||||
disabled: 'inSchema', readonly: 'isEditMode',
|
||||
visible: function(m) {
|
||||
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)'),
|
||||
},
|
||||
}
|
||||
),
|
||||
}),
|
||||
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;
|
||||
},
|
||||
node:'column',
|
||||
},{
|
||||
}, {
|
||||
id: 'description', label: gettext('Comment'), cell: 'string',
|
||||
type: 'multiline', mode: ['properties', 'create', 'edit'],
|
||||
disabled: 'inSchema',
|
||||
},
|
||||
],
|
||||
validate: function(keys) {
|
||||
var msg;
|
||||
|
||||
// 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('Name cannot be empty.');
|
||||
this.errorModel.set('name', msg);
|
||||
return msg;
|
||||
}
|
||||
if (_.isUndefined(this.get('amname'))
|
||||
|| String(this.get('amname')).replace(/^\s+|\s+$/g, '') == '') {
|
||||
msg = gettext('Access method cannot be empty.');
|
||||
this.errorModel.set('amname', msg);
|
||||
return msg;
|
||||
}
|
||||
// Checks if all columns has names
|
||||
var cols = this.get('columns');
|
||||
if(cols && cols.length > 0) {
|
||||
if(!_.every(cols.pluck('colname'))) {
|
||||
msg = gettext('You must specify column name.');
|
||||
this.errorModel.set('columns', msg);
|
||||
return msg;
|
||||
}
|
||||
} else if(cols){
|
||||
msg = gettext('You must specify at least one column.');
|
||||
this.errorModel.set('columns', msg);
|
||||
return msg;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// We will check if we are under schema node & in 'create' mode
|
||||
inSchema: function() {
|
||||
if(this.node_info && 'catalog' in this.node_info) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isEditMode: function(m) {
|
||||
return !m.isNew();
|
||||
},
|
||||
// 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) {
|
||||
if(this.node_info && 'schema' in this.node_info) {
|
||||
// We will disable control if it's system columns
|
||||
// ie: it's position is less then 1
|
||||
if (m.isNew()) {
|
||||
return false;
|
||||
} else {
|
||||
// if we are in edit mode
|
||||
return (_.isUndefined(m.get('attnum')) || m.get('attnum') < 1 );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}],
|
||||
}),
|
||||
// Below function will enable right click menu for creating column
|
||||
canCreate: function(itemData, item, data) {
|
||||
|
@ -574,6 +150,25 @@ define('pgadmin.node.index', [
|
|||
return !is_immediate_parent_table_partitioned;
|
||||
}
|
||||
},
|
||||
getSchema: (treeNodeInfo, itemNodeData) => {
|
||||
let nodeObj = pgAdmin.Browser.Nodes['index'];
|
||||
return new IndexSchema(
|
||||
()=>getColumnSchema(nodeObj, treeNodeInfo, itemNodeData),
|
||||
{
|
||||
tablespaceList: ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{
|
||||
return (m.label != 'pg_global');
|
||||
}),
|
||||
amnameList : ()=>getNodeAjaxOptions('get_access_methods', nodeObj, treeNodeInfo, itemNodeData),
|
||||
columnList: ()=>getNodeListByName('column', treeNodeInfo, itemNodeData, {}),
|
||||
},
|
||||
{
|
||||
node_info: treeNodeInfo
|
||||
},
|
||||
{
|
||||
amname: 'btree'
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,479 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../static/js/node_ajax';
|
||||
import _ from 'lodash';
|
||||
import { pgAlertify } from 'sources/helpers/legacyConnector';
|
||||
import { isEmptyString } from 'sources/validators';
|
||||
|
||||
export function getColumnSchema(nodeObj, treeNodeInfo, itemNodeData) {
|
||||
return new ColumnSchema(
|
||||
{
|
||||
columnList: ()=>getNodeListByName('column', treeNodeInfo, itemNodeData, {}),
|
||||
collationList: ()=>getNodeAjaxOptions('get_collations', nodeObj, treeNodeInfo, itemNodeData),
|
||||
opClassList: ()=>getNodeAjaxOptions('get_op_class', nodeObj, treeNodeInfo, itemNodeData)
|
||||
}, {
|
||||
node_info: treeNodeInfo
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export class ColumnSchema extends BaseUISchema {
|
||||
constructor(fieldOptions = {}, nodeData, initValues) {
|
||||
super({
|
||||
name: null,
|
||||
oid: undefined,
|
||||
description: '',
|
||||
is_sys_obj: false,
|
||||
colname: undefined,
|
||||
collspcname: undefined,
|
||||
op_class: undefined,
|
||||
sort_order: false,
|
||||
nulls: false,
|
||||
is_sort_nulls_applicable: true,
|
||||
...initValues
|
||||
});
|
||||
this.fieldOptions = {
|
||||
columnList: [],
|
||||
collationList: [],
|
||||
opClassList: [],
|
||||
...fieldOptions
|
||||
};
|
||||
this.node_info = {
|
||||
...nodeData.node_info
|
||||
};
|
||||
this.op_class_types = [];
|
||||
}
|
||||
|
||||
get idAttribute() {
|
||||
return 'oid';
|
||||
}
|
||||
|
||||
// We will check if we are under schema node & in 'create' mode
|
||||
inSchemaWithModelCheck(state) {
|
||||
if(this.node_info && 'schema' in this.node_info) {
|
||||
// We will disable control if it's in 'edit' mode
|
||||
return !this.isNew(state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setOpClassTypes(options) {
|
||||
|
||||
if(!options || (_.isArray(options) && options.length == 0))
|
||||
return this.op_class_types;
|
||||
|
||||
if(this.op_class_types.length == 0)
|
||||
this.op_class_types = options;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let columnSchemaObj = this;
|
||||
return [
|
||||
{
|
||||
id: 'colname', label: gettext('Column'),
|
||||
type: 'select', cell: 'select', noEmpty: true,
|
||||
disabled: () => inSchema(columnSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return columnSchemaObj.inSchemaWithModelCheck(state);
|
||||
},
|
||||
options: columnSchemaObj.fieldOptions.columnList,
|
||||
node: 'column',
|
||||
},{
|
||||
id: 'collspcname', label: gettext('Collation'),
|
||||
type: 'select',
|
||||
cell: 'select',
|
||||
disabled: () => inSchema(columnSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return columnSchemaObj.inSchemaWithModelCheck(state);
|
||||
},
|
||||
options: columnSchemaObj.fieldOptions.collationList,
|
||||
node: 'index',
|
||||
url_jump_after_node: 'schema',
|
||||
},{
|
||||
id: 'op_class', label: gettext('Operator class'),
|
||||
tags: true, type: 'select',
|
||||
cell: () => {
|
||||
return {
|
||||
cell: 'select',
|
||||
options: columnSchemaObj.fieldOptions.opClassList,
|
||||
optionsLoaded: (options)=>{columnSchemaObj.setOpClassTypes(options);},
|
||||
controlProps: {
|
||||
allowClear: true,
|
||||
filter: (options) => {
|
||||
/* We need to extract data from collection according
|
||||
* to access method selected by user if not selected
|
||||
* send btree related op_class options
|
||||
*/
|
||||
var amname = columnSchemaObj._top._sessData ? columnSchemaObj._top._sessData.amname : columnSchemaObj._top._origData.amname;
|
||||
|
||||
if(_.isUndefined(amname))
|
||||
return options;
|
||||
|
||||
_.each(this.op_class_types, function(v, k) {
|
||||
if(amname === k) {
|
||||
options = v;
|
||||
return;
|
||||
}
|
||||
});
|
||||
return options;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
readonly: function (state) {
|
||||
return columnSchemaObj.inSchemaWithModelCheck(state);
|
||||
},
|
||||
node: 'index',
|
||||
url_jump_after_node: 'schema',
|
||||
deps: ['amname'],
|
||||
},{
|
||||
id: 'sort_order', label: gettext('Sort order'),
|
||||
type: 'switch',
|
||||
cell: 'switch',
|
||||
depChange: (state, source, topState, actionObj) => {
|
||||
//Access method is empty or btree then do not disable field
|
||||
if(isEmptyString(topState.amname) || topState.amname === 'btree') {
|
||||
// We need to set nulls to true if sort_order is set to desc
|
||||
// nulls first is default for desc
|
||||
if(state.sort_order == true && actionObj.oldState.sort_order == false) {
|
||||
setTimeout(function() {
|
||||
state.nulls = true;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
else {
|
||||
state.is_sort_nulls_applicable = false;
|
||||
}
|
||||
},
|
||||
editable: function(state) {
|
||||
let topObj = columnSchemaObj._top;
|
||||
if(columnSchemaObj.inSchemaWithModelCheck(state)) {
|
||||
return false;
|
||||
} else if (topObj._sessData && topObj._sessData.amname === 'btree') {
|
||||
state.is_sort_nulls_applicable = true;
|
||||
return true;
|
||||
} else {
|
||||
state.is_sort_nulls_applicable = false;
|
||||
return false;
|
||||
}
|
||||
},
|
||||
deps: ['amname'],
|
||||
options: {
|
||||
'onText': 'DESC', 'offText': 'ASC',
|
||||
},
|
||||
},{
|
||||
id: 'nulls', label: gettext('NULLs'),
|
||||
type: 'switch',
|
||||
cell: 'switch',
|
||||
editable: function(state) {
|
||||
let topObj = columnSchemaObj._top;
|
||||
if(columnSchemaObj.inSchemaWithModelCheck(state)) {
|
||||
return false;
|
||||
} else if (topObj._sessData && topObj._sessData.amname === 'btree') {
|
||||
state.is_sort_nulls_applicable = true;
|
||||
return true;
|
||||
} else {
|
||||
state.is_sort_nulls_applicable = false;
|
||||
return false;
|
||||
}
|
||||
},
|
||||
deps: ['amname', 'sort_order'],
|
||||
options: {
|
||||
'onText': 'FIRST', 'offText': 'LAST',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function inSchema(node_info) {
|
||||
if(node_info && 'catalog' in node_info)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class IndexSchema extends BaseUISchema {
|
||||
constructor(getColumnSchema, fieldOptions = {}, nodeData, initValues) {
|
||||
super({
|
||||
name: undefined,
|
||||
oid: undefined,
|
||||
description: '',
|
||||
is_sys_obj: false,
|
||||
nspname: undefined,
|
||||
tabname: undefined,
|
||||
spcname: undefined,
|
||||
amname: undefined,
|
||||
columns: [],
|
||||
...initValues
|
||||
});
|
||||
this.fieldOptions = {
|
||||
tablespaceList: [],
|
||||
amnameList: [],
|
||||
columnList: [],
|
||||
...fieldOptions
|
||||
};
|
||||
this.node_info = {
|
||||
...nodeData.node_info
|
||||
};
|
||||
this.getColumnSchema = getColumnSchema;
|
||||
}
|
||||
|
||||
get idAttribute() {
|
||||
return 'oid';
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
let indexSchemaObj = this;
|
||||
return [
|
||||
{
|
||||
id: 'name', label: gettext('Name'), cell: 'string',
|
||||
type: 'text', noEmpty: true,
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
},{
|
||||
id: 'oid', label: gettext('OID'), cell: 'string',
|
||||
type: 'int', readonly: true, mode: ['properties'],
|
||||
},{
|
||||
id: 'spcname', label: gettext('Tablespace'), cell: 'string',
|
||||
node: 'tablespace',
|
||||
mode: ['properties', 'create', 'edit'],
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
type: 'select',
|
||||
options: indexSchemaObj.fieldOptions.tablespaceList,
|
||||
controlProps: { allowClear: true },
|
||||
},{
|
||||
id: 'amname', label: gettext('Access Method'), cell: 'string',
|
||||
mode: ['properties', 'create', 'edit'], noEmpty: true,
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return !indexSchemaObj.isNew(state);
|
||||
},
|
||||
url_jump_after_node: 'schema',
|
||||
group: gettext('Definition'),
|
||||
type: () => {
|
||||
return {
|
||||
type: 'select',
|
||||
options: indexSchemaObj.fieldOptions.amnameList,
|
||||
optionsLoaded: (options) => { indexSchemaObj.fieldOptions.amnameList = options; },
|
||||
controlProps: {
|
||||
allowClear: true,
|
||||
filter: (options) => {
|
||||
let res = [];
|
||||
if (options && _.isArray(options)) {
|
||||
_.each(options, function(d) {
|
||||
if(d.label != '')
|
||||
res.push({label: d.label, value: d.value, data:d});
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
deferredDepChange: (state, source, topState, actionObj) => {
|
||||
|
||||
const setColumns = (resolve)=>{
|
||||
resolve(()=>{
|
||||
state.columns.splice(0, state.columns.length);
|
||||
return {
|
||||
columns: state.columns,
|
||||
};
|
||||
});
|
||||
};
|
||||
if(state.amname != actionObj.oldState.amname) {
|
||||
return new Promise((resolve)=>{
|
||||
pgAlertify().confirm(
|
||||
gettext('Changing access method will clear columns collection'),
|
||||
function () {
|
||||
setColumns(resolve);
|
||||
},
|
||||
function() {
|
||||
resolve(()=>{
|
||||
state.amname = actionObj.oldState.amname;
|
||||
return {
|
||||
amname: state.amname,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(()=>{});
|
||||
}
|
||||
},
|
||||
},{
|
||||
id: 'columns_csv', label: gettext('Columns'), cell: 'string',
|
||||
type: 'text',
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
mode: ['properties'],
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'include', label: gettext('Include columns'),
|
||||
group: gettext('Definition'),
|
||||
editable: false, canDelete: true, canAdd: true, mode: ['properties'],
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return !indexSchemaObj.isNew(state);
|
||||
},
|
||||
type: () => {
|
||||
return {
|
||||
type: 'select',
|
||||
options: indexSchemaObj.fieldOptions.columnList,
|
||||
optionsLoaded: (options) => { indexSchemaObj.fieldOptions.columnList = options; },
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
multiple: true,
|
||||
placeholder: gettext('Select the column(s)'),
|
||||
width: 'style',
|
||||
filter: (options) => {
|
||||
let res = [];
|
||||
if (options && _.isArray(options)) {
|
||||
_.each(options, function(d) {
|
||||
if(d.label != '')
|
||||
res.push({label: d.label, value: d.value, image:'icon-column'});
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
visible: function() {
|
||||
if(!_.isUndefined(this.node_info) && !_.isUndefined(this.node_info.server)
|
||||
&& !_.isUndefined(this.node_info.server.version) &&
|
||||
this.node_info.server.version >= 110000)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
node:'column',
|
||||
},{
|
||||
id: 'fillfactor', label: gettext('Fill factor'), cell: 'string',
|
||||
type: 'int', disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
mode: ['create', 'edit', 'properties'],
|
||||
min: 10, max:100, group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisunique', label: gettext('Unique?'), cell: 'string',
|
||||
type: 'switch', disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return !indexSchemaObj.isNew(state);
|
||||
},
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisclustered', label: gettext('Clustered?'), cell: 'string',
|
||||
type: 'switch', disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisvalid', label: gettext('Valid?'), cell: 'string',
|
||||
type: 'switch', mode: ['properties'],
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indisprimary', label: gettext('Primary?'), cell: 'string',
|
||||
type: 'switch', mode: ['properties'],
|
||||
group: gettext('Definition'),
|
||||
},{
|
||||
id: 'is_sys_idx', label: gettext('System index?'), cell: 'string',
|
||||
type: 'switch', mode: ['properties'],
|
||||
},{
|
||||
id: 'isconcurrent', label: gettext('Concurrent build?'), cell: 'string',
|
||||
type: 'switch', disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return !indexSchemaObj.isNew(state);
|
||||
},
|
||||
mode: ['create', 'edit'], group: gettext('Definition'),
|
||||
},{
|
||||
id: 'indconstraint', label: gettext('Constraint'), cell: 'string',
|
||||
type: 'sql', controlProps: {className:['custom_height_css_class']},
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return !indexSchemaObj.isNew(state);
|
||||
},
|
||||
mode: ['create', 'edit'],
|
||||
control: 'sql-field', visible: true, group: gettext('Definition'),
|
||||
}, {
|
||||
id: 'columns', label: gettext('Columns'), type: 'collection', deps: ['amname'],
|
||||
group: gettext('Definition'), schema: indexSchemaObj.getColumnSchema(),
|
||||
mode: ['edit', 'create'],
|
||||
canAdd: function(state) {
|
||||
// We will disable it if it's in 'edit' mode
|
||||
return indexSchemaObj.isNew(state);
|
||||
},
|
||||
canEdit: false,
|
||||
canDelete: function(state) {
|
||||
// We will disable it if it's in 'edit' mode
|
||||
return indexSchemaObj.isNew(state);
|
||||
},
|
||||
uniqueCol : ['colname'],
|
||||
columns: ['colname', 'op_class', 'sort_order', 'nulls', 'collspcname']
|
||||
}, {
|
||||
id: 'include', label: gettext('Include columns'),
|
||||
type: () => {
|
||||
return {
|
||||
type: 'select',
|
||||
options: indexSchemaObj.fieldOptions.columnList,
|
||||
optionsLoaded: (options) => { indexSchemaObj.fieldOptions.columnList = options; },
|
||||
controlProps: {
|
||||
allowClear: false,
|
||||
multiple: true,
|
||||
placeholder: gettext('Select the column(s)'),
|
||||
width: 'style',
|
||||
filter: (options) => {
|
||||
let res = [];
|
||||
if (options && _.isArray(options)) {
|
||||
_.each(options, function(d) {
|
||||
if(d.label != '')
|
||||
res.push({label: d.label, value: d.value, image:'icon-column'});
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
group: gettext('Definition'),
|
||||
editable: false,
|
||||
canDelete: true, canAdd: true, mode: ['edit', 'create'],
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
readonly: function (state) {
|
||||
return !indexSchemaObj.isNew(state);
|
||||
},
|
||||
visible: function() {
|
||||
if(!_.isUndefined(this.node_info) && !_.isUndefined(this.node_info.server)
|
||||
&& !_.isUndefined(this.node_info.server.version) &&
|
||||
this.node_info.server.version >= 110000)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
node:'column',
|
||||
},{
|
||||
id: 'description', label: gettext('Comment'), cell: 'string',
|
||||
type: 'multiline', mode: ['properties', 'create', 'edit'],
|
||||
disabled: () => inSchema(indexSchemaObj.node_info),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
validate(state, setError) {
|
||||
var msg;
|
||||
|
||||
// Checks if columns is empty
|
||||
var cols = state.columns;
|
||||
if(_.isArray(cols) && cols.length == 0){
|
||||
msg = gettext('You must specify at least one column.');
|
||||
setError('columns', msg);
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// 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 * as nodeAjax from '../../../pgadmin/browser/static/js/node_ajax';
|
||||
import IndexSchema, { getColumnSchema } from '../../../pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/js/index.ui';
|
||||
|
||||
|
||||
describe('IndexSchema', ()=>{
|
||||
let mount;
|
||||
|
||||
describe('column schema describe', () => {
|
||||
|
||||
let columnSchemaObj = getColumnSchema({}, {server: {user: {name: 'postgres'}}}, {});
|
||||
|
||||
it('column schema collection', ()=>{
|
||||
|
||||
spyOn(nodeAjax, 'getNodeAjaxOptions').and.returnValue([]);
|
||||
spyOn(nodeAjax, 'getNodeListByName').and.returnValue([]);
|
||||
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={columnSchemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
disableDialogHelp={false}
|
||||
/>);
|
||||
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={columnSchemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'edit',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
getInitData={getInitData}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
disableDialogHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('column schema colname editable', ()=>{
|
||||
columnSchemaObj._top = {
|
||||
_sessData: { amname: 'btree' }
|
||||
};
|
||||
let cell = _.find(columnSchemaObj.fields, (f)=>f.id=='op_class').cell;
|
||||
cell();
|
||||
});
|
||||
|
||||
it('column schema sort_order depChange', ()=>{
|
||||
let topState = { amname: 'btree' };
|
||||
let depChange = _.find(columnSchemaObj.fields, (f)=>f.id=='sort_order').depChange;
|
||||
|
||||
let state = { sort_order: true };
|
||||
depChange(state, {}, topState, { oldState: { sort_order: false } });
|
||||
|
||||
state.sort_order = false;
|
||||
topState.amname = 'abc';
|
||||
depChange(state, {}, topState, { oldState: { sort_order: false } });
|
||||
expect(state.is_sort_nulls_applicable).toBe(false);
|
||||
});
|
||||
|
||||
it('column schema sort_order editable', ()=>{
|
||||
columnSchemaObj._top = {
|
||||
_sessData: { amname: 'btree' }
|
||||
};
|
||||
let state = {};
|
||||
spyOn(columnSchemaObj, 'inSchemaWithModelCheck').and.returnValue(true);
|
||||
let editable = _.find(columnSchemaObj.fields, (f)=>f.id=='sort_order').editable;
|
||||
let status = editable(state);
|
||||
expect(status).toBe(false);
|
||||
|
||||
spyOn(columnSchemaObj, 'inSchemaWithModelCheck').and.returnValue(false);
|
||||
status = editable(state);
|
||||
expect(status).toBe(true);
|
||||
|
||||
columnSchemaObj._top._sessData.amname = 'abc';
|
||||
status = editable(state);
|
||||
expect(status).toBe(false);
|
||||
});
|
||||
|
||||
it('column schema nulls editable', ()=>{
|
||||
columnSchemaObj._top = {
|
||||
_sessData: { amname: 'btree' }
|
||||
};
|
||||
let state = {};
|
||||
spyOn(columnSchemaObj, 'inSchemaWithModelCheck').and.returnValue(true);
|
||||
let editable = _.find(columnSchemaObj.fields, (f)=>f.id=='nulls').editable;
|
||||
let status = editable(state);
|
||||
expect(status).toBe(false);
|
||||
|
||||
spyOn(columnSchemaObj, 'inSchemaWithModelCheck').and.returnValue(false);
|
||||
status = editable(state);
|
||||
expect(status).toBe(true);
|
||||
|
||||
columnSchemaObj._top._sessData.amname = 'abc';
|
||||
status = editable(state);
|
||||
expect(status).toBe(false);
|
||||
});
|
||||
|
||||
it('column schema setOpClassTypes', ()=>{
|
||||
columnSchemaObj._top = {
|
||||
_sessData: { amname: 'btree' }
|
||||
};
|
||||
let options = [];
|
||||
columnSchemaObj.op_class_types = [];
|
||||
let status = columnSchemaObj.setOpClassTypes(options);
|
||||
expect(status).toEqual([]);
|
||||
|
||||
columnSchemaObj.op_class_types = [];
|
||||
options.push({label: '', value: ''});
|
||||
status = columnSchemaObj.setOpClassTypes(options);
|
||||
expect(columnSchemaObj.op_class_types.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
let indexSchemaObj = new IndexSchema(
|
||||
()=>getColumnSchema({}, {server: {user: {name: 'postgres'}}}, {}),
|
||||
{
|
||||
tablespaceList: ()=>[],
|
||||
amnameList : ()=>[{label:'abc', value:'abc'}],
|
||||
columnList: ()=>[{label:'abc', value:'abc'}],
|
||||
},
|
||||
{
|
||||
node_info: {'server': { 'version': 110000} }
|
||||
},
|
||||
{
|
||||
amname: 'btree'
|
||||
}
|
||||
);
|
||||
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(<SchemaView
|
||||
formType='dialog'
|
||||
schema={indexSchemaObj}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
disableDialogHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('edit', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='dialog'
|
||||
schema={indexSchemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'create',
|
||||
}}
|
||||
onSave={()=>{}}
|
||||
onClose={()=>{}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
onDataChange={()=>{}}
|
||||
confirmOnCloseReset={false}
|
||||
hasSQL={false}
|
||||
disableSqlHelp={false}
|
||||
disableDialogHelp={false}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('properties', ()=>{
|
||||
mount(<SchemaView
|
||||
formType='tab'
|
||||
schema={indexSchemaObj}
|
||||
getInitData={getInitData}
|
||||
viewHelperProps={{
|
||||
mode: 'properties',
|
||||
}}
|
||||
onHelp={()=>{}}
|
||||
onEdit={()=>{}}
|
||||
/>);
|
||||
});
|
||||
|
||||
it('validate', ()=>{
|
||||
let state = { columns: [] };
|
||||
let setError = jasmine.createSpy('setError');
|
||||
|
||||
indexSchemaObj.validate(state, setError);
|
||||
expect(setError).toHaveBeenCalledWith('columns', 'You must specify at least one column.');
|
||||
|
||||
state.columns.push({});
|
||||
let status = indexSchemaObj.validate(state, setError);
|
||||
expect(status).toBe(null);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue