Port Collation node to react. Fixes #6584.
parent
a06f78b2d5
commit
6fe83d1f06
|
@ -7,6 +7,9 @@
|
||||||
//
|
//
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
import CollationSchema from './collation.ui';
|
||||||
|
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../static/js/node_ajax';
|
||||||
|
|
||||||
define('pgadmin.node.collation', [
|
define('pgadmin.node.collation', [
|
||||||
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
|
||||||
'sources/pgadmin', 'pgadmin.browser',
|
'sources/pgadmin', 'pgadmin.browser',
|
||||||
|
@ -68,14 +71,6 @@ define('pgadmin.node.collation', [
|
||||||
},
|
},
|
||||||
model: pgAdmin.Browser.Node.Model.extend({
|
model: pgAdmin.Browser.Node.Model.extend({
|
||||||
idAttribute: 'oid',
|
idAttribute: 'oid',
|
||||||
defaults: {
|
|
||||||
name: undefined,
|
|
||||||
oid: undefined,
|
|
||||||
owner: undefined,
|
|
||||||
lc_type: undefined,
|
|
||||||
lc_collate: undefined,
|
|
||||||
description: undefined,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Default values!
|
// Default values!
|
||||||
initialize: function(attrs, args) {
|
initialize: function(attrs, args) {
|
||||||
|
@ -103,136 +98,29 @@ define('pgadmin.node.collation', [
|
||||||
type: 'text', mode: ['properties', 'create', 'edit'],
|
type: 'text', mode: ['properties', 'create', 'edit'],
|
||||||
disabled: 'inSchema', control: 'node-list-by-name',
|
disabled: 'inSchema', control: 'node-list-by-name',
|
||||||
node: 'role',
|
node: 'role',
|
||||||
},{
|
|
||||||
id: 'schema', label: gettext('Schema'), cell: 'string',
|
|
||||||
type: 'text', mode: ['create', 'edit'], node: 'schema',
|
|
||||||
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;
|
|
||||||
}, control: 'node-list-by-name',
|
|
||||||
cache_node: 'database', cached_level: 'database',
|
|
||||||
},{
|
|
||||||
id: 'copy_collation', label: gettext('Copy collation'), cell: 'string',
|
|
||||||
control: 'node-ajax-options',
|
|
||||||
type: 'text', mode: ['create', 'edit'], group: gettext('Definition'),
|
|
||||||
url: 'get_collations', disabled: 'inSchemaWithModelCheck',
|
|
||||||
readonly: function(m) {return !m.isNew;},
|
|
||||||
deps: ['locale', 'lc_collate', 'lc_type'],
|
|
||||||
},{
|
|
||||||
id: 'locale', label: gettext('Locale'), cell: 'string',
|
|
||||||
type: 'text', mode: ['create', 'edit'], group: gettext('Definition'),
|
|
||||||
disabled: 'inSchemaWithModelCheck', readonly: function(m) {return !m.isNew;},
|
|
||||||
deps: ['lc_collate', 'lc_type', 'copy_collation'],
|
|
||||||
},{
|
|
||||||
id: 'lc_collate', label: gettext('LC_COLLATE'), cell: 'string',
|
|
||||||
type: 'text', mode: ['properties', 'create', 'edit'], group: gettext('Definition'),
|
|
||||||
deps: ['locale', 'copy_collation'], disabled: 'inSchemaWithModelCheck',
|
|
||||||
readonly: function(m) {return !m.isNew;},
|
|
||||||
},{
|
|
||||||
id: 'lc_type', label: gettext('LC_TYPE'), cell: 'string',
|
|
||||||
type: 'text', mode: ['properties', 'create', 'edit'], group: gettext('Definition'),
|
|
||||||
disabled: 'inSchemaWithModelCheck', readonly: function(m) {return !m.isNew;},
|
|
||||||
deps: ['locale', 'copy_collation'],
|
|
||||||
},{
|
|
||||||
id: 'is_sys_obj', label: gettext('System collation?'),
|
|
||||||
cell:'boolean', type: 'switch', mode: ['properties'],
|
|
||||||
},{
|
},{
|
||||||
id: 'description', label: gettext('Comment'), cell: 'string',
|
id: 'description', label: gettext('Comment'), cell: 'string',
|
||||||
type: 'multiline', mode: ['properties', 'create', 'edit'],
|
type: 'multiline', mode: ['properties', 'create', 'edit'],
|
||||||
disabled: 'inSchema',
|
disabled: 'inSchema',
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
validate: function() {
|
|
||||||
var err = {},
|
|
||||||
msg = undefined,
|
|
||||||
changedAttrs = this.changed,
|
|
||||||
locale_flag = false,
|
|
||||||
lc_type_flag = false,
|
|
||||||
lc_coll_flag = false,
|
|
||||||
copy_coll_flag = false,
|
|
||||||
data = this.toJSON();
|
|
||||||
|
|
||||||
this.errorModel.clear();
|
|
||||||
|
|
||||||
if (_.has(changedAttrs,data.name) && _.isUndefined(this.get('name'))
|
|
||||||
|| String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
|
|
||||||
msg = gettext('Name cannot be empty.');
|
|
||||||
this.errorModel.set('name', msg);
|
|
||||||
}
|
|
||||||
if (_.has(changedAttrs,data.locale) && (_.isUndefined(this.get('locale'))
|
|
||||||
|| String(this.get('locale')).replace(/^\s+|\s+$/g, '') == '')) {
|
|
||||||
locale_flag = true;
|
|
||||||
}
|
|
||||||
if (_.has(changedAttrs,data.lc_collate) && (_.isUndefined(this.get('lc_collate'))
|
|
||||||
|| String(this.get('lc_collate')).replace(/^\s+|\s+$/g, '') == '')) {
|
|
||||||
lc_coll_flag = true;
|
|
||||||
}
|
|
||||||
if (_.has(changedAttrs,data.lc_type) && (_.isUndefined(this.get('lc_type'))
|
|
||||||
|| String(this.get('lc_type')).replace(/^\s+|\s+$/g, '') == '')) {
|
|
||||||
lc_type_flag = true;
|
|
||||||
}
|
|
||||||
if (_.has(changedAttrs,data.copy_collation) && (_.isUndefined(this.get('copy_collation'))
|
|
||||||
|| String(this.get('copy_collation')).replace(/^\s+|\s+$/g, '') == '')) {
|
|
||||||
copy_coll_flag = true;
|
|
||||||
}
|
|
||||||
if (locale_flag && (lc_coll_flag || lc_type_flag) && copy_coll_flag) {
|
|
||||||
msg = gettext('Definition incomplete. Please provide Locale OR Copy Collation OR LC_TYPE/LC_COLLATE.');
|
|
||||||
err['locale'] = msg;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
// We will disable everything if we are under catalog node
|
|
||||||
inSchema: function() {
|
|
||||||
if(this.node_info && 'catalog' in this.node_info)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
// Enable copy_collation only if locale & lc_* is not provided
|
|
||||||
if (m.isNew() && this.name == 'copy_collation')
|
|
||||||
{
|
|
||||||
if(m.get('locale'))
|
|
||||||
return true;
|
|
||||||
if(m.get('lc_collate') || m.get('lc_type'))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable lc_* only if copy_collation & locale is not provided
|
|
||||||
if (m.isNew() && (this.name == 'lc_collate' || this.name == 'lc_type'))
|
|
||||||
{
|
|
||||||
if(m.get('locale'))
|
|
||||||
return true;
|
|
||||||
if(m.get('copy_collation'))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable localy only if lc_* & copy_collation is not provided
|
|
||||||
if (m.isNew() && this.name == 'locale')
|
|
||||||
{
|
|
||||||
if(m.get('lc_collate') || m.get('lc_type'))
|
|
||||||
return true;
|
|
||||||
if(m.get('copy_collation'))
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
getSchema: (treeNodeInfo, itemNodeData)=>{
|
||||||
|
let nodeObj = pgAdmin.Browser.Nodes['collation'];
|
||||||
|
let schema = new CollationSchema(
|
||||||
|
{
|
||||||
|
rolesList: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData, {cacheLevel: 'server'}),
|
||||||
|
schemaList: ()=>getNodeListByName('schema', treeNodeInfo, itemNodeData, {cacheLevel: 'database'}),
|
||||||
|
collationsList: ()=>getNodeAjaxOptions('get_collations', nodeObj, treeNodeInfo, itemNodeData, {cacheLevel: 'server'})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner: pgBrowser.serverInfo[treeNodeInfo.server._id].user.name,
|
||||||
|
schema: ('schema' in treeNodeInfo)? treeNodeInfo.schema.label : ''
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pgBrowser.Nodes['collation'];
|
return pgBrowser.Nodes['collation'];
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// pgAdmin 4 - PostgreSQL Tools
|
||||||
|
//
|
||||||
|
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
|
||||||
|
// This software is released under the PostgreSQL Licence
|
||||||
|
//
|
||||||
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||||
|
import gettext from 'sources/gettext';
|
||||||
|
import { isEmptyString } from 'sources/validators';
|
||||||
|
|
||||||
|
export default class CollationSchema extends BaseUISchema {
|
||||||
|
constructor(fieldOptions = {},initValues) {
|
||||||
|
super({
|
||||||
|
name: undefined,
|
||||||
|
oid: undefined,
|
||||||
|
owner: undefined,
|
||||||
|
copy_collation: null,
|
||||||
|
locale: undefined,
|
||||||
|
lc_type: undefined,
|
||||||
|
lc_collate: undefined,
|
||||||
|
description: undefined,
|
||||||
|
schema: null,
|
||||||
|
...initValues
|
||||||
|
});
|
||||||
|
this.schemaList = fieldOptions.schemaList;
|
||||||
|
this.ownerList = fieldOptions.rolesList;
|
||||||
|
this.collationsList = fieldOptions.collationsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get idAttribute() {
|
||||||
|
return 'oid';
|
||||||
|
}
|
||||||
|
|
||||||
|
get baseFields() {
|
||||||
|
let obj = this;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'name', label: gettext('Name'),
|
||||||
|
type: 'text', mode: ['properties', 'create', 'edit'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'oid', label: gettext('OID'),
|
||||||
|
type: 'text', mode: ['properties'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'owner', label: gettext('Owner'),
|
||||||
|
type: 'select', mode: ['properties', 'create', 'edit'],
|
||||||
|
options: obj.ownerList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'schema', label: gettext('Schema'),
|
||||||
|
mode: ['create', 'edit'], node: 'schema',
|
||||||
|
type: () => {
|
||||||
|
return {
|
||||||
|
type: 'select',
|
||||||
|
options: obj.schemaList,
|
||||||
|
controlProps: {
|
||||||
|
filter: (options) => {
|
||||||
|
let res = [];
|
||||||
|
options.forEach((d) => {
|
||||||
|
if (!(d && d.label.match(/^pg_/)))
|
||||||
|
res.push(d);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy_collation', label: gettext('Copy collation'),
|
||||||
|
type: 'select', mode: ['create', 'edit'], group: gettext('Definition'),
|
||||||
|
readonly: function (state) { return !obj.isNew(state); },
|
||||||
|
options: obj.collationsList,
|
||||||
|
disabled: function (state) {
|
||||||
|
// Enable copy_collation only if locale & lc_* is not provided
|
||||||
|
if (state.locale)
|
||||||
|
return true;
|
||||||
|
if (state.lc_collate || state.lc_type)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
deps: ['locale', 'lc_collate', 'lc_type'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'locale', label: gettext('Locale'),
|
||||||
|
type: 'text', mode: ['create', 'edit'], group: gettext('Definition'),
|
||||||
|
readonly: function (state) { return !obj.isNew(state); },
|
||||||
|
deps: ['lc_collate', 'lc_type', 'copy_collation'],
|
||||||
|
disabled: function (state) {
|
||||||
|
// Enable localy only if lc_* & copy_collation is not provided
|
||||||
|
if (state.lc_collate || state.lc_type)
|
||||||
|
return true;
|
||||||
|
if (state.copy_collation)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lc_collate', label: gettext('LC_COLLATE'),
|
||||||
|
type: 'text', mode: ['properties', 'create', 'edit'], group: gettext('Definition'),
|
||||||
|
readonly: function (state) { return !obj.isNew(state); },
|
||||||
|
disabled: obj.disableFields,
|
||||||
|
deps: ['locale', 'copy_collation'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lc_type', label: gettext('LC_TYPE'),
|
||||||
|
type: 'text', mode: ['properties', 'create', 'edit'], group: gettext('Definition'),
|
||||||
|
readonly: function (state) { return !obj.isNew(state); },
|
||||||
|
disabled: obj.disableFields,
|
||||||
|
deps: ['locale', 'copy_collation'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'is_sys_obj', label: gettext('System collation?'),
|
||||||
|
cell: 'boolean', type: 'switch', mode: ['properties'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'description', label: gettext('Comment'),
|
||||||
|
type: 'multiline', mode: ['properties', 'create', 'edit'],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
disableFields(state) {
|
||||||
|
// Enable lc_* only if copy_collation & locale is not provided
|
||||||
|
if (state.locale || state.copy_collation) {
|
||||||
|
if (state.locale)
|
||||||
|
return true;
|
||||||
|
if (state.copy_collation)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(state, setError) {
|
||||||
|
let errmsg = null,
|
||||||
|
locale_flag = false,
|
||||||
|
lc_type_flag = false,
|
||||||
|
lc_coll_flag = false,
|
||||||
|
copy_coll_flag = false;
|
||||||
|
|
||||||
|
if (isEmptyString(state.name)) {
|
||||||
|
errmsg = gettext('Name cannot be empty.');
|
||||||
|
setError('name', errmsg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (isEmptyString(state.locale)) {
|
||||||
|
locale_flag = true;
|
||||||
|
}
|
||||||
|
if (isEmptyString(state.copy_collation)) {
|
||||||
|
copy_coll_flag = true;
|
||||||
|
}
|
||||||
|
if (isEmptyString(state.lc_collate)) {
|
||||||
|
lc_coll_flag = true;
|
||||||
|
}
|
||||||
|
if (isEmptyString(state.lc_type)) {
|
||||||
|
lc_type_flag = true;
|
||||||
|
}
|
||||||
|
if (locale_flag && (lc_coll_flag || lc_type_flag) && copy_coll_flag) {
|
||||||
|
errmsg = gettext('Definition incomplete. Please provide Locale OR Copy Collation OR LC_TYPE/LC_COLLATE.');
|
||||||
|
setError('copy_collation', errmsg);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
/////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// 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';
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
|
||||||
|
describe('CollationsSchema', () => {
|
||||||
|
let mount;
|
||||||
|
let schemaObj = new CollationSchema(
|
||||||
|
{
|
||||||
|
rolesList: () => [],
|
||||||
|
schemaList: () => [],
|
||||||
|
collationsList: () => []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner: 'postgres',
|
||||||
|
schema: ''
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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.name = null;
|
||||||
|
state.locale = 'locale';
|
||||||
|
schemaObj.validate(state, setError);
|
||||||
|
expect(setError).toHaveBeenCalledWith('name', 'Name cannot be empty.');
|
||||||
|
|
||||||
|
state.name = 'test';
|
||||||
|
state.copy_collation = null;
|
||||||
|
state.lc_type = null;
|
||||||
|
state.lc_collate = null;
|
||||||
|
state.locale = null;
|
||||||
|
schemaObj.validate(state, setError);
|
||||||
|
expect(setError).toHaveBeenCalledWith('copy_collation', 'Definition incomplete. Please provide Locale OR Copy Collation OR LC_TYPE/LC_COLLATE.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disableFields',() => {
|
||||||
|
let state = {};
|
||||||
|
|
||||||
|
state.name = 'test';
|
||||||
|
state.locale = 'locale';
|
||||||
|
expect(schemaObj.disableFields(state)).toBeTrue();
|
||||||
|
|
||||||
|
state.name = 'test';
|
||||||
|
state.copy_collation = 'copy_collation';
|
||||||
|
state.locale = null;
|
||||||
|
expect(schemaObj.disableFields(state)).toBeTrue();
|
||||||
|
|
||||||
|
state.name = 'test';
|
||||||
|
state.copy_collation = null;
|
||||||
|
state.locale = null;
|
||||||
|
expect(schemaObj.disableFields(state)).toBeFalse();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue