Port Cast node to react. Fixes #6566.

This commit is contained in:
Pradip Parkale 2021-07-14 14:49:30 +05:30 committed by Akshay Joshi
parent d846ae64ea
commit c94fc73c11
4 changed files with 347 additions and 225 deletions

View File

@ -6,12 +6,16 @@
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { getNodeAjaxOptions } from '../../../../../../static/js/node_ajax';
import CastSchema from './cast.ui';
import getApiInstance from '../../../../../../../static/js/api_instance';
define('pgadmin.node.cast', [
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
'sources/pgadmin', 'pgadmin.browser',
'pgadmin.alertifyjs', 'pgadmin.backform', 'pgadmin.browser.collection',
], function(gettext, url_for, $, _, pgAdmin, pgBrowser, alertify, Backform) {
], function(gettext, url_for, $, _, pgAdmin, pgBrowser) {
// Extend the collection class for cast
if (!pgBrowser.Nodes['coll-cast']) {
pgAdmin.Browser.Nodes['coll-cast'] =
@ -68,16 +72,6 @@ define('pgadmin.node.cast', [
// Define the backform model for cast node
model: pgAdmin.Browser.Node.Model.extend({
idAttribute: 'oid',
defaults: {
name: undefined, // Name of the cast
encoding: 'UTF8',
srctyp: undefined, // Source type
trgtyp: undefined, // Target type
proname: undefined, // Function
castcontext: undefined, // Context (IMPLICIT/EXPLICIT/ASSIGNMENT)
syscast: undefined, // Is this cast is system object? Yes/No
description: undefined, // Comment on the cast
},
// Define the schema for cast
schema: [{
@ -86,224 +80,47 @@ define('pgadmin.node.cast', [
},{
id: 'oid', label: gettext('OID'), cell: 'string',
editable: false, type: 'text', mode: ['properties'],
},{
id: 'srctyp', label: gettext('Source type'), url: 'get_type',
type: 'text', group: gettext('Definition'), readonly: function(m) {
return !m.isNew();
}, mode: ['create'],
transform: function(rows) {
_.each(rows, function(r) {
r['image'] = 'icon-cast';
});
return rows;
},
/*
* Control is extended to create cast name from source type and destination type
* once their values are changed
*/
control: Backform.NodeAjaxOptionsControl.extend({
onChange: function() {
Backform.NodeAjaxOptionsControl.prototype.onChange.apply(
this, arguments
);
/*
* On source type change, check if both source type and
* target type are set, if yes then fetch values from both
* controls and generate cast name
*/
var srctype = this.model.get('srctyp');
var trgtype = this.model.get('trgtyp');
if(srctype != undefined && srctype != '' &&
trgtype != undefined && trgtype != '')
this.model.set('name', srctype+'->'+trgtype);
else
this.model.unset('name');
},
}),
},
/*
* Text control for viewing source type in properties and
* edit mode only
*/
{
id: 'srctyp', label: gettext('Source type'), type: 'text',
group: gettext('Definition'), readonly: true, mode:['properties','edit'],
},{
id: 'trgtyp', label: gettext('Target type'), url: 'get_type',
type: 'text', group: gettext('Definition'), readonly: function(m) {
return !m.isNew();
}, mode: ['create'],
transform: function(rows) {
_.each(rows, function(r) {
r['image'] = 'icon-cast';
});
return rows;
},
/*
* Control is extended to create cast name from source type and destination type
* once their values are changed
*/
control: Backform.NodeAjaxOptionsControl.extend({
onChange: function() {
Backform.NodeAjaxOptionsControl.prototype.onChange.apply(
this, arguments
);
/*
* on target type change, check if both source type and
* target type are set, if yes then fetch values from both
* controls and generate cast name
*/
var srcType = this.model.get('srctyp');
var trgtype = this.model.get('trgtyp');
if(srcType != undefined && srcType != '' &&
trgtype != undefined && trgtype != '')
this.model.set('name', srcType+'->'+trgtype);
else
this.model.unset('name');
},
}),
},
/*
* Text control for viewing target type in properties and
* edit mode only
*/
{
id: 'trgtyp', label: gettext('Target type'), type: 'text',
group: gettext('Definition'), readonly: true, mode:['properties','edit'],
},
/*
* Proname field is dependent on source type and target type.
* On source and target type changed event,
* associated functions will be fetch using ajax call
*/
{
id: 'proname', label: gettext('Function'), deps:['srctyp', 'trgtyp'],
type: 'text', readonly: function(m) { return !m.isNew(); },
group: gettext('Definition'), mode: ['create'],
control: 'node-ajax-options',
options: function(control) {
var srcTyp = control.model.get('srctyp');
var trgtyp = control.model.get('trgtyp');
var res = [];
if(srcTyp != undefined && srcTyp != '' &&
trgtyp != undefined && trgtyp != '')
{
var node = control.field.get('schema_node'),
_url = node.generate_url.apply(
node, [
null, 'get_functions', control.field.get('node_data'), false,
control.field.get('node_info'),
]);
$.ajax({
type: 'POST',
timeout: 30000,
url: _url,
cache: false,
async: false,
data: {'srctyp' : srcTyp, 'trgtyp' : trgtyp},
})
// On success return function list from server
.done(function(result) {
res = result.data;
return res;
})
// On failure show error appropriate error message to user
.fail(function(xhr, status, error) {
alertify.pgRespErrorNotify(xhr, error);
});
}
return res;
},
},
/*
* Text type control for viewing function name in properties and
* edit mode only
*/
{
id: 'proname', label: gettext('Function'), type: 'text',
group: gettext('Definition'), readonly: true, mode:['properties','edit'],
},{
id: 'castcontext', label: gettext('Context'),
options:{'onText':'IMPLICIT','offText':'EXPLICIT', width: '90'},
editable: false, type: 'string', group: gettext('Definition'),
mode:['create'],
control: Backform.SwitchControl.extend({
getValueFromDOM: function() {
return this.$input.prop('checked') ? 'IMPLICIT' : 'EXPLICIT';
},
}),
},
/*
* Text control for viewing context in properties and
* edit mode
*/
{
id: 'castcontext', label: gettext('Context'), readonly: true,
options:[{
label: 'IMPLICIT', value: 'IMPLICIT',
},{
label: 'EXPLICIT', value: 'EXPLICIT',
},{
label: 'ASSIGNMENT', value: 'ASSIGNMENT',
}], editable: false, type: 'select2', group: gettext('Definition'),
mode:['properties', 'edit'],
},{
id: 'syscast', label: gettext('System cast?'),
cell: 'switch', type: 'switch', mode: ['properties'],
},{
id: 'description', label: gettext('Comment'),
type: 'multiline', cellHeaderClasses: 'width_percent_50',
},
],
/*
* Triggers control specific error messages for source type and
* target type if any one of them is not selected while creating
* new cast
*/
validate: function() {
var srctype = this.get('srctyp'),
trgtype = this.get('trgtyp'),
msg;
// validate source type control
if (
_.isUndefined(srctype) || _.isNull(srctype) ||
String(srctype).replace(/^\s+|\s+$/g, '') == ''
) {
msg = gettext('Source type must be selected.');
this.errorModel.set('srctyp', msg);
return msg;
} else {
this.errorModel.unset('srctyp');
}
// validate target type control
if (
_.isUndefined(trgtype) || _.isNull(trgtype) ||
String(trgtype).replace(/^\s+|\s+$/g, '') == ''
) {
msg = gettext('Target type must be selected.');
this.errorModel.set('trgtyp', msg);
return msg;
} else {
this.errorModel.unset('trgtyp');
}
this.trigger('on-status-clear');
return null;
},
}),
getSchema: function(treeNodeInfo, itemNodeData){
let schema = new CastSchema({
getTypeOptions: ()=>getNodeAjaxOptions('get_type', this, treeNodeInfo, itemNodeData),
getFuncOptions: (srcTyp, trgtyp) =>
{
return new Promise((resolve, reject)=>{
const api = getApiInstance();
var _url = pgBrowser.Nodes['cast'].generate_url.apply(
pgBrowser.Nodes['cast'], [
null, 'get_functions', itemNodeData, false,
treeNodeInfo,
]);
var data = {'srctyp' : srcTyp, 'trgtyp' : trgtyp};
if(srcTyp != undefined && srcTyp != '' &&
trgtyp != undefined && trgtyp != ''){
api.post(_url, data)
.then(res=>{
data = res.data.data;
resolve(data);
})
.catch((err)=>{
reject(err);
});
}
});
},
},
);
return schema;
},
});
}

View File

@ -0,0 +1,193 @@
/////////////////////////////////////////////////////////////
//
// 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 { isEmptyString } from 'sources/validators';
export default class CastSchema extends BaseUISchema {
constructor(fieldOptions={}, initValues) {
super({
name: undefined, // Name of the cast
encoding: 'UTF8',
srctyp: undefined, // Source type
trgtyp: undefined, // Target type
proname: undefined, // Function
castcontext: true, // Context (IMPLICIT/EXPLICIT/ASSIGNMENT)
syscast: undefined, // Is this cast is system object? Yes/No
description: undefined, // Comment on the cast
...initValues,
});
this.fieldOptions=fieldOptions;
}
get idAttribute() {
return 'oid';
}
get baseFields() {
let obj = this;
return [{
id: 'name', label: gettext('Name'), cell: 'string',
editable: false, type: 'text', readonly: true, cellHeaderClasses: 'width_percent_50',
},{
id: 'oid', label: gettext('OID'), cell: 'string',
editable: false, type: 'text', mode: ['properties'],
},{
id: 'srctyp', label: gettext('Source type'),
type: 'select', group: gettext('Definition'), readonly: function(state) {
return !obj.isNew(state);
}, mode: ['create'],
options:this.fieldOptions.getTypeOptions,
/*
* Control is extended to create cast name from source type and destination type
* once their values are changed
*/
depChange: (state)=>{
/*
* On source type change, check if both source type and
* target type are set, if yes then fetch values from both
* controls and generate cast name
*/
var srctype = state.srctyp;
var trgtype = state.trgtyp;
if(srctype != undefined && srctype != '' &&
trgtype != undefined && trgtype != '')
return state.name = srctype+'->'+trgtype;
else
return state.name = '';
},
},
/*
* Text control for viewing source type in properties and
* edit mode only
*/
{
id: 'srctyp', label: gettext('Source type'), type: 'text',
group: gettext('Definition'), readonly: true, mode:['properties','edit'],
},{
id: 'trgtyp', label: gettext('Target type'),
type: 'select', group: gettext('Definition'), readonly: function(state) {
return !obj.isNew(state);
}, mode: ['create'],
options:this.fieldOptions.getTypeOptions,
depChange: (state)=>{
/*
* On source type change, check if both source type and
* target type are set, if yes then fetch values from both
* controls and generate cast name
*/
var srctype = state.srctyp;
var trgtype = state.trgtyp;
if(srctype != undefined && srctype != '' &&
trgtype != undefined && trgtype != '')
return state.name = srctype+'->'+trgtype;
else
return state.name = '';
},
},
/*
* Text control for viewing target type in properties and
* edit mode only
*/
{
id: 'trgtyp', label: gettext('Target type'), type: 'text',
group: gettext('Definition'), readonly: true, mode:['properties','edit'],
},
/*
* Proname field is dependent on source type and target type.
* On source and target type changed event,
* associated functions will be fetch using ajax call
*/
{
id: 'proname', label: gettext('Function'), deps:['srctyp', 'trgtyp'],
readonly: function(state) { return !obj.isNew(state); },
group: gettext('Definition'), mode: ['create'],
first_empty: true,
type: (state)=>{
let fetchOptionsBasis = state.srctyp + state.trgtyp;
return {
type: 'select',
options: ()=>obj.fieldOptions.getFuncOptions(state.srctyp, state.trgtyp),
optionsReloadBasis: fetchOptionsBasis,
};
},
},
/*
* Text type control for viewing function name in properties and
* edit mode only
*/
{
id: 'proname', label: gettext('Function'), type: 'text',
group: gettext('Definition'), readonly: true, mode:['properties','edit'],
},
{
id: 'castcontext', label: gettext('Context'),type: 'toggle',
options: [
{'label': gettext('IMPLICIT'), value: true},
{'label': gettext('EXPLICIT'), value: false},
],
group: gettext('Definition'),
mode:['create'],
},
/*
* Text control for viewing context in properties and
* edit mode
*/
{
id: 'castcontext', label: gettext('Context'), readonly: true,
options:[{
label: 'IMPLICIT', value: 'IMPLICIT',
},{
label: 'EXPLICIT', value: 'EXPLICIT',
},{
label: 'ASSIGNMENT', value: 'ASSIGNMENT',
}], type: 'select', group: gettext('Definition'),
mode:['properties', 'edit'],
controlProps: {
editable: false,
},
},{
id: 'syscast', label: gettext('System cast?'),
cell: 'switch', type: 'switch', mode: ['properties'],
},{
id: 'description', label: gettext('Comment'),
type: 'multiline', cellHeaderClasses: 'width_percent_50',
},
];
}
validate(state, setError) {
let errmsg = null;
if (isEmptyString(state.srctyp)) {
errmsg = gettext('Source type must be selected.');
setError('srctyp', errmsg);
return true;
} else {
errmsg = null;
setError('srctyp', errmsg);
}
if (isEmptyString(state.trgtyp)) {
errmsg = gettext('Target type must be selected.');
setError('trgtyp', errmsg);
return true;
} else {
errmsg = null;
setError('trgtyp', errmsg);
}
return false;
}
}

View File

@ -5,11 +5,11 @@ CREATE CAST ({{ conn|qtTypeIdent(data.srctyp) }} AS {{ conn|qtTypeIdent(data.trg
{% if data.proname and data.proname != 'binary compatible'%}
WITH FUNCTION {{data.proname}}{% else %}
WITHOUT FUNCTION{% endif %}
{% if data.castcontext and data.castcontext != 'EXPLICIT' %}
{% if data.castcontext and data.castcontext != 'false' %}
AS {{data.castcontext}}{% endif %};
AS {{'IMPLICIT'}}{% endif %};
{# Description for CAST #}
{% if data.description %}
COMMENT ON CAST ({{ conn|qtTypeIdent(data.srctyp) }} AS {{ conn|qtTypeIdent(data.trgtyp) }})
IS {{ data.description|qtLiteral }};
{% endif %}{% endif %}
{% endif %}{% endif %}

View File

@ -0,0 +1,112 @@
/////////////////////////////////////////////////////////////
//
// 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 CastSchema from '../../../pgadmin/browser/server_groups/servers/databases/casts/static/js/cast.ui';
describe('CastSchema', ()=>{
let mount;
let schemaObj = new CastSchema(
{
getTypeOptions: ()=>[],
getFuncOptions: ()=>[],
},
);
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={schemaObj}
viewHelperProps={{
mode: 'create',
}}
onSave={()=>{}}
onClose={()=>{}}
onHelp={()=>{}}
onEdit={()=>{}}
onDataChange={()=>{}}
confirmOnCloseReset={false}
hasSQL={false}
disableSqlHelp={false}
/>);
});
it('edit', ()=>{
mount(<SchemaView
formType='dialog'
schema={schemaObj}
getInitData={getInitData}
viewHelperProps={{
mode: 'create',
}}
onSave={()=>{}}
onClose={()=>{}}
onHelp={()=>{}}
onEdit={()=>{}}
onDataChange={()=>{}}
confirmOnCloseReset={false}
hasSQL={false}
disableSqlHelp={false}
/>);
});
it('properties', ()=>{
mount(<SchemaView
formType='tab'
schema={schemaObj}
getInitData={getInitData}
viewHelperProps={{
mode: 'properties',
}}
onHelp={()=>{}}
onEdit={()=>{}}
/>);
});
it('validate', ()=>{
let state = {};
let setError = jasmine.createSpy('setError');
state.srctyp = null;
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('srctyp', 'Source type must be selected.');
state.srctyp = 'bigint';
state.trgtyp = null;
schemaObj.validate(state, setError);
expect(setError).toHaveBeenCalledWith('trgtyp', 'Target type must be selected.');
});
});