mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Improved the extendability of the SchemaView and DataGridView. (#7876)
Restructured these modules for ease of maintenance and apply the single responsibility principle (wherever applicable). * SchemaView - Split the code based on the functionality and responsibility. - Introduced a new View 'InlineView' instead of using the 'nextInline' configuration of the fields to have a better, and manageable view. - Using the separate class 'SchemaState' for managing the data and states of the SchemaView (separated from the 'useSchemaState' custom hook). - Introduced three new custom hooks 'useFieldValue', 'useFieldOptions', 'useFieldError' for the individual control to use for each Schema Field. - Don't pass value as the parameter props, and let the 'useFieldValue' and other custom hooks to decide, whether to rerender the control itself or the whole dialog/view. (single responsibility principle) - Introduced a new data store with a subscription facility. - Moving the field metadata (option) evaluation to a separate place for better management, and each option can be defined for a particular kind of field (for example - collection, row, cell, general, etc). - Allow to provide custom control for all kind of Schema field. * DataGridView - Same as SchemaView, split the DataGridView call into smaller, manageable chunks. (For example - grid, row, mappedCell, etc). - Use context based approach for providing the row and table data instead of passing them as parameters to every component separately. - Have a facility to extend this feature separately in future. (for example - selectable cell, column grouping, etc.) - Separated the features like deletable, editable, reorder, expandable etc. cells using the above feature support. - Added ability to provide the CustomHeader, and CustomRow through the Schema field, which will extend the ability to customize better. - Removed the 'DataGridViewWithHeaderForm' as it has been achieved through providing 'CustomHeader', and also introduced 'DataGridFormHeader' (a custom header) to achieve the same feature as 'DataGridViewWithHeaderForm'.
This commit is contained in:
@@ -80,7 +80,7 @@ module.exports = [
|
||||
'error',
|
||||
'only-multiline',
|
||||
],
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'no-console': ['error', { allow: ['warn', 'error', 'trace'] }],
|
||||
// We need to exclude below for RegEx case
|
||||
'no-useless-escape': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
|
@@ -140,7 +140,7 @@
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-new-window": "^1.0.1",
|
||||
"react-resize-detector": "^11.0.1",
|
||||
"react-rnd": "^10.3.5",
|
||||
"react-rnd": "^10.4.12",
|
||||
"react-select": "^5.7.2",
|
||||
"react-timer-hook": "^3.0.5",
|
||||
"react-virtualized-auto-sizer": "^1.0.6",
|
||||
@@ -152,7 +152,7 @@
|
||||
"uplot-react": "^1.1.4",
|
||||
"valid-filename": "^2.0.1",
|
||||
"wkx": "^0.5.0",
|
||||
"zustand": "^4.4.1"
|
||||
"zustand": "^4.5.4"
|
||||
},
|
||||
"scripts": {
|
||||
"linter": "yarn run eslint -c .eslintrc.js .",
|
||||
|
@@ -39,7 +39,9 @@ export class DomainConstSchema extends BaseUISchema {
|
||||
id: 'convalidated', label: gettext('Validate?'), cell: 'checkbox',
|
||||
type: 'checkbox',
|
||||
readonly: function(state) {
|
||||
let currCon = _.find(obj.top.origData.constraints, (con)=>con.conoid == state.conoid);
|
||||
let currCon = _.find(
|
||||
obj.top.origData.constraints, (con) => con.conoid == state.conoid
|
||||
);
|
||||
return !obj.isNew(state) && currCon.convalidated;
|
||||
},
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import DataGridViewWithHeaderForm from 'sources/helpers/DataGridViewWithHeaderForm';
|
||||
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
|
||||
import { isEmptyString } from '../../../../../../../../static/js/validators';
|
||||
|
||||
class TokenHeaderSchema extends BaseUISchema {
|
||||
@@ -155,8 +155,8 @@ export default class FTSConfigurationSchema extends BaseUISchema {
|
||||
group: gettext('Tokens'), mode: ['create','edit'],
|
||||
editable: false, schema: this.tokColumnSchema,
|
||||
headerSchema: this.tokHeaderSchema,
|
||||
headerVisible: function() { return true;},
|
||||
CustomControl: DataGridViewWithHeaderForm,
|
||||
headerFormVisible: true,
|
||||
GridHeader: DataGridFormHeader,
|
||||
uniqueCol : ['token'],
|
||||
canAdd: true, canEdit: false, canDelete: true,
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@ import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import _ from 'lodash';
|
||||
import { isEmptyString } from 'sources/validators';
|
||||
import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../../../static/js/SchemaView';
|
||||
import DataGridViewWithHeaderForm from '../../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm';
|
||||
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView';
|
||||
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
|
||||
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax';
|
||||
import TableSchema from '../../../../static/js/table.ui';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
@@ -342,10 +342,12 @@ export default class ExclusionConstraintSchema extends BaseUISchema {
|
||||
group: gettext('Columns'), type: 'collection',
|
||||
mode: ['create', 'edit', 'properties'],
|
||||
editable: false, schema: this.exColumnSchema,
|
||||
headerSchema: this.exHeaderSchema, headerVisible: (state)=>obj.isNew(state),
|
||||
CustomControl: DataGridViewWithHeaderForm,
|
||||
headerSchema: this.exHeaderSchema,
|
||||
headerFormVisible: (state)=>obj.isNew(state),
|
||||
GridHeader: DataGridFormHeader,
|
||||
uniqueCol: ['column'],
|
||||
canAdd: false, canDelete: function(state) {
|
||||
canAdd: (state)=>obj.isNew(state),
|
||||
canDelete: function(state) {
|
||||
// We can't update columns of existing
|
||||
return obj.isNew(state);
|
||||
},
|
||||
|
@@ -11,11 +11,12 @@ import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import _ from 'lodash';
|
||||
import { isEmptyString } from 'sources/validators';
|
||||
import { SCHEMA_STATE_ACTIONS } from '../../../../../../../../../../static/js/SchemaView';
|
||||
import DataGridViewWithHeaderForm from '../../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm';
|
||||
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView';
|
||||
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
|
||||
import { getNodeAjaxOptions, getNodeListByName } from '../../../../../../../../../static/js/node_ajax';
|
||||
import TableSchema from '../../../../static/js/table.ui';
|
||||
|
||||
|
||||
export function getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, noColumns=false, initData={}) {
|
||||
return new ForeignKeySchema({
|
||||
local_column: noColumns ? [] : ()=>getNodeListByName('column', treeNodeInfo, itemNodeData),
|
||||
@@ -58,12 +59,20 @@ class ForeignKeyHeaderSchema extends BaseUISchema {
|
||||
}
|
||||
|
||||
addDisabled(state) {
|
||||
return !(state.local_column && (state.references || this.origData.references) && state.referenced);
|
||||
return !(
|
||||
state.local_column && (
|
||||
state.references || this.origData.references
|
||||
) && state.referenced
|
||||
);
|
||||
}
|
||||
|
||||
/* Data to ForeignKeyColumnSchema will added using the header form */
|
||||
getNewData(data) {
|
||||
let references_table_name = _.find(this.refTables, (t)=>t.value==data.references || t.value == this.origData.references)?.label;
|
||||
let references_table_name = _.find(
|
||||
this.refTables,
|
||||
(t) => t.value == data.references || t.value == this.origData.references
|
||||
)?.label;
|
||||
|
||||
return {
|
||||
local_column: data.local_column,
|
||||
referenced: data.referenced,
|
||||
@@ -229,7 +238,10 @@ export default class ForeignKeySchema extends BaseUISchema {
|
||||
if(!obj.isNew(state)) {
|
||||
let origData = {};
|
||||
if(obj.inTable && obj.top) {
|
||||
origData = _.find(obj.top.origData['foreign_key'], (r)=>r.cid == state.cid);
|
||||
origData = _.find(
|
||||
obj.top.origData['foreign_key'],
|
||||
(r) => r.cid == state.cid
|
||||
);
|
||||
} else {
|
||||
origData = obj.origData;
|
||||
}
|
||||
@@ -304,14 +316,14 @@ export default class ForeignKeySchema extends BaseUISchema {
|
||||
group: gettext('Columns'), type: 'collection',
|
||||
mode: ['create', 'edit', 'properties'],
|
||||
editable: false, schema: this.fkColumnSchema,
|
||||
headerSchema: this.fkHeaderSchema, headerVisible: (state)=>obj.isNew(state),
|
||||
CustomControl: DataGridViewWithHeaderForm,
|
||||
headerSchema: this.fkHeaderSchema,
|
||||
headerFormVisible: (state)=>obj.isNew(state),
|
||||
GridHeader: DataGridFormHeader,
|
||||
uniqueCol: ['local_column', 'references', 'referenced'],
|
||||
canAdd: false, canDelete: function(state) {
|
||||
// We can't update columns of existing foreign key.
|
||||
return obj.isNew(state);
|
||||
},
|
||||
readonly: obj.isReadonly, cell: ()=>({
|
||||
canAdd: (state)=>obj.isNew(state),
|
||||
canDelete: (state)=>obj.isNew(state),
|
||||
readonly: obj.isReadonly,
|
||||
cell: () => ({
|
||||
cell: '',
|
||||
controlProps: {
|
||||
formatter: {
|
||||
@@ -358,9 +370,10 @@ export default class ForeignKeySchema extends BaseUISchema {
|
||||
}
|
||||
}
|
||||
if(actionObj.type == SCHEMA_STATE_ACTIONS.ADD_ROW) {
|
||||
obj.fkHeaderSchema.origData.references = null;
|
||||
// Set references value.
|
||||
obj.fkHeaderSchema.origData.references = obj.fkHeaderSchema.sessData.references;
|
||||
obj.fkHeaderSchema.origData.references = null;
|
||||
obj.fkHeaderSchema.origData.references =
|
||||
obj.fkHeaderSchema.sessData.references;
|
||||
obj.fkHeaderSchema.origData._disable_references = true;
|
||||
}
|
||||
return {columns: currColumns};
|
||||
|
@@ -7,12 +7,13 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import DataGridViewWithHeaderForm from '../../../../../../../../../static/js/helpers/DataGridViewWithHeaderForm';
|
||||
import _ from 'lodash';
|
||||
import { isEmptyString } from 'sources/validators';
|
||||
|
||||
import { DataGridFormHeader } from 'sources/SchemaView/DataGridView';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import gettext from 'sources/gettext';
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
import { isEmptyString } from 'sources/validators';
|
||||
|
||||
|
||||
function inSchema(node_info) {
|
||||
@@ -23,8 +24,8 @@ class IndexColHeaderSchema extends BaseUISchema {
|
||||
constructor(columns) {
|
||||
super({
|
||||
is_exp: true,
|
||||
colname: undefined,
|
||||
expression: undefined,
|
||||
colname: '',
|
||||
expression: '',
|
||||
});
|
||||
|
||||
this.columns = columns;
|
||||
@@ -90,10 +91,10 @@ class IndexColumnSchema extends BaseUISchema {
|
||||
}
|
||||
|
||||
isEditable(state) {
|
||||
let topObj = this._top;
|
||||
let topObj = this.top;
|
||||
if(this.inSchemaWithModelCheck(state)) {
|
||||
return false;
|
||||
} else if (topObj._sessData && topObj._sessData.amname === 'btree') {
|
||||
} else if (topObj.sessData && topObj.sessData.amname === 'btree') {
|
||||
state.is_sort_nulls_applicable = true;
|
||||
return true;
|
||||
} else {
|
||||
@@ -155,9 +156,8 @@ class IndexColumnSchema extends BaseUISchema {
|
||||
* to access method selected by user if not selected
|
||||
* send btree related op_class options
|
||||
*/
|
||||
let amname = obj._top?._sessData ?
|
||||
obj._top?._sessData.amname :
|
||||
obj._top?.origData.amname;
|
||||
let amname = obj.top?.sessData.amname ||
|
||||
obj.top?.origData.amname;
|
||||
|
||||
if(_.isUndefined(amname))
|
||||
return options;
|
||||
@@ -573,10 +573,12 @@ export default class IndexSchema extends BaseUISchema {
|
||||
group: gettext('Columns'), type: 'collection',
|
||||
mode: ['create', 'edit', 'properties'],
|
||||
editable: false, schema: this.indexColumnSchema,
|
||||
headerSchema: this.indexHeaderSchema, headerVisible: (state)=>indexSchemaObj.isNew(state),
|
||||
CustomControl: DataGridViewWithHeaderForm,
|
||||
headerSchema: this.indexHeaderSchema,
|
||||
headerFormVisible: (state)=>indexSchemaObj.isNew(state),
|
||||
GridHeader: DataGridFormHeader,
|
||||
uniqueCol: ['colname'],
|
||||
canAdd: false, canDelete: function(state) {
|
||||
canAdd: (state)=>indexSchemaObj.isNew(state),
|
||||
canDelete: function(state) {
|
||||
// We can't update columns of existing
|
||||
return indexSchemaObj.isNew(state);
|
||||
}, cell: ()=>({
|
||||
|
@@ -25,9 +25,11 @@ import { getNodePrivilegeRoleSchema } from '../../../../../static/js/privilege.u
|
||||
import pgAdmin from 'sources/pgadmin';
|
||||
|
||||
export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) {
|
||||
const spcname = ()=>getNodeListByName('tablespace', treeNodeInfo, itemNodeData, {}, (m)=>{
|
||||
return (m.label != 'pg_global');
|
||||
});
|
||||
const spcname = () => getNodeListByName(
|
||||
'tablespace', treeNodeInfo, itemNodeData, {}, (m) => {
|
||||
return (m.label != 'pg_global');
|
||||
}
|
||||
);
|
||||
|
||||
let tableNode = pgBrowser.Nodes['table'];
|
||||
|
||||
@@ -48,9 +50,9 @@ export function getNodeTableSchema(treeNodeInfo, itemNodeData, pgBrowser) {
|
||||
},
|
||||
treeNodeInfo,
|
||||
{
|
||||
columns: ()=>getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser),
|
||||
vacuum_settings: ()=>getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData),
|
||||
constraints: ()=>new ConstraintsSchema(
|
||||
columns: () => getNodeColumnSchema(treeNodeInfo, itemNodeData, pgBrowser),
|
||||
vacuum_settings: () => getNodeVacuumSettingsSchema(tableNode, treeNodeInfo, itemNodeData),
|
||||
constraints: () => new ConstraintsSchema(
|
||||
treeNodeInfo,
|
||||
()=>getNodeForeignKeySchema(treeNodeInfo, itemNodeData, pgBrowser, true, {autoindex: false}),
|
||||
()=>getNodeExclusionConstraintSchema(treeNodeInfo, itemNodeData, pgBrowser, true),
|
||||
@@ -274,46 +276,47 @@ export class LikeSchema extends BaseUISchema {
|
||||
id: 'like_default_value', label: gettext('With default values?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_constraints', label: gettext('With constraints?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_indexes', label: gettext('With indexes?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_storage', label: gettext('With storage?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_comments', label: gettext('With comments?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_compression', label: gettext('With compression?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
min_version: 140000, inlineNext: true,
|
||||
min_version: 140000, inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_generated', label: gettext('With generated?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
min_version: 120000, inlineNext: true,
|
||||
min_version: 120000, inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_identity', label: gettext('With identity?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'like_relation',
|
||||
},{
|
||||
id: 'like_statistics', label: gettext('With statistics?'),
|
||||
type: 'switch', mode: ['create'], deps: ['like_relation'],
|
||||
disabled: this.isRelationDisable, depChange: (...args)=>obj.resetVals(...args),
|
||||
inlineGroup: 'like_relation',
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -484,6 +487,12 @@ export default class TableSchema extends BaseUISchema {
|
||||
}
|
||||
};
|
||||
}
|
||||
},{
|
||||
id: 'columns', type: 'group', label: gettext('Columns'),
|
||||
},{
|
||||
id: 'advanced', label: gettext('Advanced'), type: 'group',
|
||||
},{
|
||||
id: 'constraints', label: gettext('Constraints'), type: 'group',
|
||||
},{
|
||||
id: 'partition', type: 'group', label: gettext('Partitions'),
|
||||
mode: ['edit', 'create'], min_version: 100000,
|
||||
@@ -494,6 +503,12 @@ export default class TableSchema extends BaseUISchema {
|
||||
// Always show in case of create mode
|
||||
return (obj.isNew(state) || state.is_partitioned);
|
||||
},
|
||||
},{
|
||||
type: 'group', id: 'parameters', label: gettext('Parameters'),
|
||||
visible: !this.inErd,
|
||||
},{
|
||||
id: 'security_group', type: 'group', label: gettext('Security'),
|
||||
visible: !this.inErd,
|
||||
},{
|
||||
id: 'is_partitioned', label:gettext('Partitioned table?'), cell: 'switch',
|
||||
type: 'switch', mode: ['properties', 'create', 'edit'],
|
||||
@@ -510,9 +525,12 @@ export default class TableSchema extends BaseUISchema {
|
||||
mode: ['properties', 'create', 'edit'], disabled: this.inCatalog,
|
||||
},{
|
||||
id: 'coll_inherits', label: gettext('Inherited from table(s)'),
|
||||
type: 'select', group: gettext('Columns'),
|
||||
type: 'select', group: 'columns',
|
||||
deps: ['typname', 'is_partitioned'], mode: ['create', 'edit'],
|
||||
controlProps: { multiple: true, allowClear: false, placeholder: gettext('Select to inherit from...')},
|
||||
controlProps: {
|
||||
multiple: true, allowClear: false,
|
||||
placeholder: gettext('Select to inherit from...')
|
||||
},
|
||||
options: this.fieldOptions.coll_inherits, visible: !this.inErd,
|
||||
optionsLoaded: (res)=>obj.inheritedTableList=res,
|
||||
disabled: (state)=>{
|
||||
@@ -611,9 +629,6 @@ export default class TableSchema extends BaseUISchema {
|
||||
}
|
||||
});
|
||||
},
|
||||
},{
|
||||
id: 'advanced', label: gettext('Advanced'), type: 'group',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
id: 'rlspolicy', label: gettext('RLS Policy?'), cell: 'switch',
|
||||
@@ -654,12 +669,9 @@ export default class TableSchema extends BaseUISchema {
|
||||
},{
|
||||
// Tab control for columns
|
||||
id: 'columns', label: gettext('Columns'), type: 'collection',
|
||||
group: gettext('Columns'),
|
||||
schema: this.columnsSchema,
|
||||
mode: ['create', 'edit'],
|
||||
disabled: this.inCatalog,
|
||||
deps: ['typname', 'is_partitioned'],
|
||||
depChange: (state, source, topState, actionObj)=>{
|
||||
group: 'columns', schema: this.columnsSchema, mode: ['create', 'edit'],
|
||||
disabled: this.inCatalog, deps: ['typname', 'is_partitioned'],
|
||||
depChange: (state, source, topState, actionObj) => {
|
||||
if(source[0] === 'columns') {
|
||||
/* In ERD, attnum is an imp let for setting the links
|
||||
Here, attnum is set to max avail value.
|
||||
@@ -718,7 +730,7 @@ export default class TableSchema extends BaseUISchema {
|
||||
allowMultipleEmptyRow: false,
|
||||
},{
|
||||
// Here we will create tab control for constraints
|
||||
type: 'nested-tab', group: gettext('Constraints'),
|
||||
type: 'nested-tab', group: 'constraints',
|
||||
mode: ['edit', 'create'],
|
||||
schema: obj.constraintsObj,
|
||||
},{
|
||||
@@ -995,17 +1007,12 @@ export default class TableSchema extends BaseUISchema {
|
||||
'</li></ul>',
|
||||
].join(''),
|
||||
min_version: 100000,
|
||||
},{
|
||||
type: 'group', id: 'parameters', label: gettext('Parameters'),
|
||||
visible: !this.inErd,
|
||||
},{
|
||||
// Here - we will create tab control for storage parameters
|
||||
// (auto vacuum).
|
||||
type: 'nested-tab', group: 'parameters',
|
||||
mode: ['edit', 'create'], deps: ['is_partitioned'],
|
||||
schema: this.vacuumSettingsSchema, visible: !this.inErd,
|
||||
},{
|
||||
id: 'security_group', type: 'group', label: gettext('Security'), visible: !this.inErd,
|
||||
},
|
||||
{
|
||||
id: 'relacl_str', label: gettext('Privileges'), disabled: this.inCatalog,
|
||||
|
@@ -1056,7 +1056,7 @@ class DataTypeSchema extends BaseUISchema {
|
||||
}
|
||||
},{
|
||||
id: 'maxsize',
|
||||
group: gettext('Definition'),
|
||||
group: gettext('Data Type'),
|
||||
label: gettext('Size'),
|
||||
type: 'int',
|
||||
deps: ['typtype'],
|
||||
|
@@ -7,6 +7,7 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import SecLabelSchema from '../../../../../static/js/sec_label.ui';
|
||||
@@ -154,7 +155,10 @@ export default class MViewSchema extends BaseUISchema {
|
||||
|
||||
if (state.definition) {
|
||||
obj.warningText = null;
|
||||
if (obj.origData.oid !== undefined && state.definition !== obj.origData.definition) {
|
||||
if (
|
||||
!_.isUndefined(obj.origData.oid) &&
|
||||
state.definition !== obj.origData.definition
|
||||
) {
|
||||
obj.warningText = gettext(
|
||||
'Updating the definition will drop and re-create the materialized view. It may result in loss of information about its dependent objects.'
|
||||
) + '<br><br><b>' + gettext('Do you want to continue?') + '</b>';
|
||||
|
@@ -7,6 +7,7 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
import SecLabelSchema from '../../../../../static/js/sec_label.ui';
|
||||
@@ -135,9 +136,10 @@ export default class ViewSchema extends BaseUISchema {
|
||||
}
|
||||
|
||||
if (state.definition) {
|
||||
if (!(obj.nodeInfo.server.server_type == 'pg' &&
|
||||
if (!(
|
||||
obj.nodeInfo.server.server_type == 'pg' &&
|
||||
// No need to check this when creating a view
|
||||
obj.origData.oid !== undefined
|
||||
!_.isUndefined(obj.sessData.oid)
|
||||
) || (
|
||||
state.definition === obj.origData.definition
|
||||
)) {
|
||||
@@ -150,7 +152,7 @@ export default class ViewSchema extends BaseUISchema {
|
||||
).split('FROM'),
|
||||
new_def = [];
|
||||
|
||||
if (state.definition !== undefined) {
|
||||
if (!_.isUndefined(state.definition)) {
|
||||
new_def = state.definition.replace(
|
||||
/\s/gi, ''
|
||||
).split('FROM');
|
||||
|
@@ -129,7 +129,10 @@ export default class SubscriptionSchema extends BaseUISchema{
|
||||
id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'], min: 1, max: 65535,
|
||||
depChange: (state)=>{
|
||||
if(obj.origData.port != state.port && !obj.isNew(state) && state.connected){
|
||||
if(
|
||||
obj.origData.port != state.port && !obj.isNew(state) &&
|
||||
state.connected
|
||||
) {
|
||||
obj.informText = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
@@ -145,7 +148,10 @@ export default class SubscriptionSchema extends BaseUISchema{
|
||||
id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
|
||||
mode: ['properties', 'edit', 'create'],
|
||||
depChange: (state)=>{
|
||||
if(obj.origData.username != state.username && !obj.isNew(state) && state.connected){
|
||||
if(
|
||||
obj.origData.username != state.username && !obj.isNew(state) &&
|
||||
state.connected
|
||||
) {
|
||||
obj.informText = gettext(
|
||||
'To apply changes to the connection configuration, please disconnect from the server and then reconnect.'
|
||||
);
|
||||
|
@@ -28,7 +28,7 @@ export default class PrivilegeRoleSchema extends BaseUISchema {
|
||||
super({
|
||||
grantee: undefined,
|
||||
grantor: nodeInfo?.server?.user?.name,
|
||||
privileges: undefined,
|
||||
privileges: [],
|
||||
});
|
||||
this.granteeOptions = granteeOptions;
|
||||
this.grantorOptions = grantorOptions;
|
||||
@@ -56,9 +56,12 @@ export default class PrivilegeRoleSchema extends BaseUISchema {
|
||||
{
|
||||
id: 'privileges', label: gettext('Privileges'),
|
||||
type: 'text', group: null,
|
||||
cell: ()=>({cell: 'privilege', controlProps: {
|
||||
supportedPrivs: this.supportedPrivs,
|
||||
}}),
|
||||
cell: () => ({
|
||||
cell: 'privilege',
|
||||
controlProps: {
|
||||
supportedPrivs: this.supportedPrivs,
|
||||
}
|
||||
}),
|
||||
disabled : function(state) {
|
||||
return !(
|
||||
obj.nodeInfo &&
|
||||
|
@@ -136,7 +136,7 @@ export default class ServerSchema extends BaseUISchema {
|
||||
id: 'shared_username', label: gettext('Shared Username'), type: 'text',
|
||||
controlProps: { maxLength: 64},
|
||||
mode: ['properties', 'create', 'edit'], deps: ['shared', 'username'],
|
||||
readonly: (s)=>{
|
||||
readonly: (s) => {
|
||||
return !(!this.origData.shared && s.shared);
|
||||
}, visible: ()=>{
|
||||
return current_user.is_admin && pgAdmin.server_mode == 'True';
|
||||
|
@@ -148,7 +148,7 @@ export default class VariableSchema extends BaseUISchema {
|
||||
editable: function(state) {
|
||||
return obj.isNew(state) || !obj.allReadOnly;
|
||||
},
|
||||
cell: ()=>({
|
||||
cell: () => ({
|
||||
cell: 'select',
|
||||
options: this.vnameOptions,
|
||||
optionsLoaded: (options)=>{obj.setVarTypes(options);},
|
||||
|
@@ -10,6 +10,7 @@
|
||||
import gettext from 'sources/gettext';
|
||||
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
|
||||
|
||||
|
||||
export default class ServerGroupSchema extends BaseUISchema {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -28,7 +29,7 @@ export default class ServerGroupSchema extends BaseUISchema {
|
||||
id: 'name', label: gettext('Name'), type: 'text', group: null,
|
||||
mode: ['properties', 'edit', 'create'], noEmpty: true,
|
||||
disabled: false,
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,8 @@ export default function ObjectNodeProperties({panelId, node, treeNodeInfo, nodeD
|
||||
const treeNodeId = objToString(treeNodeInfo);
|
||||
|
||||
let schema = useMemo(
|
||||
() => node.getSchema(treeNodeInfo, nodeData), [treeNodeId, isActive]
|
||||
() => node.getSchema(treeNodeInfo, nodeData),
|
||||
[treeNodeId]
|
||||
);
|
||||
|
||||
// We only have two actionTypes, 'create' and 'edit' to initiate the dialog,
|
||||
|
@@ -87,6 +87,7 @@ const StyledBox = styled(Box)(({theme}) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
class PreferencesSchema extends BaseUISchema {
|
||||
constructor(initValues = {}, schemaFields = []) {
|
||||
super({
|
||||
@@ -100,8 +101,8 @@ class PreferencesSchema extends BaseUISchema {
|
||||
return 'id';
|
||||
}
|
||||
|
||||
setSelectedCategory(category) {
|
||||
this.category = category;
|
||||
categoryUpdated() {
|
||||
this.state?.validate(this.sessData);
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
@@ -110,7 +111,8 @@ class PreferencesSchema extends BaseUISchema {
|
||||
}
|
||||
|
||||
|
||||
function RightPanel({ schema, ...props }) {
|
||||
function RightPanel({ schema, refreshKey, ...props }) {
|
||||
const schemaViewRef = React.useRef(null);
|
||||
let initData = () => new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(props.initValues);
|
||||
@@ -118,20 +120,31 @@ function RightPanel({ schema, ...props }) {
|
||||
reject(error instanceof Error ? error : Error(gettext('Something went wrong')));
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
const timeID = setTimeout(() => {
|
||||
const focusableElement = schemaViewRef.current?.querySelector(
|
||||
'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
if (focusableElement) focusableElement.focus();
|
||||
}, 50);
|
||||
return () => clearTimeout(timeID);
|
||||
}, [refreshKey]);
|
||||
|
||||
return (
|
||||
<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={initData}
|
||||
viewHelperProps={{ mode: 'edit' }}
|
||||
schema={schema}
|
||||
showFooter={false}
|
||||
isTabView={false}
|
||||
formClassName='PreferencesComponent-preferencesContainerBackground'
|
||||
onDataChange={(isChanged, changedData) => {
|
||||
props.onDataChange(changedData);
|
||||
}}
|
||||
/>
|
||||
<div ref={schemaViewRef}>
|
||||
<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={initData}
|
||||
viewHelperProps={{ mode: 'edit' }}
|
||||
schema={schema}
|
||||
showFooter={false}
|
||||
isTabView={false}
|
||||
formClassName='PreferencesComponent-preferencesContainerBackground'
|
||||
onDataChange={(isChanged, changedData) => {
|
||||
props.onDataChange(changedData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,6 +157,7 @@ RightPanel.propTypes = {
|
||||
|
||||
export default function PreferencesComponent({ ...props }) {
|
||||
|
||||
const [refreshKey, setRefreshKey] = React.useState(0);
|
||||
const [disableSave, setDisableSave] = React.useState(true);
|
||||
const prefSchema = React.useRef(new PreferencesSchema({}, []));
|
||||
const prefChangedData = React.useRef({});
|
||||
@@ -214,12 +228,17 @@ export default function PreferencesComponent({ ...props }) {
|
||||
setPrefTreeData(preferencesTreeData);
|
||||
setInitValues(preferencesValues);
|
||||
// set Preferences schema
|
||||
prefSchema.current = new PreferencesSchema(preferencesValues, preferencesData);
|
||||
prefSchema.current = new PreferencesSchema(
|
||||
preferencesValues, preferencesData,
|
||||
);
|
||||
}).catch((err) => {
|
||||
pgAdmin.Browser.notifier.alert(err);
|
||||
});
|
||||
}, []);
|
||||
function setPreferences(node, subNode, nodeData, preferencesValues, preferencesData) {
|
||||
|
||||
function setPreferences(
|
||||
node, subNode, nodeData, preferencesValues, preferencesData
|
||||
) {
|
||||
let addBinaryPathNote = false;
|
||||
subNode.preferences.forEach((element) => {
|
||||
let note = '';
|
||||
@@ -335,9 +354,10 @@ export default function PreferencesComponent({ ...props }) {
|
||||
preferencesData.push(
|
||||
{
|
||||
id: _.uniqueId('note') + subNode.id,
|
||||
type: 'note', text: note,
|
||||
type: 'note',
|
||||
text: note,
|
||||
'parentId': nodeData['id'],
|
||||
visible: false,
|
||||
'parentId': nodeData['id']
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -351,28 +371,25 @@ export default function PreferencesComponent({ ...props }) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let initTreeTimeout = null;
|
||||
let firstElement = null;
|
||||
// Listen selected preferences tree node event and show the appropriate components in right panel.
|
||||
pgAdmin.Browser.Events.on('preferences:tree:selected', (event, item) => {
|
||||
if (item.type == FileType.File) {
|
||||
prefSchema.current.setSelectedCategory(item._metadata.data.name);
|
||||
prefSchema.current.schemaFields.forEach((field) => {
|
||||
field.visible = field.parentId === item._metadata.data.id && !field?.hidden ;
|
||||
field.visible = field.parentId === item._metadata.data.id &&
|
||||
!field?.hidden ;
|
||||
|
||||
if(field.visible && _.isNull(firstElement)) {
|
||||
firstElement = field;
|
||||
}
|
||||
field.labelTooltip = item._parent._metadata.data.name.toLowerCase() + ':' + item._metadata.data.name + ':' + field.name;
|
||||
|
||||
field.labelTooltip =
|
||||
item._parent._metadata.data.name.toLowerCase() + ':' +
|
||||
item._metadata.data.name + ':' + field.name;
|
||||
});
|
||||
setLoadTree(crypto.getRandomValues(new Uint16Array(1)));
|
||||
initTreeTimeout = setTimeout(() => {
|
||||
prefTreeInit.current = true;
|
||||
if(firstElement) {
|
||||
//set focus on first element on right side panel.
|
||||
document.getElementsByName(firstElement.id.toString())[0].focus();
|
||||
firstElement = '';
|
||||
}
|
||||
}, 10);
|
||||
prefSchema.current.categoryUpdated(item._metadata.data.id);
|
||||
setLoadTree(Date.now());
|
||||
setRefreshKey(Date.now());
|
||||
}
|
||||
else {
|
||||
selectChildNode(item, prefTreeInit);
|
||||
@@ -386,10 +403,6 @@ export default function PreferencesComponent({ ...props }) {
|
||||
|
||||
// Listen added preferences tree node event to expand the newly added node on tree load.
|
||||
pgAdmin.Browser.Events.on('preferences:tree:added', addPrefTreeNode);
|
||||
/* Clear the initTreeTimeout timeout if unmounted */
|
||||
return () => {
|
||||
clearTimeout(initTreeTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function addPrefTreeNode(event, item) {
|
||||
@@ -655,32 +668,54 @@ export default function PreferencesComponent({ ...props }) {
|
||||
<Box className='PreferencesComponent-treeContainer' >
|
||||
<Box className='PreferencesComponent-tree' id={'treeContainer'} tabIndex={0}>
|
||||
{
|
||||
useMemo(() => (prefTreeData && props.renderTree(prefTreeData)), [prefTreeData])
|
||||
useMemo(
|
||||
() => (prefTreeData && props.renderTree(prefTreeData)),
|
||||
[prefTreeData]
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className='PreferencesComponent-preferencesContainer'>
|
||||
{
|
||||
prefSchema.current && loadTree > 0 &&
|
||||
<RightPanel schema={prefSchema.current} initValues={initValues} onDataChange={(changedData) => {
|
||||
Object.keys(changedData).length > 0 ? setDisableSave(false) : setDisableSave(true);
|
||||
prefChangedData.current = changedData;
|
||||
}}></RightPanel>
|
||||
<RightPanel
|
||||
schema={prefSchema.current} initValues={initValues}
|
||||
refreshKey={refreshKey}
|
||||
onDataChange={(changedData) => {
|
||||
Object.keys(changedData).length > 0 ?
|
||||
setDisableSave(false) : setDisableSave(true);
|
||||
prefChangedData.current = changedData;
|
||||
}}
|
||||
></RightPanel>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className='PreferencesComponent-footer'>
|
||||
<Box>
|
||||
<PgIconButton data-test="dialog-help" onClick={onDialogHelp} icon={<HelpIcon />} title={gettext('Help for this dialog.')} />
|
||||
<PgIconButton
|
||||
data-test="dialog-help" onClick={onDialogHelp}
|
||||
icon={<HelpIcon />} title={gettext('Help for this dialog.')}
|
||||
/>
|
||||
</Box>
|
||||
<Box className='PreferencesComponent-actionBtn' marginLeft="auto">
|
||||
<DefaultButton className='PreferencesComponent-buttonMargin' onClick={reset} startIcon={<SettingsBackupRestoreIcon />}>
|
||||
<DefaultButton className='PreferencesComponent-buttonMargin'
|
||||
onClick={reset} startIcon={<SettingsBackupRestoreIcon />}>
|
||||
{gettext('Reset all preferences')}
|
||||
</DefaultButton>
|
||||
<DefaultButton className='PreferencesComponent-buttonMargin' onClick={() => { props.closeModal();}} startIcon={<CloseSharpIcon onClick={() => { props.closeModal();}} />}>
|
||||
<DefaultButton className='PreferencesComponent-buttonMargin'
|
||||
onClick={() => { props.closeModal();}}
|
||||
startIcon={
|
||||
<CloseSharpIcon onClick={() => { props.closeModal();}} />
|
||||
}>
|
||||
{gettext('Cancel')}
|
||||
</DefaultButton>
|
||||
<PrimaryButton className='PreferencesComponent-buttonMargin' startIcon={<SaveSharpIcon />} disabled={disableSave} onClick={() => { savePreferences(prefChangedData, initValues); }}>
|
||||
<PrimaryButton
|
||||
className='PreferencesComponent-buttonMargin'
|
||||
startIcon={<SaveSharpIcon />}
|
||||
disabled={disableSave}
|
||||
onClick={() => {
|
||||
savePreferences(prefChangedData, initValues);
|
||||
}}>
|
||||
{gettext('Save')}
|
||||
</PrimaryButton>
|
||||
</Box>
|
||||
|
@@ -1,625 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The DataGridView component is based on react-table component */
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/AddOutlined';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
getExpandedRowModel,
|
||||
flexRender,
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend';
|
||||
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import { PgIconButton } from 'sources/components/Buttons';
|
||||
import {
|
||||
PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader,
|
||||
PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent,
|
||||
getDeleteCell, getEditCell, getReorderCell
|
||||
} from 'sources/components/PgReactTableStyled';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
import { InputText } from 'sources/components/FormComponents';
|
||||
import gettext from 'sources/gettext';
|
||||
import { evalFunc, requestAnimationAndFocus } from 'sources/utils';
|
||||
|
||||
import FormView from './FormView';
|
||||
import { MappedCellControl } from './MappedControl';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData,
|
||||
isModeSupportedByField
|
||||
} from './common';
|
||||
import { StyleDataGridBox } from './StyledComponents';
|
||||
|
||||
|
||||
function DataTableRow({
|
||||
index, row, totalRows, isResizing, isHovered, schema, schemaRef, accessPath,
|
||||
moveRow, setHoverIndex, viewHelperProps
|
||||
}) {
|
||||
|
||||
const [key, setKey] = useState(false);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const rowRef = useRef(null);
|
||||
const dragHandleRef = useRef(null);
|
||||
|
||||
/* Memoize the row to avoid unnecessary re-render.
|
||||
* If table data changes, then react-table re-renders the complete tables
|
||||
* We can avoid re-render by if row data is not changed
|
||||
*/
|
||||
let depsMap = [JSON.stringify(row.original)];
|
||||
const externalDeps = useMemo(()=>{
|
||||
let retVal = [];
|
||||
/* Calculate the fields which depends on the current field
|
||||
deps has info on fields which the current field depends on. */
|
||||
schema.fields.forEach((field)=>{
|
||||
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||
let source = accessPath.concat(dep);
|
||||
if(_.isArray(dep)) {
|
||||
source = dep;
|
||||
/* If its an array, then dep is from the top schema and external */
|
||||
retVal.push(source);
|
||||
}
|
||||
});
|
||||
});
|
||||
return retVal;
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
schemaRef.current.fields.forEach((field)=>{
|
||||
/* Self change is also dep change */
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||
let source = accessPath.concat(dep);
|
||||
if(_.isArray(dep)) {
|
||||
source = dep;
|
||||
}
|
||||
if(field.depChange) {
|
||||
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return ()=>{
|
||||
/* Cleanup the listeners when unmounting */
|
||||
schemaState?.removeDepListener(accessPath);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [{ handlerId }, drop] = useDrop({
|
||||
accept: 'row',
|
||||
collect(monitor) {
|
||||
return {
|
||||
handlerId: monitor.getHandlerId(),
|
||||
};
|
||||
},
|
||||
hover(item, monitor) {
|
||||
if (!rowRef.current) {
|
||||
return;
|
||||
}
|
||||
item.hoverIndex = null;
|
||||
// Don't replace items with themselves
|
||||
if (item.index === index) {
|
||||
return;
|
||||
}
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = rowRef.current?.getBoundingClientRect();
|
||||
// Determine mouse position
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
// Get pixels to the top
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
// Only perform the move when the mouse has crossed certain part of the items height
|
||||
// Dragging downwards
|
||||
if (item.index < index && hoverClientY < (hoverBoundingRect.bottom - hoverBoundingRect.top)/3) {
|
||||
return;
|
||||
}
|
||||
// Dragging upwards
|
||||
if (item.index > index && hoverClientY > ((hoverBoundingRect.bottom - hoverBoundingRect.top)*2/3)) {
|
||||
return;
|
||||
}
|
||||
setHoverIndex(index);
|
||||
item.hoverIndex = index;
|
||||
},
|
||||
});
|
||||
|
||||
const [, drag] = useDrag({
|
||||
type: 'row',
|
||||
item: () => {
|
||||
return {index};
|
||||
},
|
||||
end: (item)=>{
|
||||
// Time to actually perform the action
|
||||
setHoverIndex(null);
|
||||
if(item.hoverIndex >= 0) {
|
||||
moveRow(item.index, item.hoverIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* External deps values are from top schema sess data */
|
||||
depsMap = depsMap.concat(externalDeps.map((source)=>_.get(schemaRef.current.top?.sessData, source)));
|
||||
depsMap = depsMap.concat([totalRows, row.getIsExpanded(), key, isResizing, isHovered]);
|
||||
|
||||
drag(dragHandleRef);
|
||||
drop(rowRef);
|
||||
|
||||
return useMemo(()=>
|
||||
<PgReactTableRowContent ref={rowRef} data-handler-id={handlerId}
|
||||
className={isHovered ? 'DataGridView-tableRowHovered' : null}
|
||||
data-test='data-table-row' style={{position: 'initial'}}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
// Let's not render the cell, which are not supported in this mode.
|
||||
if (cell.column.field && !isModeSupportedByField(
|
||||
cell.column.field, viewHelperProps
|
||||
)) return;
|
||||
|
||||
const content = flexRender(cell.column.columnDef.cell, {
|
||||
key: cell.column.columnDef.cell?.type ?? cell.column.columnDef.id,
|
||||
...cell.getContext(),
|
||||
reRenderRow: ()=>{setKey((currKey)=>!currKey);}
|
||||
});
|
||||
|
||||
return (
|
||||
<PgReactTableCell cell={cell} row={row} key={cell.id}
|
||||
ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null}>
|
||||
{content}
|
||||
</PgReactTableCell>
|
||||
);
|
||||
})}
|
||||
<div className='hover-overlay'></div>
|
||||
</PgReactTableRowContent>, depsMap);
|
||||
}
|
||||
|
||||
export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTextChange}) {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
return (
|
||||
<Box className='DataGridView-gridHeader'>
|
||||
{ label &&
|
||||
<Box className='DataGridView-gridHeaderText'>{label}</Box>
|
||||
}
|
||||
{ canSearch &&
|
||||
<Box className='DataGridView-gridHeaderText' width={'100%'}>
|
||||
<InputText value={searchText}
|
||||
onChange={(value)=>{
|
||||
onSearchTextChange(value);
|
||||
setSearchText(value);
|
||||
}}
|
||||
placeholder={gettext('Search')}>
|
||||
</InputText>
|
||||
</Box>
|
||||
}
|
||||
<Box className='DataGridView-gridControls'>
|
||||
{canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={()=>{
|
||||
setSearchText('');
|
||||
onSearchTextChange('');
|
||||
onAddClick();
|
||||
}} icon={<AddIcon />} className='DataGridView-gridControlsButton' />}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
DataGridHeader.propTypes = {
|
||||
label: PropTypes.string,
|
||||
canAdd: PropTypes.bool,
|
||||
onAddClick: PropTypes.func,
|
||||
canSearch: PropTypes.bool,
|
||||
onSearchTextChange: PropTypes.func,
|
||||
};
|
||||
|
||||
function getMappedCell({
|
||||
field,
|
||||
schemaRef,
|
||||
viewHelperProps,
|
||||
accessPath,
|
||||
dataDispatch
|
||||
}) {
|
||||
const Cell = ({row, ...other}) => {
|
||||
const value = other.getValue();
|
||||
/* Make sure to take the latest field info from schema */
|
||||
field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field;
|
||||
|
||||
let {editable, disabled, modeSupported} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps);
|
||||
|
||||
if(_.isUndefined(field.cell)) {
|
||||
console.error('cell is required ', field);
|
||||
}
|
||||
|
||||
return modeSupported && <MappedCellControl rowIndex={row.index} value={value}
|
||||
row={row} {...field}
|
||||
readonly={!editable}
|
||||
disabled={disabled}
|
||||
visible={true}
|
||||
onCellChange={(changeValue)=>{
|
||||
if(field.radioType) {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.BULK_UPDATE,
|
||||
path: accessPath,
|
||||
value: changeValue,
|
||||
id: field.id
|
||||
});
|
||||
}
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat([row.index, field.id]),
|
||||
value: changeValue,
|
||||
});
|
||||
}}
|
||||
reRenderRow={other.reRenderRow}
|
||||
/>;
|
||||
};
|
||||
|
||||
Cell.displayName = 'Cell';
|
||||
Cell.propTypes = {
|
||||
row: PropTypes.object.isRequired,
|
||||
value: PropTypes.any,
|
||||
onCellChange: PropTypes.func,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export default function DataGridView({
|
||||
value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName,
|
||||
fixedRows, ...props
|
||||
}) {
|
||||
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const checkIsMounted = useIsMounted();
|
||||
const [hoverIndex, setHoverIndex] = useState();
|
||||
const newRowIndex = useRef();
|
||||
const pgAdmin = usePgAdmin();
|
||||
const [searchVal, setSearchVal] = useState('');
|
||||
|
||||
/* Using ref so that schema variable is not frozen in columns closure */
|
||||
const schemaRef = useRef(schema);
|
||||
const columns = useMemo(
|
||||
()=>{
|
||||
let cols = [];
|
||||
if(props.canReorder) {
|
||||
let colInfo = {
|
||||
header: <> </>,
|
||||
id: 'btn-reorder',
|
||||
accessorFn: ()=>{/*This is intentional (SonarQube)*/},
|
||||
enableResizing: false,
|
||||
enableSorting: false,
|
||||
dataType: 'reorder',
|
||||
size: 36,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
cell: getReorderCell(),
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
if(props.canEdit) {
|
||||
let colInfo = {
|
||||
header: <> </>,
|
||||
id: 'btn-edit',
|
||||
accessorFn: ()=>{/*This is intentional (SonarQube)*/},
|
||||
enableResizing: false,
|
||||
enableSorting: false,
|
||||
dataType: 'edit',
|
||||
size: 26,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
cell: getEditCell({
|
||||
isDisabled: (row)=>{
|
||||
let canEditRow = true;
|
||||
if(props.canEditRow) {
|
||||
canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {});
|
||||
}
|
||||
return !canEditRow;
|
||||
},
|
||||
title: gettext('Edit row'),
|
||||
})
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
if(props.canDelete) {
|
||||
let colInfo = {
|
||||
header: <> </>,
|
||||
id: 'btn-delete',
|
||||
accessorFn: ()=>{/*This is intentional (SonarQube)*/},
|
||||
enableResizing: false,
|
||||
enableSorting: false,
|
||||
dataType: 'delete',
|
||||
size: 26,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
cell: getDeleteCell({
|
||||
title: gettext('Delete row'),
|
||||
isDisabled: (row)=>{
|
||||
let canDeleteRow = true;
|
||||
if(props.canDeleteRow) {
|
||||
canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});
|
||||
}
|
||||
return !canDeleteRow;
|
||||
},
|
||||
onClick: (row)=>{
|
||||
const deleteRow = ()=> {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
||||
path: accessPath,
|
||||
value: row.index,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
if (props.onDelete){
|
||||
props.onDelete(row.original || {}, deleteRow);
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
props.customDeleteTitle || gettext('Delete Row'),
|
||||
props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),
|
||||
deleteRow,
|
||||
function() {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
cols.push(colInfo);
|
||||
}
|
||||
|
||||
cols = cols.concat(
|
||||
schemaRef.current.fields.filter((f) => (
|
||||
_.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true
|
||||
)).sort((firstF, secondF) => (
|
||||
_.isArray(props.columns) ? ((
|
||||
props.columns.indexOf(firstF.id) <
|
||||
props.columns.indexOf(secondF.id)
|
||||
) ? -1 : 1) : 0
|
||||
)).map((field) => {
|
||||
let widthParms = {};
|
||||
if(field.width) {
|
||||
widthParms.size = field.width;
|
||||
widthParms.minSize = field.width;
|
||||
} else {
|
||||
widthParms.size = 75;
|
||||
widthParms.minSize = 75;
|
||||
}
|
||||
if(field.minWidth) {
|
||||
widthParms.minSize = field.minWidth;
|
||||
}
|
||||
if(field.maxWidth) {
|
||||
widthParms.maxSize = field.maxWidth;
|
||||
}
|
||||
widthParms.enableResizing =
|
||||
_.isUndefined(field.enableResizing) ? true : Boolean(
|
||||
field.enableResizing
|
||||
);
|
||||
|
||||
let colInfo = {
|
||||
header: field.label||<> </>,
|
||||
accessorKey: field.id,
|
||||
field: field,
|
||||
enableResizing: true,
|
||||
enableSorting: false,
|
||||
...widthParms,
|
||||
cell: getMappedCell({
|
||||
field: field,
|
||||
schemaRef: schemaRef,
|
||||
viewHelperProps: viewHelperProps,
|
||||
accessPath: accessPath,
|
||||
dataDispatch: dataDispatch,
|
||||
}),
|
||||
};
|
||||
|
||||
return colInfo;
|
||||
})
|
||||
);
|
||||
return cols;
|
||||
},[props.canEdit, props.canDelete, props.canReorder]
|
||||
);
|
||||
|
||||
const columnVisibility = useMemo(()=>{
|
||||
const ret = {};
|
||||
|
||||
columns.forEach(column => {
|
||||
ret[column.id] = isModeSupportedByField(column.field, viewHelperProps);
|
||||
});
|
||||
|
||||
return ret;
|
||||
}, [columns, viewHelperProps]);
|
||||
|
||||
const table = useReactTable({
|
||||
columns,
|
||||
data: value,
|
||||
autoResetAll: false,
|
||||
state: {
|
||||
globalFilter: searchVal,
|
||||
columnVisibility: columnVisibility,
|
||||
},
|
||||
columnResizeMode: 'onChange',
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
});
|
||||
|
||||
const rows = table.getRowModel().rows;
|
||||
|
||||
const onAddClick = useCallback(()=>{
|
||||
if(!props.canAddRow) {
|
||||
return;
|
||||
}
|
||||
let newRow = schemaRef.current.getNewData();
|
||||
|
||||
const current_macros = schemaRef.current?._top?._sessData?.macro || null;
|
||||
if (current_macros){
|
||||
newRow = schemaRef.current.getNewData(current_macros);
|
||||
}
|
||||
|
||||
newRowIndex.current = props.addOnTop ? 0 : rows.length;
|
||||
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
|
||||
path: accessPath,
|
||||
value: newRow,
|
||||
addOnTop: props.addOnTop
|
||||
});
|
||||
}, [props.canAddRow, rows?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
let rowsPromise = fixedRows;
|
||||
|
||||
// If fixedRows is defined, fetch the details.
|
||||
if(typeof rowsPromise === 'function') {
|
||||
rowsPromise = rowsPromise();
|
||||
}
|
||||
|
||||
if(rowsPromise) {
|
||||
Promise.resolve(rowsPromise)
|
||||
.then((res) => {
|
||||
/* If component unmounted, dont update state */
|
||||
if(checkIsMounted()) {
|
||||
schemaState.setUnpreparedData(accessPath, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
if(newRowIndex.current >= 0) {
|
||||
virtualizer.scrollToIndex(newRowIndex.current);
|
||||
|
||||
// Try autofocus on newly added row.
|
||||
setTimeout(() => {
|
||||
const rowInput = tableRef.current?.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
);
|
||||
if(!rowInput) return;
|
||||
|
||||
requestAnimationAndFocus(tableRef.current.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
));
|
||||
props.expandEditOnAdd && props.canEdit &&
|
||||
rows[newRowIndex.current]?.toggleExpanded(true);
|
||||
newRowIndex.current = undefined;
|
||||
}, 50);
|
||||
}
|
||||
}, [rows?.length]);
|
||||
|
||||
const tableRef = useRef();
|
||||
|
||||
const moveRow = (dragIndex, hoverIndex) => {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.MOVE_ROW,
|
||||
path: accessPath,
|
||||
oldIndex: dragIndex,
|
||||
newIndex: hoverIndex,
|
||||
});
|
||||
};
|
||||
|
||||
const isResizing = _.flatMap(table.getHeaderGroups(), headerGroup => headerGroup.headers.map(header=>header.column.getIsResizing())).includes(true);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: () => tableRef.current,
|
||||
estimateSize: () => 42,
|
||||
measureElement:
|
||||
typeof window !== 'undefined' &&
|
||||
navigator.userAgent.indexOf('Firefox') === -1
|
||||
? element => element?.getBoundingClientRect().height
|
||||
: undefined,
|
||||
overscan: viewHelperProps.virtualiseOverscan ?? 10,
|
||||
});
|
||||
|
||||
if(!props.visible) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyleDataGridBox className={containerClassName}>
|
||||
<Box className='DataGridView-grid'>
|
||||
{(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick}
|
||||
canSearch={props.canSearch}
|
||||
onSearchTextChange={(value)=>{
|
||||
setSearchVal(value || undefined);
|
||||
}}
|
||||
/>}
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<PgReactTable ref={tableRef} table={table} data-test="data-grid-view" tableClassName='DataGridView-table'>
|
||||
<PgReactTableHeader table={table} />
|
||||
<PgReactTableBody style={{ height: virtualizer.getTotalSize() + 'px'}}>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
|
||||
return <PgReactTableRow key={row.id} data-index={virtualRow.index}
|
||||
ref={node => virtualizer.measureElement(node)}
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start}px)`, // this should always be a `style` as it changes on scroll
|
||||
}}>
|
||||
<DataTableRow index={virtualRow.index} row={row} totalRows={rows.length} isResizing={isResizing}
|
||||
schema={schemaRef.current} schemaRef={schemaRef} accessPath={accessPath.concat([row.index])}
|
||||
moveRow={moveRow} isHovered={virtualRow.index == hoverIndex} setHoverIndex={setHoverIndex} viewHelperProps={viewHelperProps}
|
||||
measureElement={virtualizer.measureElement}
|
||||
/>
|
||||
{props.canEdit &&
|
||||
<PgReactTableRowExpandContent row={row}>
|
||||
<FormView value={row.original} viewHelperProps={viewHelperProps} dataDispatch={dataDispatch}
|
||||
schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className='DataGridView-expandedForm'
|
||||
isDataGridForm={true} firstEleRef={(ele)=>{
|
||||
requestAnimationAndFocus(ele);
|
||||
}}/>
|
||||
</PgReactTableRowExpandContent>
|
||||
}
|
||||
</PgReactTableRow>;
|
||||
})}
|
||||
</PgReactTableBody>
|
||||
</PgReactTable>
|
||||
</DndProvider>
|
||||
</Box>
|
||||
</StyleDataGridBox>
|
||||
);
|
||||
}
|
||||
|
||||
DataGridView.propTypes = {
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
viewHelperProps: PropTypes.object,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func,
|
||||
containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
fixedRows: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),
|
||||
columns: PropTypes.array,
|
||||
canEdit: PropTypes.bool,
|
||||
canAdd: PropTypes.bool,
|
||||
canDelete: PropTypes.bool,
|
||||
canReorder: PropTypes.bool,
|
||||
visible: PropTypes.bool,
|
||||
canAddRow: PropTypes.oneOfType([
|
||||
PropTypes.bool, PropTypes.func,
|
||||
]),
|
||||
canEditRow: PropTypes.oneOfType([
|
||||
PropTypes.bool, PropTypes.func,
|
||||
]),
|
||||
canDeleteRow: PropTypes.oneOfType([
|
||||
PropTypes.bool, PropTypes.func,
|
||||
]),
|
||||
expandEditOnAdd: PropTypes.bool,
|
||||
customDeleteTitle: PropTypes.string,
|
||||
customDeleteMsg: PropTypes.string,
|
||||
canSearch: PropTypes.bool,
|
||||
onDelete: PropTypes.func,
|
||||
addOnTop: PropTypes.bool
|
||||
};
|
47
web/pgadmin/static/js/SchemaView/DataGridView/SearchBox.jsx
Normal file
47
web/pgadmin/static/js/SchemaView/DataGridView/SearchBox.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import {
|
||||
SEARCH_INPUT_ALIGNMENT, SEARCH_INPUT_SIZE, SearchInputText,
|
||||
} from 'sources/components/SearchInputText';
|
||||
|
||||
import { SchemaStateContext } from '../SchemaState';
|
||||
|
||||
import { DataGridContext } from './context';
|
||||
import { GRID_STATE } from './utils';
|
||||
|
||||
export const SEARCH_STATE_PATH = [GRID_STATE, '__searchText'];
|
||||
|
||||
export function SearchBox() {
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const {
|
||||
accessPath, field, options: { canSearch }
|
||||
} = useContext(DataGridContext);
|
||||
|
||||
if (!canSearch) return <></>;
|
||||
|
||||
const searchText = schemaState.state(accessPath.concat(SEARCH_STATE_PATH));
|
||||
const searchTextChange = (value) => {
|
||||
schemaState.setState(accessPath.concat(SEARCH_STATE_PATH), value);
|
||||
};
|
||||
|
||||
const searchOptions = field.searchOptions || {
|
||||
size: SEARCH_INPUT_SIZE.HALF,
|
||||
alignment: SEARCH_INPUT_ALIGNMENT.RIGHT,
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchInputText
|
||||
{...searchOptions}
|
||||
searchText={searchText || ''} onChange={searchTextChange}
|
||||
/>
|
||||
);
|
||||
}
|
14
web/pgadmin/static/js/SchemaView/DataGridView/context.js
Normal file
14
web/pgadmin/static/js/SchemaView/DataGridView/context.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const DataGridContext = createContext();
|
||||
export const DataGridRowContext = createContext();
|
||||
|
@@ -0,0 +1,21 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const ACTION_COLUMN = {
|
||||
header: <> </>,
|
||||
accessorFn: ()=>{/*This is intentional (SonarQube)*/},
|
||||
enableResizing: false,
|
||||
enableSorting: false,
|
||||
dataType: 'reorder',
|
||||
size: 36,
|
||||
maxSize: 26,
|
||||
minSize: 26,
|
||||
};
|
@@ -0,0 +1,95 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getDeleteCell } from 'sources/components/PgReactTableStyled';
|
||||
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import {
|
||||
canAddOrDelete, evalIfNotDisabled, registerOptionEvaluator
|
||||
} from '../../options';
|
||||
|
||||
import { SchemaStateContext } from '../../SchemaState';
|
||||
import { useFieldOptions } from '../../hooks';
|
||||
import { DataGridRowContext } from '../context';
|
||||
import { ACTION_COLUMN } from './common';
|
||||
import Feature from './feature';
|
||||
|
||||
|
||||
// Register the 'canDelete' options for the collection
|
||||
registerOptionEvaluator('canDelete', canAddOrDelete, false, ['collection']);
|
||||
|
||||
// Register the 'canDeleteRow' option for the table row
|
||||
registerOptionEvaluator('canDeleteRow', evalIfNotDisabled, true, ['row']);
|
||||
|
||||
|
||||
export default class DeletableRow extends Feature {
|
||||
// Always add 'edit' column at the start of the columns list
|
||||
// (but - not before the reorder column).
|
||||
static priority = 50;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.canDelete = false;
|
||||
}
|
||||
|
||||
generateColumns({pgAdmin, columns, columnVisibility, options}) {
|
||||
this.canDelete = options.canDelete;
|
||||
|
||||
if (!this.canDelete) return;
|
||||
|
||||
const instance = this;
|
||||
const field = instance.field;
|
||||
const accessPath = instance.accessPath;
|
||||
const dataDispatch = instance.dataDispatch;
|
||||
|
||||
columnVisibility['btn-delete'] = true;
|
||||
|
||||
columns.splice(0, 0, {
|
||||
...ACTION_COLUMN,
|
||||
id: 'btn-delete',
|
||||
dataType: 'delete',
|
||||
cell: getDeleteCell({
|
||||
isDisabled: () => {
|
||||
const schemaState = React.useContext(SchemaStateContext);
|
||||
const { rowAccessPath } = React.useContext(DataGridRowContext);
|
||||
const options = useFieldOptions(rowAccessPath, schemaState);
|
||||
|
||||
return !options.canDeleteRow;
|
||||
},
|
||||
title: gettext('Delete row'),
|
||||
onClick: (row) => {
|
||||
const deleteRow = () => {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DELETE_ROW,
|
||||
path: accessPath,
|
||||
value: row.index,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
if (field.onDelete){
|
||||
field.onDelete(row?.original || {}, deleteRow);
|
||||
} else {
|
||||
pgAdmin.Browser.notifier.confirm(
|
||||
field.customDeleteTitle || gettext('Delete Row'),
|
||||
field.customDeleteMsg || gettext(
|
||||
'Are you sure you wish to delete this row?'
|
||||
),
|
||||
deleteRow,
|
||||
function() { return true; }
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import { getExpandedRowModel } from '@tanstack/react-table';
|
||||
|
||||
import { getEditCell } from 'sources/components/PgReactTableStyled';
|
||||
import gettext from 'sources/gettext';
|
||||
import FormView from 'sources/SchemaView/FormView';
|
||||
|
||||
import { SchemaStateContext } from '../../SchemaState';
|
||||
import { useFieldOptions } from '../../hooks';
|
||||
import { DataGridRowContext } from '../context';
|
||||
import { ACTION_COLUMN } from './common';
|
||||
import Feature from './feature';
|
||||
|
||||
|
||||
export default class ExpandedFormView extends Feature {
|
||||
// Always add 'edit' column at the start of the columns list
|
||||
// (but - not before the reorder column).
|
||||
static priority = 70;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.canEdit = false;
|
||||
}
|
||||
|
||||
generateColumns({columns, columnVisibility, options}) {
|
||||
this.canEdit = options.canEdit;
|
||||
|
||||
if (!this.canEdit) return;
|
||||
|
||||
columnVisibility['btn-edit'] = true;
|
||||
|
||||
columns.splice(0, 0, {
|
||||
...ACTION_COLUMN,
|
||||
id: 'btn-edit',
|
||||
dataType: 'edit',
|
||||
cell: getEditCell({
|
||||
isDisabled: () => {
|
||||
const schemaState = React.useContext(SchemaStateContext);
|
||||
const { rowAccessPath } = React.useContext(DataGridRowContext);
|
||||
const options = useFieldOptions(rowAccessPath, schemaState);
|
||||
|
||||
return !options.canEditRow;
|
||||
},
|
||||
title: gettext('Edit row'),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
onTable({table}) {
|
||||
table.setOptions(prev => ({
|
||||
...prev,
|
||||
getExpandedRowModel: getExpandedRowModel(),
|
||||
state: {
|
||||
...prev.state,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onRow({row, expandedRowContents, rowOptions}) {
|
||||
const instance = this;
|
||||
|
||||
if (rowOptions.canEditRow && row?.getIsExpanded()) {
|
||||
expandedRowContents.splice(
|
||||
0, 0, <FormView
|
||||
key={`expanded-form-row-${row.id}`}
|
||||
value={row?.original}
|
||||
viewHelperProps={instance.viewHelperProps}
|
||||
dataDispatch={instance.dataDispatch}
|
||||
schema={instance.schema}
|
||||
accessPath={instance.accessPath.concat([row.index])}
|
||||
isNested={true}
|
||||
className='DataGridView-expandedForm'
|
||||
isDataGridForm={true}
|
||||
focusOnFirstInput={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,134 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// Feature class
|
||||
|
||||
// Let's not expose the features directory.
|
||||
let _featureClasses = [];
|
||||
|
||||
export default class Feature {
|
||||
static priority = 1;
|
||||
|
||||
constructor() {
|
||||
this.accessPath = this.field = this.schema = this.table = this.cols =
|
||||
this.viewHelperProps = null;
|
||||
}
|
||||
|
||||
setContext({
|
||||
accessPath, field, schema, viewHelperProps, dataDispatch, schemaState
|
||||
}) {
|
||||
this.accessPath = accessPath;
|
||||
this.field = field;
|
||||
this.schema = schema;
|
||||
this.dataDispatch = dataDispatch;
|
||||
this.viewHelperProps = viewHelperProps;
|
||||
this.schemaState = schemaState;
|
||||
}
|
||||
|
||||
generateColumns(/* { pgAdmin, columns, columnVisibility, options } */) {}
|
||||
onTable(/* { table, options, classList } */) {}
|
||||
onRow(/* {
|
||||
index, row, rowRef, classList, attributes,
|
||||
expandedRowContents, rowOptions, tableOptions
|
||||
} */) {}
|
||||
}
|
||||
|
||||
function isValidFeatureClass(cls) {
|
||||
// Check if provided class is direct decendent of the Feature class
|
||||
try {
|
||||
if (Reflect.getPrototypeOf(cls) != Feature) {
|
||||
console.error(cls, 'Not a valid Feature class:');
|
||||
console.trace();
|
||||
return false;
|
||||
}
|
||||
} catch(err) {
|
||||
console.trace();
|
||||
console.error('Error while checking type:\n', err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function addToSortedList(_list, _item, _comparator = (a, b) => (a < b)) {
|
||||
// Insert the given feature class in sorted list based on the priority.
|
||||
let idx = 0;
|
||||
|
||||
for (; idx < _list.length; idx++) {
|
||||
if (_comparator(_item, _list[idx])) {
|
||||
_list.splice(idx, 0, _item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_list.splice(idx, 0, _item);
|
||||
}
|
||||
|
||||
const featurePriorityCompare = (f1, f2) => (f1.priorty < f2.priority);
|
||||
|
||||
export function register(cls) {
|
||||
|
||||
if (!isValidFeatureClass(cls)) return;
|
||||
|
||||
addToSortedList(_featureClasses, cls, featurePriorityCompare);
|
||||
}
|
||||
|
||||
export class FeatureSet {
|
||||
constructor() {
|
||||
this.id = Date.now();
|
||||
this.features = _featureClasses.map((cls) => new cls());
|
||||
}
|
||||
|
||||
addFeatures(features) {
|
||||
features.forEach((feature) => {
|
||||
if (!(feature instanceof Feature)) {
|
||||
console.error(feature, 'is not a valid feature!\n');
|
||||
console.trace();
|
||||
return;
|
||||
}
|
||||
addToSortedList(
|
||||
this.features, feature, featurePriorityCompare
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setContext({
|
||||
accessPath, field, schema, viewHelperProps, dataDispatch, schemaState
|
||||
}) {
|
||||
this.features.forEach((feature) => {
|
||||
feature.setContext({
|
||||
accessPath, field, schema, viewHelperProps, dataDispatch, schemaState
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
generateColumns({pgAdmin, columns, columnVisibility, options}) {
|
||||
this.features.forEach((feature) => {
|
||||
feature.generateColumns({pgAdmin, columns, columnVisibility, options});
|
||||
});
|
||||
}
|
||||
|
||||
onTable({table, options, classList}) {
|
||||
this.features.forEach((feature) => {
|
||||
feature.onTable({table, options, classList});
|
||||
});
|
||||
}
|
||||
|
||||
onRow({
|
||||
index, row, rowRef, classList, attributes, expandedRowContents,
|
||||
rowOptions, tableOptions
|
||||
}) {
|
||||
this.features.forEach((feature) => {
|
||||
feature.onRow({
|
||||
index, row, rowRef, classList, attributes, expandedRowContents,
|
||||
rowOptions, tableOptions
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
import { SchemaStateContext } from 'sources/SchemaView/SchemaState';
|
||||
import Feature from './feature';
|
||||
|
||||
|
||||
export default class FixedRows extends Feature {
|
||||
|
||||
onTable() {
|
||||
const instance = this;
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const checkIsMounted = useIsMounted();
|
||||
|
||||
useEffect(() => {
|
||||
let rowsPromise = instance.field.fixedRows;
|
||||
|
||||
// Fixed rows is supported only in 'create' mode.
|
||||
if (instance.viewHelperProps.mode !== 'create') return;
|
||||
|
||||
// If fixedRows is defined, fetch the details.
|
||||
if(typeof rowsPromise === 'function') {
|
||||
rowsPromise = rowsPromise();
|
||||
}
|
||||
|
||||
if(rowsPromise) {
|
||||
Promise.resolve(rowsPromise)
|
||||
.then((res) => {
|
||||
// If component is unmounted, don't update state.
|
||||
if(checkIsMounted()) {
|
||||
schemaState.setUnpreparedData(instance.accessPath, res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
// The DataGridView component is feature support better extendability.
|
||||
|
||||
import { Feature, FeatureSet, register } from './feature';
|
||||
import FixedRows from './fixedRows';
|
||||
import Reorder from './reorder';
|
||||
import ExpandedFormView from './expandabledFormView';
|
||||
import DeletableRow from './deletable';
|
||||
import GlobalSearch from './search';
|
||||
|
||||
register(FixedRows);
|
||||
register(DeletableRow);
|
||||
register(ExpandedFormView);
|
||||
register(GlobalSearch);
|
||||
register(Reorder);
|
||||
|
||||
export {
|
||||
Feature,
|
||||
FeatureSet,
|
||||
register
|
||||
};
|
@@ -0,0 +1,160 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||
|
||||
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState';
|
||||
|
||||
import { booleanEvaluator, registerOptionEvaluator } from '../../options';
|
||||
|
||||
import { ACTION_COLUMN } from './common';
|
||||
import Feature from './feature';
|
||||
|
||||
|
||||
// Register the 'canReorder' options for the collection
|
||||
registerOptionEvaluator('canReorder', booleanEvaluator, false, ['collection']);
|
||||
|
||||
export default class Reorder extends Feature {
|
||||
// Always add reorder column at the start of the columns list.
|
||||
static priority = 100;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.canReorder = false;
|
||||
this.hoverIndex = null;
|
||||
this.moveRow = null;
|
||||
}
|
||||
|
||||
setHoverIndex(index) {
|
||||
this.hoverIndex = index;
|
||||
}
|
||||
|
||||
generateColumns({columns, columnVisibility, options}) {
|
||||
this.canReorder = options.canReorder;
|
||||
|
||||
if (!this.canReorder) return;
|
||||
|
||||
columnVisibility['reorder-cell'] = true;
|
||||
|
||||
const Cell = function({row}) {
|
||||
const dragHandleRef = row?.reorderDragHandleRef;
|
||||
const handlerId = row?.dragHandlerId;
|
||||
|
||||
if (!dragHandleRef) return <></>;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='reorder-cell'
|
||||
data-handler-id={handlerId}
|
||||
ref={dragHandleRef ? dragHandleRef : null}>
|
||||
<DragIndicatorRoundedIcon fontSize="small" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'ReorderCell';
|
||||
|
||||
columns.splice(0, 0, {
|
||||
...ACTION_COLUMN,
|
||||
id: 'btn-reorder',
|
||||
dataType: 'reorder',
|
||||
cell: Cell,
|
||||
});
|
||||
}
|
||||
|
||||
onTable() {
|
||||
if (this.canReorder) {
|
||||
this.moveRow = (dragIndex, hoverIndex) => {
|
||||
this.dataDispatch?.({
|
||||
type: SCHEMA_STATE_ACTIONS.MOVE_ROW,
|
||||
path: this.accessPath,
|
||||
oldIndex: dragIndex,
|
||||
newIndex: hoverIndex,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onRow({index, row, rowRef, classList}) {
|
||||
const instance = this;
|
||||
const reorderDragHandleRef = useRef(null);
|
||||
|
||||
const [{ handlerId }, drop] = useDrop({
|
||||
accept: 'row',
|
||||
collect(monitor) {
|
||||
return {
|
||||
handlerId: monitor.getHandlerId(),
|
||||
};
|
||||
},
|
||||
hover(item, monitor) {
|
||||
if (!rowRef.current) return;
|
||||
|
||||
item.hoverIndex = null;
|
||||
// Don't replace items with themselves
|
||||
if (item.index === index) return;
|
||||
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundry = rowRef.current?.getBoundingClientRect();
|
||||
|
||||
// Determine mouse position
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
|
||||
// Get pixels to the top
|
||||
const hoverClientY = clientOffset.y - hoverBoundry.top;
|
||||
|
||||
// Only perform the move when the mouse has crossed certain part of the
|
||||
// items height dragging downwards.
|
||||
if (
|
||||
item.index < index &&
|
||||
hoverClientY < (hoverBoundry.bottom - hoverBoundry.top)/3
|
||||
) return;
|
||||
|
||||
// Dragging upwards
|
||||
if (
|
||||
item.index > index &&
|
||||
hoverClientY > ((hoverBoundry.bottom - hoverBoundry.top) * 2 / 3)
|
||||
) return;
|
||||
|
||||
instance.setHoverIndex(index);
|
||||
item.hoverIndex = index;
|
||||
},
|
||||
});
|
||||
|
||||
const [, drag, preview] = useDrag({
|
||||
type: 'row',
|
||||
item: () => {
|
||||
return {index};
|
||||
},
|
||||
end: (item) => {
|
||||
// Time to actually perform the action
|
||||
instance.setHoverIndex(null);
|
||||
if(item.hoverIndex >= 0) {
|
||||
instance.moveRow(item.index, item.hoverIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.canReorder || !row) return;
|
||||
|
||||
if (row)
|
||||
row.reorderDragHandleRef = reorderDragHandleRef;
|
||||
|
||||
drag(row.reorderDragHandleRef);
|
||||
drop(rowRef);
|
||||
preview(rowRef);
|
||||
|
||||
if (index == this.hoverIndex) {
|
||||
classList?.append('DataGridView-tableRowHovered');
|
||||
}
|
||||
|
||||
row.dragHandlerId = handlerId;
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import {
|
||||
booleanEvaluator, registerOptionEvaluator
|
||||
} from '../../options';
|
||||
|
||||
import Feature from './feature';
|
||||
import { SEARCH_STATE_PATH } from '../SearchBox';
|
||||
|
||||
|
||||
registerOptionEvaluator('canSearch', booleanEvaluator, false, ['collection']);
|
||||
|
||||
|
||||
export default class GlobalSearch extends Feature {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onTable({table, options}) {
|
||||
|
||||
if (!options.canSearch) {
|
||||
const searchText = '';
|
||||
|
||||
table.setOptions((prev) => ({
|
||||
...prev,
|
||||
state: {
|
||||
...prev.state,
|
||||
globalFilter: searchText,
|
||||
}
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const searchText = this.schemaState.state(
|
||||
this.accessPath.concat(SEARCH_STATE_PATH)
|
||||
);
|
||||
|
||||
table.setOptions((prev) => ({
|
||||
...prev,
|
||||
state: {
|
||||
...prev.state,
|
||||
globalFilter: searchText,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
166
web/pgadmin/static/js/SchemaView/DataGridView/formHeader.jsx
Normal file
166
web/pgadmin/static/js/SchemaView/DataGridView/formHeader.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useRef, useState
|
||||
} from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState';
|
||||
import { DefaultButton } from 'sources/components/Buttons';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import gettext from 'sources/gettext';
|
||||
import { requestAnimationAndFocus } from 'sources/utils';
|
||||
|
||||
import { SchemaStateContext } from '../SchemaState';
|
||||
import { booleanEvaluator, registerOptionEvaluator } from '../options';
|
||||
import { View } from '../registry';
|
||||
|
||||
import { SearchBox, SEARCH_STATE_PATH } from './SearchBox';
|
||||
import { DataGridContext } from './context';
|
||||
|
||||
|
||||
// Register the 'headerFormVisible' options for the collection
|
||||
registerOptionEvaluator(
|
||||
'headerFormVisible', booleanEvaluator, false, ['collection']
|
||||
);
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
'& .DataGridFormHeader-border': {
|
||||
...theme.mixins.panelBorder,
|
||||
borderBottom: 0,
|
||||
'& .DataGridFormHeader-body': {
|
||||
padding: '0',
|
||||
backgroundColor: theme.palette.grey[400],
|
||||
'& .FormView-singleCollectionPanel': {
|
||||
paddingBottom: 0,
|
||||
},
|
||||
'& .DataGridFormHeader-btn-group' :{
|
||||
display: 'flex',
|
||||
padding: theme.spacing(1),
|
||||
paddingTop: 0,
|
||||
'& .DataGridFormHeader-addBtn': {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
},
|
||||
'& [data-test="tabpanel"]': {
|
||||
overflow: 'unset',
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export function DataGridFormHeader({tableEleRef}) {
|
||||
|
||||
const {
|
||||
accessPath, field, dataDispatch, options, virtualizer, table,
|
||||
viewHelperProps,
|
||||
} = useContext(DataGridContext);
|
||||
const {
|
||||
addOnTop, canAddRow, canEdit, expandEditOnAdd, headerFormVisible
|
||||
} = options;
|
||||
const rows = table.getRowModel().rows;
|
||||
|
||||
const label = field.label || '';
|
||||
const newRowIndex = useRef(-1);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const headerFormData = useRef({});
|
||||
const [addDisabled, setAddDisabled] = useState(canAddRow);
|
||||
const {headerSchema} = field;
|
||||
|
||||
const onAddClick = useCallback(() => {
|
||||
|
||||
if(!canAddRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newRow = headerSchema.getNewData(headerFormData.current);
|
||||
|
||||
newRowIndex.current = addOnTop ? 0 : rows.length;
|
||||
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
|
||||
path: accessPath,
|
||||
value: newRow,
|
||||
addOnTop: addOnTop
|
||||
});
|
||||
|
||||
schemaState.setState(accessPath.concat(SEARCH_STATE_PATH), '');
|
||||
headerSchema.state?.validate(headerSchema._defaults || {});
|
||||
}, [canAddRow, rows?.length, addOnTop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newRowIndex.current < -1) return;
|
||||
|
||||
virtualizer.scrollToIndex(newRowIndex.current);
|
||||
|
||||
// Try autofocus on newly added row.
|
||||
setTimeout(() => {
|
||||
const rowInput = tableEleRef.current?.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
);
|
||||
|
||||
if(!rowInput) return;
|
||||
|
||||
requestAnimationAndFocus(tableEleRef.current.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
));
|
||||
|
||||
expandEditOnAdd && canEdit &&
|
||||
rows[newRowIndex.current]?.toggleExpanded(true);
|
||||
|
||||
newRowIndex.current = undefined;
|
||||
}, 50);
|
||||
}, [rows?.length]);
|
||||
|
||||
const SchemaView = View('SchemaView');
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
<Box className='DataGridFormHeader-border'>
|
||||
<Box className='DataGridView-gridHeader'>
|
||||
{label && <Box className='DataGridView-gridHeaderText'>{label}</Box>}
|
||||
<Box className='DataGridView-gridHeaderText' style={{flex: 1}}>
|
||||
<SearchBox/>
|
||||
</Box>
|
||||
</Box>
|
||||
{headerFormVisible &&
|
||||
<Box className='DataGridFormHeader-body'>
|
||||
<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={()=>Promise.resolve({})}
|
||||
schema={headerSchema}
|
||||
viewHelperProps={viewHelperProps}
|
||||
showFooter={false}
|
||||
onDataChange={(isDataChanged, dataChanged)=>{
|
||||
headerFormData.current = dataChanged;
|
||||
setAddDisabled(
|
||||
canAddRow && headerSchema.addDisabled(headerFormData.current)
|
||||
);
|
||||
}}
|
||||
hasSQL={false}
|
||||
isTabView={false}
|
||||
/>
|
||||
<Box className='DataGridFormHeader-btn-group'>
|
||||
<DefaultButton
|
||||
className='DataGridFormHeader-addBtn'
|
||||
onClick={onAddClick} disabled={addDisabled}>
|
||||
{gettext('Add')}
|
||||
</DefaultButton>
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
DataGridFormHeader.propTypes = {
|
||||
tableEleRef: CustomPropTypes.ref,
|
||||
};
|
198
web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
Normal file
198
web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, {
|
||||
useContext, useEffect, useMemo, useRef, useState,
|
||||
} from 'react';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend';
|
||||
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import {
|
||||
PgReactTable, PgReactTableBody, PgReactTableHeader,
|
||||
PgReactTableRow,
|
||||
} from 'sources/components/PgReactTableStyled';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
|
||||
import { StyleDataGridBox } from '../StyledComponents';
|
||||
import { SchemaStateContext } from '../SchemaState';
|
||||
import { useFieldOptions, useFieldValue } from '../hooks';
|
||||
import { registerView } from '../registry';
|
||||
import { listenDepChanges } from '../utils';
|
||||
|
||||
import { DataGridContext } from './context';
|
||||
import { DataGridHeader } from './header';
|
||||
import { DataGridRow } from './row';
|
||||
import { FeatureSet } from './features';
|
||||
import { createGridColumns, GRID_STATE } from './utils';
|
||||
|
||||
|
||||
export default function DataGridView({
|
||||
field, viewHelperProps, accessPath, dataDispatch, containerClassName
|
||||
}) {
|
||||
const pgAdmin = usePgAdmin();
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const options = useFieldOptions(
|
||||
accessPath, schemaState, refreshKey, setRefreshKey
|
||||
);
|
||||
const value = useFieldValue(accessPath, schemaState);
|
||||
const schema = field.schema;
|
||||
const features = useRef();
|
||||
|
||||
// Update refresh key on changing the number of rows.
|
||||
useFieldValue(
|
||||
[...accessPath, 'length'], schemaState, refreshKey,
|
||||
(newKey) => {
|
||||
setRefreshKey(newKey);
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return schemaState.subscribe(
|
||||
accessPath.concat(GRID_STATE),
|
||||
() => setRefreshKey(Date.now()), 'states'
|
||||
);
|
||||
}, [refreshKey]);
|
||||
|
||||
listenDepChanges(accessPath, field, options.visible, schemaState);
|
||||
|
||||
if (!features.current) {
|
||||
features.current = new FeatureSet();
|
||||
};
|
||||
|
||||
features.current.setContext({
|
||||
accessPath, field, schema: schema, dataDispatch, viewHelperProps,
|
||||
schemaState,
|
||||
});
|
||||
|
||||
const [columns, columnVisibility] = useMemo(() => {
|
||||
|
||||
const [columns, columnVisibility] = createGridColumns({
|
||||
schema, field, accessPath, viewHelperProps, dataDispatch,
|
||||
});
|
||||
|
||||
features.current?.generateColumns({
|
||||
pgAdmin, columns, columnVisibility, options
|
||||
});
|
||||
|
||||
return [columns, columnVisibility];
|
||||
|
||||
}, [options]);
|
||||
|
||||
const table = useReactTable({
|
||||
columns: columns|| [],
|
||||
data: value || [],
|
||||
autoResetAll: false,
|
||||
state: {
|
||||
columnVisibility: columnVisibility || {},
|
||||
},
|
||||
columnResizeMode: 'onChange',
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
});
|
||||
|
||||
const classList = [].concat(containerClassName);
|
||||
features.current?.onTable({table, classList, options});
|
||||
|
||||
const rows = table.getRowModel().rows;
|
||||
const tableEleRef = useRef();
|
||||
|
||||
const isResizing = _.flatMap(
|
||||
table.getHeaderGroups(),
|
||||
headerGroup => headerGroup.headers.map(
|
||||
header => header.column.getIsResizing()
|
||||
)
|
||||
).includes(true);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: () => tableEleRef.current,
|
||||
estimateSize: () => 50,
|
||||
measureElement:
|
||||
typeof window !== 'undefined' &&
|
||||
navigator.userAgent.indexOf('Firefox') === -1
|
||||
? element => element?.getBoundingClientRect().height
|
||||
: undefined,
|
||||
overscan: viewHelperProps.virtualiseOverscan ?? 10,
|
||||
});
|
||||
|
||||
const GridHeader = field.GridHeader || DataGridHeader;
|
||||
const GridRow = field.GridRow || DataGridRow;
|
||||
|
||||
if (!options.visible) return (<></>);
|
||||
|
||||
return (
|
||||
<DataGridContext.Provider value={{
|
||||
table, accessPath, virtualizer, field, dataDispatch, features, options,
|
||||
viewHelperProps,
|
||||
}}>
|
||||
<StyleDataGridBox className={classList.join(' ')}>
|
||||
<Box className='DataGridView-grid'>
|
||||
<GridHeader tableEleRef={tableEleRef} />
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<PgReactTable
|
||||
ref={tableEleRef} table={table} data-test="data-grid-view"
|
||||
tableClassName='DataGridView-table'>
|
||||
<PgReactTableHeader table={table} />
|
||||
<PgReactTableBody style={{
|
||||
height: virtualizer.getTotalSize() + 'px'
|
||||
}}>
|
||||
{
|
||||
virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
return (
|
||||
<PgReactTableRow
|
||||
key={row.id}
|
||||
data-index={virtualRow.index}
|
||||
ref={node => virtualizer.measureElement(node)}
|
||||
style={{
|
||||
// This should always be a `style` as it changes on
|
||||
// scroll.
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
>
|
||||
<GridRow rowId={row.id} isResizing={isResizing}/>
|
||||
</PgReactTableRow>
|
||||
);
|
||||
})
|
||||
}
|
||||
</PgReactTableBody>
|
||||
</PgReactTable>
|
||||
</DndProvider>
|
||||
</Box>
|
||||
</StyleDataGridBox>
|
||||
</DataGridContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
DataGridView.propTypes = {
|
||||
viewHelperProps: PropTypes.object,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func,
|
||||
containerClassName: PropTypes.oneOfType([
|
||||
PropTypes.object, PropTypes.string
|
||||
]),
|
||||
field: PropTypes.object,
|
||||
};
|
||||
|
||||
registerView(DataGridView, 'DataGridView');
|
103
web/pgadmin/static/js/SchemaView/DataGridView/header.jsx
Normal file
103
web/pgadmin/static/js/SchemaView/DataGridView/header.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import AddIcon from '@mui/icons-material/AddOutlined';
|
||||
|
||||
import { SCHEMA_STATE_ACTIONS } from 'sources/SchemaView/SchemaState';
|
||||
import { PgIconButton } from 'sources/components/Buttons';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import gettext from 'sources/gettext';
|
||||
import { requestAnimationAndFocus } from 'sources/utils';
|
||||
|
||||
import { SchemaStateContext } from '../SchemaState';
|
||||
import { SearchBox, SEARCH_STATE_PATH } from './SearchBox';
|
||||
import { DataGridContext } from './context';
|
||||
|
||||
|
||||
export function DataGridHeader({tableEleRef}) {
|
||||
const {
|
||||
accessPath, field, dataDispatch, options, virtualizer, table,
|
||||
} = useContext(DataGridContext);
|
||||
const {
|
||||
addOnTop, canAdd, canAddRow, canEdit, expandEditOnAdd
|
||||
} = options;
|
||||
const rows = table.getRowModel().rows;
|
||||
|
||||
const label = field.label || '';
|
||||
const newRowIndex = useRef(-1);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
|
||||
const onAddClick = useCallback(() => {
|
||||
|
||||
if(!canAddRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = field.schema.getNewData();
|
||||
|
||||
newRowIndex.current = addOnTop ? 0 : rows.length;
|
||||
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
|
||||
path: accessPath,
|
||||
value: newRow,
|
||||
addOnTop: addOnTop
|
||||
});
|
||||
|
||||
schemaState.setState(accessPath.concat(SEARCH_STATE_PATH), '');
|
||||
}, [canAddRow, rows?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newRowIndex.current < -1) return;
|
||||
|
||||
virtualizer.scrollToIndex(newRowIndex.current);
|
||||
|
||||
// Try autofocus on newly added row.
|
||||
setTimeout(() => {
|
||||
const rowInput = tableEleRef.current?.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
);
|
||||
|
||||
if(!rowInput) return;
|
||||
|
||||
requestAnimationAndFocus(tableEleRef.current.querySelector(
|
||||
`.pgrt-row[data-index="${newRowIndex.current}"] input`
|
||||
));
|
||||
|
||||
expandEditOnAdd && canEdit &&
|
||||
rows[newRowIndex.current]?.toggleExpanded(true);
|
||||
|
||||
newRowIndex.current = undefined;
|
||||
}, 50);
|
||||
}, [rows?.length]);
|
||||
|
||||
return (
|
||||
<Box className='DataGridView-gridHeader'>
|
||||
{label && <Box className='DataGridView-gridHeaderText'>{label}</Box>}
|
||||
<Box className='DataGridView-gridHeader-middle'
|
||||
style={{flex: 1, padding: 0}}>
|
||||
<SearchBox />
|
||||
</Box>
|
||||
<Box className='DataGridView-gridControls'>
|
||||
{ canAdd &&
|
||||
<PgIconButton data-test="add-row" title={gettext('Add row')}
|
||||
onClick={onAddClick}
|
||||
icon={<AddIcon />} className='DataGridView-gridControlsButton'
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
DataGridHeader.propTypes = {
|
||||
tableEleRef: CustomPropTypes.ref,
|
||||
};
|
24
web/pgadmin/static/js/SchemaView/DataGridView/index.js
Normal file
24
web/pgadmin/static/js/SchemaView/DataGridView/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/* The DataGridView component is based on react-table component */
|
||||
|
||||
import { DataGridFormHeader } from './formHeader.jsx';
|
||||
import { DataGridHeader } from './header';
|
||||
import { getMappedCell } from './mappedCell';
|
||||
import DataGridView from './grid';
|
||||
|
||||
|
||||
export default DataGridView;
|
||||
|
||||
export {
|
||||
DataGridFormHeader,
|
||||
DataGridHeader,
|
||||
getMappedCell,
|
||||
};
|
105
web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx
Normal file
105
web/pgadmin/static/js/SchemaView/DataGridView/mappedCell.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
import { MappedCellControl } from '../MappedControl';
|
||||
import { SCHEMA_STATE_ACTIONS, SchemaStateContext } from '../SchemaState';
|
||||
import { flatternObject } from '../common';
|
||||
import { useFieldOptions, useFieldValue } from '../hooks';
|
||||
import { listenDepChanges } from '../utils';
|
||||
|
||||
import { DataGridContext, DataGridRowContext } from './context';
|
||||
|
||||
|
||||
export function getMappedCell({field}) {
|
||||
const Cell = ({reRenderRow, getValue}) => {
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const { dataDispatch } = useContext(DataGridContext);
|
||||
const { rowAccessPath, row } = useContext(DataGridRowContext);
|
||||
const colAccessPath = schemaState.accessPath(rowAccessPath, field.id);
|
||||
|
||||
let colOptions = useFieldOptions(colAccessPath, schemaState, key, setKey);
|
||||
let value = useFieldValue(colAccessPath, schemaState, key, setKey);
|
||||
let rowValue = useFieldValue(rowAccessPath, schemaState);
|
||||
|
||||
listenDepChanges(colAccessPath, field, true, schemaState);
|
||||
|
||||
if (!field.id) {
|
||||
console.error(`No id set for the field: ${field}`);
|
||||
value = getValue();
|
||||
rowValue = row.original;
|
||||
colOptions = { disabled: true, readonly: true };
|
||||
} else {
|
||||
colOptions['readonly'] = !colOptions['editable'];
|
||||
rowValue = value;
|
||||
}
|
||||
|
||||
let cellProps = {};
|
||||
|
||||
if (_.isFunction(field.cell) && field.id) {
|
||||
cellProps = evalFunc(null, field.cell, rowValue);
|
||||
|
||||
if (typeof (cellProps) !== 'object')
|
||||
cellProps = {cell: cellProps};
|
||||
}
|
||||
|
||||
const props = {
|
||||
...field,
|
||||
...cellProps,
|
||||
...colOptions,
|
||||
visible: true,
|
||||
rowIndex: row.index,
|
||||
value,
|
||||
row,
|
||||
dataDispatch,
|
||||
onCellChange: (changeValue) => {
|
||||
if (colOptions.disabled) return;
|
||||
if(field.radioType) {
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.BULK_UPDATE,
|
||||
path: rowAccessPath,
|
||||
value: changeValue,
|
||||
id: field.id
|
||||
});
|
||||
}
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: colAccessPath,
|
||||
value: changeValue,
|
||||
});
|
||||
},
|
||||
reRenderRow: reRenderRow
|
||||
};
|
||||
|
||||
if(_.isUndefined(field.cell)) {
|
||||
console.error('cell is required ', field);
|
||||
props.cell = 'unknown';
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => <MappedCellControl {...props}/>,
|
||||
[...flatternObject(colOptions), value, row.index]
|
||||
);
|
||||
};
|
||||
|
||||
Cell.displayName = 'Cell';
|
||||
Cell.propTypes = {
|
||||
reRenderRow: PropTypes.func,
|
||||
getValue: PropTypes.func,
|
||||
};
|
||||
|
||||
return Cell;
|
||||
}
|
93
web/pgadmin/static/js/SchemaView/DataGridView/row.jsx
Normal file
93
web/pgadmin/static/js/SchemaView/DataGridView/row.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useMemo, useRef } from 'react';
|
||||
|
||||
import { flexRender } from '@tanstack/react-table';
|
||||
|
||||
import {
|
||||
PgReactTableCell, PgReactTableRowContent, PgReactTableRowExpandContent,
|
||||
} from 'sources/components/PgReactTableStyled';
|
||||
|
||||
import { SchemaStateContext } from '../SchemaState';
|
||||
import { useFieldOptions } from '../hooks';
|
||||
|
||||
import { DataGridContext, DataGridRowContext } from './context';
|
||||
|
||||
|
||||
export function DataGridRow({rowId, isResizing}) {
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
|
||||
const { accessPath, options, table, features } = useContext(
|
||||
DataGridContext
|
||||
);
|
||||
|
||||
const rowAccessPath = schemaState.accessPath(accessPath, rowId);
|
||||
const rowOptions = useFieldOptions(rowAccessPath, schemaState);
|
||||
|
||||
const rowRef = useRef(null);
|
||||
const row = table.getRowModel().rows[rowId];
|
||||
|
||||
/*
|
||||
* Memoize the row to avoid unnecessary re-render. If table data changes,
|
||||
* then react-table re-renders the complete tables.
|
||||
*
|
||||
* We can avoid re-render by if row data has not changed.
|
||||
*/
|
||||
let classList = [];
|
||||
let attributes = {};
|
||||
let expandedRowContents = [];
|
||||
|
||||
features.current?.onRow({
|
||||
index: rowId, row, rowRef, classList, attributes, expandedRowContents,
|
||||
rowOptions, tableOptions: options
|
||||
});
|
||||
|
||||
let depsMap = [
|
||||
rowId, row?.getIsExpanded(), isResizing, expandedRowContents.length
|
||||
];
|
||||
|
||||
return useMemo(() => (
|
||||
!row ? <></> :
|
||||
<DataGridRowContext.Provider value={{ rowAccessPath, row }}>
|
||||
<PgReactTableRowContent ref={rowRef}
|
||||
className={classList.join[' ']}
|
||||
data-test='data-table-row' style={{position: 'initial'}}
|
||||
{...attributes}
|
||||
>
|
||||
{
|
||||
row?.getVisibleCells().map((cell) => {
|
||||
const columnDef = cell.column.columnDef;
|
||||
const content = flexRender(
|
||||
columnDef.cell, {
|
||||
key: columnDef.cell?.type ?? columnDef.id,
|
||||
row: row,
|
||||
getValue: cell.getValue,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<PgReactTableCell cell={cell} row={row} key={cell.id}>
|
||||
{content}
|
||||
</PgReactTableCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
<div className='hover-overlay'></div>
|
||||
</PgReactTableRowContent>
|
||||
{
|
||||
expandedRowContents.length ?
|
||||
<PgReactTableRowExpandContent
|
||||
row={row} key={`expanded-row-${row?.id}`}>
|
||||
{expandedRowContents}
|
||||
</PgReactTableRowExpandContent> : <></>
|
||||
}
|
||||
</DataGridRowContext.Provider>
|
||||
), [...depsMap]);
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { isModeSupportedByField } from 'sources/SchemaView/common';
|
||||
import { getMappedCell } from '../mappedCell';
|
||||
|
||||
|
||||
export function createGridColumns({schema, field, viewHelperProps}) {
|
||||
|
||||
const columns = field.columns;
|
||||
const colunnFilterExp = _.isArray(columns) ?
|
||||
((f) => (columns.indexOf(f.id) > -1)) : (() => true);
|
||||
const sortExp = _.isArray(columns) ?
|
||||
((firstF, secondF) => (
|
||||
(columns.indexOf(firstF.id) < columns.indexOf(secondF.id)) ? -1 : 1
|
||||
)) : (() => 0);
|
||||
const columnVisibility = {};
|
||||
|
||||
const cols = schema.fields.filter(colunnFilterExp).sort(sortExp).map(
|
||||
(field) => {
|
||||
let widthParms = {};
|
||||
|
||||
if(field.width) {
|
||||
widthParms.size = field.width;
|
||||
widthParms.minSize = field.width;
|
||||
} else {
|
||||
widthParms.size = 75;
|
||||
widthParms.minSize = 75;
|
||||
}
|
||||
|
||||
if(field.minWidth) {
|
||||
widthParms.minSize = field.minWidth;
|
||||
}
|
||||
|
||||
if(field.maxWidth) {
|
||||
widthParms.maxSize = field.maxWidth;
|
||||
}
|
||||
|
||||
widthParms.enableResizing =
|
||||
_.isUndefined(field.enableResizing) ? true : Boolean(
|
||||
field.enableResizing
|
||||
);
|
||||
columnVisibility[field.id] = isModeSupportedByField(
|
||||
field, viewHelperProps
|
||||
);
|
||||
|
||||
return {
|
||||
header: field.label||<> </>,
|
||||
accessorKey: field.id,
|
||||
field: field,
|
||||
enableResizing: true,
|
||||
enableSorting: false,
|
||||
...widthParms,
|
||||
cell: getMappedCell({field}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return [cols, columnVisibility];
|
||||
}
|
16
web/pgadmin/static/js/SchemaView/DataGridView/utils/index.js
Normal file
16
web/pgadmin/static/js/SchemaView/DataGridView/utils/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { createGridColumns } from './createGridColumns';
|
||||
|
||||
export const GRID_STATE = '__gridState';
|
||||
|
||||
export {
|
||||
createGridColumns,
|
||||
};
|
@@ -37,7 +37,10 @@ export class DepListener {
|
||||
if(dataPath.length > 0) {
|
||||
data = _.get(state, dataPath);
|
||||
}
|
||||
_.assign(data, listener.callback?.(data, listener.source, state, actionObj) || {});
|
||||
_.assign(
|
||||
data,
|
||||
listener.callback?.(data, listener.source, state, actionObj) || {}
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -70,7 +73,10 @@ export class DepListener {
|
||||
|
||||
getDeferredDepChange(currPath, state, actionObj) {
|
||||
let deferredList = [];
|
||||
let allListeners = _.filter(this._depListeners, (entry)=>_.join(currPath, '|').startsWith(_.join(entry.source, '|')));
|
||||
let allListeners = _.filter(this._depListeners, (entry) => _.join(
|
||||
currPath, '|'
|
||||
).startsWith(_.join(entry.source, '|')));
|
||||
|
||||
if(allListeners) {
|
||||
for(const listener of allListeners) {
|
||||
if(listener.defCallback) {
|
||||
|
27
web/pgadmin/static/js/SchemaView/FieldControl.jsx
Normal file
27
web/pgadmin/static/js/SchemaView/FieldControl.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
export const FieldControl = ({schemaId, item}) => {
|
||||
const Control = item.control;
|
||||
const props = item.controlProps;
|
||||
const children = item.controls;
|
||||
|
||||
return useMemo(() =>
|
||||
<Control {...props}>
|
||||
{
|
||||
children &&
|
||||
children.map(
|
||||
(child, idx) => <FieldControl key={idx} item={child}/>
|
||||
)
|
||||
}
|
||||
</Control>, [schemaId, Control, props, children]
|
||||
);
|
||||
};
|
@@ -7,156 +7,67 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
import Grid from '@mui/material/Grid';
|
||||
import _ from 'lodash';
|
||||
import React, { useContext, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FieldSet from 'sources/components/FieldSet';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import {
|
||||
getFieldMetaData, SCHEMA_STATE_ACTIONS, SchemaStateContext
|
||||
} from './common';
|
||||
import { FieldControl } from './FieldControl';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import { useFieldSchema, useFieldValue } from './hooks';
|
||||
import { registerView } from './registry';
|
||||
import { createFieldControls, listenDepChanges } from './utils';
|
||||
|
||||
|
||||
const INLINE_COMPONENT_ROWGAP = '8px';
|
||||
|
||||
export default function FieldSetView({
|
||||
value, schema={}, viewHelperProps, accessPath, dataDispatch,
|
||||
controlClassName, isDataGridForm=false, label, visible
|
||||
field, accessPath, dataDispatch, viewHelperProps, controlClassName,
|
||||
}) {
|
||||
const [key, setRefreshKey] = useState(0);
|
||||
const schema = field.schema;
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const value = useFieldValue(accessPath, schemaState);
|
||||
const options = useFieldSchema(
|
||||
field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey
|
||||
);
|
||||
const label = field.label;
|
||||
|
||||
useEffect(() => {
|
||||
// Calculate the fields which depends on the current field.
|
||||
if(!isDataGridForm && schemaState) {
|
||||
schema.fields.forEach((field) => {
|
||||
/* Self change is also dep change */
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState?.addDepListener(
|
||||
accessPath.concat(field.id), accessPath.concat(field.id),
|
||||
field.depChange, field.deferredDepChange
|
||||
);
|
||||
}
|
||||
(evalFunc(null, field.deps) || []).forEach((dep) => {
|
||||
let source = accessPath.concat(dep);
|
||||
if(_.isArray(dep)) {
|
||||
source = dep;
|
||||
}
|
||||
if(field.depChange) {
|
||||
schemaState?.addDepListener(
|
||||
source, accessPath.concat(field.id), field.depChange
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
listenDepChanges(accessPath, field, options.visible, schemaState);
|
||||
|
||||
let viewFields = [];
|
||||
let inlineComponents = [];
|
||||
const fieldGroups = useMemo(
|
||||
() => createFieldControls({
|
||||
schema, schemaState, accessPath, viewHelperProps, dataDispatch
|
||||
}),
|
||||
[schema, schemaState, accessPath, viewHelperProps, dataDispatch]
|
||||
);
|
||||
|
||||
if(!visible) {
|
||||
// We won't show empty feldset too.
|
||||
if(!options.visible || !fieldGroups.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// Prepare the array of components based on the types.
|
||||
for(const field of schema.fields) {
|
||||
const {
|
||||
visible, disabled, readonly, modeSupported
|
||||
} = getFieldMetaData(field, schema, value, viewHelperProps);
|
||||
|
||||
if(!modeSupported) continue;
|
||||
|
||||
// Its a form control.
|
||||
const hasError = (field.id === schemaState?.errors.name);
|
||||
|
||||
/*
|
||||
* When there is a change, the dependent values can also change.
|
||||
* Let's pass these changes to dependent for take them into effect to
|
||||
* generate new values.
|
||||
*/
|
||||
const currentControl = <MappedFormControl
|
||||
state={value}
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
name={field.id}
|
||||
value={value[field.id]}
|
||||
{...field}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
visible={visible}
|
||||
onChange={(changeValue)=>{
|
||||
/* Get the changes on dependent fields as well */
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat(field.id),
|
||||
value: changeValue,
|
||||
});
|
||||
}}
|
||||
hasError={hasError}
|
||||
className={controlClassName}
|
||||
memoDeps={[
|
||||
value[field.id],
|
||||
readonly,
|
||||
disabled,
|
||||
visible,
|
||||
hasError,
|
||||
controlClassName,
|
||||
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
|
||||
]}
|
||||
/>;
|
||||
|
||||
if(field.inlineNext) {
|
||||
inlineComponents.push(React.cloneElement(currentControl, {
|
||||
withContainer: false, controlGridBasis: 3
|
||||
}));
|
||||
} else if(inlineComponents?.length > 0) {
|
||||
inlineComponents.push(React.cloneElement(currentControl, {
|
||||
withContainer: false, controlGridBasis: 3
|
||||
}));
|
||||
viewFields.push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className={controlClassName} rowGap={INLINE_COMPONENT_ROWGAP}>
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
inlineComponents = [];
|
||||
} else {
|
||||
viewFields.push(currentControl);
|
||||
}
|
||||
}
|
||||
|
||||
if(inlineComponents?.length > 0) {
|
||||
viewFields.push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className={controlClassName} rowGap={INLINE_COMPONENT_ROWGAP}>
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldSet title={label} className={controlClassName}>
|
||||
{viewFields}
|
||||
{fieldGroups.map(
|
||||
(fieldGroup, gidx) => (
|
||||
<React.Fragment key={gidx}>
|
||||
{fieldGroup.controls.map(
|
||||
(item, idx) => <FieldControl
|
||||
item={item} key={idx} schemaId={schema._id} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
FieldSetView.propTypes = {
|
||||
value: PropTypes.any,
|
||||
schema: CustomPropTypes.schemaUI.isRequired,
|
||||
viewHelperProps: PropTypes.object,
|
||||
isDataGridForm: PropTypes.bool,
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func,
|
||||
controlClassName: CustomPropTypes.className,
|
||||
label: PropTypes.string,
|
||||
visible: PropTypes.oneOfType([
|
||||
PropTypes.bool, PropTypes.func,
|
||||
]),
|
||||
field: PropTypes.object,
|
||||
};
|
||||
|
||||
registerView(FieldSetView, 'FieldSetView');
|
||||
|
30
web/pgadmin/static/js/SchemaView/FormLoader.jsx
Normal file
30
web/pgadmin/static/js/SchemaView/FormLoader.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useEffect, useState, useMemo } from 'react';
|
||||
|
||||
import Loader from 'sources/components/Loader';
|
||||
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
|
||||
|
||||
export const FormLoader = () => {
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const message = schemaState.loadingMessage;
|
||||
|
||||
useEffect(() => {
|
||||
// Refresh on message changes.
|
||||
return schemaState.subscribe(
|
||||
['message'], () => setKey(Date.now()), 'states'
|
||||
);
|
||||
}, [key]);
|
||||
|
||||
return useMemo(() => <Loader message={message}/>, [message, key]);
|
||||
};
|
@@ -8,80 +8,117 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, {
|
||||
useContext, useEffect, useMemo, useRef, useState
|
||||
useCallback, useContext, useEffect, useMemo, useRef, useState
|
||||
} from 'react';
|
||||
import { Box, Tab, Tabs, Grid } from '@mui/material';
|
||||
import { Box, Tab, Tabs } from '@mui/material';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormNote, InputSQL } from 'sources/components/FormComponents';
|
||||
|
||||
import {
|
||||
FormFooterMessage, MESSAGE_TYPE, FormNote
|
||||
} from 'sources/components/FormComponents';
|
||||
import TabPanel from 'sources/components/TabPanel';
|
||||
import { useOnScreen } from 'sources/custom_hooks';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import gettext from 'sources/gettext';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
import DataGridView from './DataGridView';
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import FieldSetView from './FieldSetView';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS, SchemaStateContext, getFieldMetaData
|
||||
} from './common';
|
||||
|
||||
import { FieldControl } from './FieldControl';
|
||||
import { SQLTab } from './SQLTab';
|
||||
import { FormContentBox } from './StyledComponents';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import { useFieldSchema, useFieldValue } from './hooks';
|
||||
import { registerView, View } from './registry';
|
||||
import { createFieldControls, listenDepChanges } from './utils';
|
||||
|
||||
const ErrorMessageBox = () => {
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const onErrClose = useCallback(() => {
|
||||
const err = { ...schemaState.errors, message: '' };
|
||||
// Unset the error message, but not the name.
|
||||
schemaState.setError(err);
|
||||
}, [schemaState]);
|
||||
const errors = schemaState.errors;
|
||||
const message = errors?.message || '';
|
||||
|
||||
/* Optional SQL tab */
|
||||
function SQLTab({active, getSQLValue}) {
|
||||
const [sql, setSql] = useState('Loading...');
|
||||
useEffect(()=>{
|
||||
let unmounted = false;
|
||||
if(active) {
|
||||
setSql('Loading...');
|
||||
getSQLValue().then((value)=>{
|
||||
if(!unmounted) {
|
||||
setSql(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ()=>{unmounted=true;};
|
||||
}, [active]);
|
||||
useEffect(() => {
|
||||
// Refresh on message changes.
|
||||
return schemaState.subscribe(
|
||||
['errors', 'message'], () => setKey(Date.now()), 'states'
|
||||
);
|
||||
}, [key]);
|
||||
|
||||
return <InputSQL
|
||||
value={sql}
|
||||
options={{
|
||||
readOnly: true,
|
||||
}}
|
||||
readonly={true}
|
||||
className='FormView-sqlTabInput'
|
||||
return <FormFooterMessage
|
||||
type={MESSAGE_TYPE.ERROR} message={message} onClose={onErrClose}
|
||||
/>;
|
||||
}
|
||||
|
||||
SQLTab.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
/* The first component of schema view form */
|
||||
// The first component of schema view form.
|
||||
export default function FormView({
|
||||
value, schema={}, viewHelperProps, isNested=false, accessPath, dataDispatch, hasSQLTab,
|
||||
getSQLValue, onTabChange, firstEleRef, className, isDataGridForm=false, isTabView=true, visible}) {
|
||||
let defaultTab = gettext('General');
|
||||
let tabs = {};
|
||||
let tabsClassname = {};
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
accessPath, schema=null, isNested=false, dataDispatch, className,
|
||||
hasSQLTab, getSQLValue, isTabView=true, viewHelperProps, field,
|
||||
showError=false, resetKey, focusOnFirstInput=false
|
||||
}) {
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const value = useFieldValue(accessPath, schemaState);
|
||||
const { visible } = useFieldSchema(
|
||||
field, accessPath, value, viewHelperProps, schemaState, key, setKey
|
||||
);
|
||||
|
||||
const firstEleID = useRef();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
const formRef = useRef();
|
||||
const onScreenTracker = useRef(false);
|
||||
let groupLabels = {};
|
||||
const schemaRef = useRef(schema);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
|
||||
let isOnScreen = useOnScreen(formRef);
|
||||
|
||||
useEffect(()=>{
|
||||
if (!schema) schema = field.schema;
|
||||
|
||||
// Set focus on the first focusable element.
|
||||
useEffect(() => {
|
||||
if (!focusOnFirstInput) return;
|
||||
setTimeout(() => {
|
||||
const formEle = formRef.current;
|
||||
if (!formEle) return;
|
||||
const activeTabElement = formEle.querySelector(
|
||||
'[data-test="tabpanel"]:not([hidden])'
|
||||
);
|
||||
if (!activeTabElement) return;
|
||||
|
||||
// Find the first focusable input, which is either:
|
||||
// * An editable Input element.
|
||||
// * A select element, which is not disabled.
|
||||
// * An href element.
|
||||
// * Any element with 'tabindex', but - tabindex is not set to '-1'.
|
||||
const firstFocussableElement = activeTabElement.querySelector([
|
||||
'button:not([role="tab"])',
|
||||
'[href]',
|
||||
'input:not(disabled)',
|
||||
'select:not(disabled)',
|
||||
'textarea',
|
||||
'[tabindex]:not([tabindex="-1"]):not([data-test="tabpanel"])',
|
||||
].join(', '));
|
||||
|
||||
if (firstFocussableElement) firstFocussableElement.focus();
|
||||
}, 200);
|
||||
}, [tabValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// Refresh on message changes.
|
||||
return schemaState.subscribe(
|
||||
['errors', 'message'],
|
||||
(newState, prevState) => {
|
||||
if (_.isUndefined(newState) || _.isUndefined(prevState));
|
||||
setKey(Date.now());
|
||||
},
|
||||
'states'
|
||||
);
|
||||
}, [key]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
if(isOnScreen) {
|
||||
/* Don't do it when the form is alredy visible */
|
||||
if(!onScreenTracker.current) {
|
||||
@@ -93,347 +130,184 @@ export default function FormView({
|
||||
onScreenTracker.current = false;
|
||||
}
|
||||
}, [isOnScreen]);
|
||||
|
||||
listenDepChanges(accessPath, field, visible, schemaState);
|
||||
|
||||
useEffect(()=>{
|
||||
/* Calculate the fields which depends on the current field */
|
||||
if(!isDataGridForm) {
|
||||
schemaRef.current.fields.forEach((field)=>{
|
||||
/* Self change is also dep change */
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
(evalFunc(null, field.deps) || []).forEach((dep)=>{
|
||||
// when dep is a string then prepend the complete accessPath
|
||||
let source = accessPath.concat(dep);
|
||||
|
||||
// but when dep is an array, then the intention is to provide the exact accesspath
|
||||
if(_.isArray(dep)) {
|
||||
source = dep;
|
||||
}
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState?.addDepListener(source, accessPath.concat(field.id), field.depChange, field.deferredDepChange);
|
||||
}
|
||||
});
|
||||
});
|
||||
return ()=>{
|
||||
/* Cleanup the listeners when unmounting */
|
||||
schemaState?.removeDepListener(accessPath);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* Upon reset, set the tab to first */
|
||||
useEffect(()=>{
|
||||
if (schemaState?.isReady)
|
||||
// Upon reset, set the tab to first.
|
||||
useEffect(() => {
|
||||
if (!visible || !resetKey) return;
|
||||
if (resetKey) {
|
||||
setTabValue(0);
|
||||
}, [schemaState?.isReady]);
|
||||
|
||||
let fullTabs = [];
|
||||
let inlineComponents = [];
|
||||
let inlineCompGroup = null;
|
||||
|
||||
/* Prepare the array of components based on the types */
|
||||
for(const field of schemaRef.current.fields) {
|
||||
let {
|
||||
visible, disabled, readonly, canAdd, canEdit, canDelete, canReorder,
|
||||
canAddRow, modeSupported
|
||||
} = getFieldMetaData(field, schema, value, viewHelperProps);
|
||||
|
||||
if(!modeSupported) continue;
|
||||
|
||||
let {group, CustomControl} = field;
|
||||
|
||||
if(field.type === 'group') {
|
||||
groupLabels[field.id] = field.label;
|
||||
|
||||
if(!visible) {
|
||||
schemaRef.current.filterGroups.push(field.label);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}, [resetKey]);
|
||||
|
||||
group = groupLabels[group] || group || defaultTab;
|
||||
|
||||
if(!tabs[group]) tabs[group] = [];
|
||||
|
||||
// Lets choose the path based on type.
|
||||
if(field.type === 'nested-tab') {
|
||||
/* Pass on the top schema */
|
||||
if(isNested) {
|
||||
field.schema.top = schemaRef.current.top;
|
||||
} else {
|
||||
field.schema.top = schema;
|
||||
}
|
||||
tabs[group].push(
|
||||
<FormView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}
|
||||
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
|
||||
{...field} visible={visible}/>
|
||||
);
|
||||
} else if(field.type === 'nested-fieldset') {
|
||||
/* Pass on the top schema */
|
||||
if(isNested) {
|
||||
field.schema.top = schemaRef.current.top;
|
||||
} else {
|
||||
field.schema.top = schema;
|
||||
}
|
||||
tabs[group].push(
|
||||
<FieldSetView key={`nested${tabs[group].length}`} value={value} viewHelperProps={viewHelperProps}
|
||||
schema={field.schema} accessPath={accessPath} dataDispatch={dataDispatch} isNested={true} isDataGridForm={isDataGridForm}
|
||||
controlClassName='FormView-controlRow'
|
||||
{...field} visible={visible}/>
|
||||
);
|
||||
} else if(field.type === 'collection') {
|
||||
/* If its a collection, let data grid view handle it */
|
||||
/* Pass on the top schema */
|
||||
if(isNested) {
|
||||
field.schema.top = schemaRef.current.top;
|
||||
} else {
|
||||
field.schema.top = schemaRef.current;
|
||||
}
|
||||
|
||||
if(!_.isUndefined(field.fixedRows)) {
|
||||
canAdd = false;
|
||||
canDelete = false;
|
||||
}
|
||||
|
||||
const ctrlProps = {
|
||||
key: field.id, ...field,
|
||||
value: value[field.id] || [], viewHelperProps: viewHelperProps,
|
||||
schema: field.schema, accessPath: accessPath.concat(field.id), dataDispatch: dataDispatch,
|
||||
containerClassName: 'FormView-controlRow',
|
||||
canAdd: canAdd, canReorder: canReorder,
|
||||
canEdit: canEdit, canDelete: canDelete,
|
||||
visible: visible, canAddRow: canAddRow, onDelete: field.onDelete, canSearch: field.canSearch,
|
||||
expandEditOnAdd: field.expandEditOnAdd,
|
||||
fixedRows: (viewHelperProps.mode == 'create' ? field.fixedRows : undefined),
|
||||
addOnTop: Boolean(field.addOnTop)
|
||||
};
|
||||
|
||||
if(CustomControl) {
|
||||
tabs[group].push(<CustomControl {...ctrlProps}/>);
|
||||
} else {
|
||||
tabs[group].push(<DataGridView {...ctrlProps} />);
|
||||
}
|
||||
} else {
|
||||
/* Its a form control */
|
||||
const hasError = _.isEqual(
|
||||
accessPath.concat(field.id), schemaState.errors?.name
|
||||
);
|
||||
/* When there is a change, the dependent values can change
|
||||
* lets pass the new changes to dependent and get the new values
|
||||
* from there as well.
|
||||
*/
|
||||
if(field.isFullTab) {
|
||||
tabsClassname[group] ='FormView-fullSpace';
|
||||
fullTabs.push(group);
|
||||
}
|
||||
|
||||
const id = field.id || `control${tabs[group].length}`;
|
||||
if(visible && !disabled && !firstEleID.current) {
|
||||
firstEleID.current = field.id;
|
||||
}
|
||||
|
||||
let currentControl = <MappedFormControl
|
||||
inputRef={(ele)=>{
|
||||
if(firstEleRef && firstEleID.current === field.id) {
|
||||
if(typeof firstEleRef == 'function') {
|
||||
firstEleRef(ele);
|
||||
} else {
|
||||
firstEleRef.current = ele;
|
||||
}
|
||||
}
|
||||
}}
|
||||
state={value}
|
||||
key={id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
name={id}
|
||||
value={value[id]}
|
||||
{...field}
|
||||
id={id}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
visible={visible}
|
||||
onChange={(changeValue)=>{
|
||||
/* Get the changes on dependent fields as well */
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: accessPath.concat(id),
|
||||
value: changeValue,
|
||||
});
|
||||
}}
|
||||
hasError={hasError}
|
||||
className='FormView-controlRow'
|
||||
noLabel={field.isFullTab}
|
||||
memoDeps={[
|
||||
value[id],
|
||||
readonly,
|
||||
disabled,
|
||||
visible,
|
||||
hasError,
|
||||
'FormView-controlRow',
|
||||
...(evalFunc(null, field.deps) || []).map((dep)=>value[dep]),
|
||||
]}
|
||||
/>;
|
||||
|
||||
if(field.isFullTab && field.helpMessage) {
|
||||
currentControl = (<React.Fragment key={`coll-${field.id}`}>
|
||||
<FormNote key={`note-${field.id}`} text={field.helpMessage}/>
|
||||
{currentControl}
|
||||
</React.Fragment>);
|
||||
}
|
||||
|
||||
if(field.inlineNext) {
|
||||
inlineComponents.push(React.cloneElement(currentControl, {
|
||||
withContainer: false, controlGridBasis: 3
|
||||
}));
|
||||
inlineCompGroup = group;
|
||||
} else if(inlineComponents?.length > 0) {
|
||||
inlineComponents.push(React.cloneElement(currentControl, {
|
||||
withContainer: false, controlGridBasis: 3
|
||||
}));
|
||||
tabs[group].push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className='FormView-controlRow' rowGap="8px">
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
inlineComponents = [];
|
||||
inlineCompGroup = null;
|
||||
} else {
|
||||
tabs[group].push(currentControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(inlineComponents?.length > 0) {
|
||||
tabs[inlineCompGroup].push(
|
||||
<Grid container spacing={0} key={`ic-${inlineComponents[0].key}`}
|
||||
className='FormView-controlRow' rowGap="8px">
|
||||
{inlineComponents}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
let finalTabs = _.pickBy(
|
||||
tabs, (v, tabName) => schemaRef.current.filterGroups.indexOf(tabName) <= -1
|
||||
const finalGroups = useMemo(
|
||||
() => createFieldControls({
|
||||
schema, schemaState, accessPath, viewHelperProps, dataDispatch
|
||||
}),
|
||||
[schema, schemaState, accessPath, viewHelperProps, dataDispatch]
|
||||
);
|
||||
|
||||
// Add the SQL tab (if required)
|
||||
let sqlTabActive = false;
|
||||
let sqlTabName = gettext('SQL');
|
||||
|
||||
if(hasSQLTab) {
|
||||
sqlTabActive = (Object.keys(finalTabs).length === tabValue);
|
||||
// Re-render and fetch the SQL tab when it is active.
|
||||
finalTabs[sqlTabName] = [
|
||||
<SQLTab key="sqltab" active={sqlTabActive} getSQLValue={getSQLValue} />,
|
||||
];
|
||||
tabsClassname[sqlTabName] = 'FormView-fullSpace';
|
||||
fullTabs.push(sqlTabName);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onTabChange?.(tabValue, Object.keys(tabs)[tabValue], sqlTabActive);
|
||||
}, [tabValue]);
|
||||
|
||||
const isSingleCollection = useMemo(()=>{
|
||||
// we can check if it is a single-collection.
|
||||
// in that case, we could force virtualization of the collection.
|
||||
if(isTabView) return false;
|
||||
|
||||
const visibleEle = Object.values(finalTabs)[0].filter(
|
||||
(c) => c.props.visible
|
||||
);
|
||||
return visibleEle.length == 1 && visibleEle[0]?.type == DataGridView;
|
||||
}, [isTabView, finalTabs]);
|
||||
|
||||
// Check whether form is kept hidden by visible prop.
|
||||
if(!_.isUndefined(visible) && !visible) {
|
||||
if(!finalGroups || (!_.isUndefined(visible) && !visible)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const isSingleCollection = () => {
|
||||
const DataGridView = View('DataGridView');
|
||||
return (
|
||||
finalGroups.length == 1 &&
|
||||
finalGroups[0].controls.length == 1 &&
|
||||
finalGroups[0].controls[0].control == DataGridView
|
||||
);
|
||||
};
|
||||
|
||||
if(isTabView) {
|
||||
return (
|
||||
<FormContentBox height="100%" display="flex" flexDirection="column"
|
||||
className={className} ref={formRef} data-test="form-view">
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(event, selTabValue) => { setTabValue(selTabValue); }}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
action={(ref)=>ref?.updateIndicator()}
|
||||
>
|
||||
{Object.keys(finalTabs).map((tabName)=>{
|
||||
return <Tab key={tabName} label={tabName} data-test={tabName}/>;
|
||||
})}
|
||||
</Tabs>
|
||||
</Box>
|
||||
{Object.keys(finalTabs).map((tabName, i)=>{
|
||||
let contentClassName = [(
|
||||
schemaState.errors?.message ? 'FormView-errorMargin': null
|
||||
)];
|
||||
<>
|
||||
<FormContentBox height="100%" display="flex" flexDirection="column"
|
||||
className={className} ref={formRef} data-test="form-view">
|
||||
<Box>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(ev, nextTabIndex) => { setTabValue(nextTabIndex); }}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
action={(ref) => ref?.updateIndicator()}
|
||||
>{
|
||||
finalGroups.map((tabGroup, idx) =>
|
||||
<Tab
|
||||
key={tabGroup.id}
|
||||
label={tabGroup.label}
|
||||
data-test={tabGroup.id}
|
||||
className={
|
||||
tabGroup.hasError &&
|
||||
tabValue != idx ? 'tab-with-error' : ''
|
||||
}
|
||||
/>
|
||||
)
|
||||
}{hasSQLTab &&
|
||||
<Tab
|
||||
key={'sql-tab'}
|
||||
label={gettext('SQL')}
|
||||
data-test={'SQL'}
|
||||
/>
|
||||
}</Tabs>
|
||||
</Box>
|
||||
{
|
||||
finalGroups.map((group, idx) => {
|
||||
let contentClassName = [
|
||||
group.isFullTab ?
|
||||
'FormView-fullControl' : 'FormView-nestedControl',
|
||||
schemaState.errors?.message ? 'FormView-errorMargin' : null
|
||||
];
|
||||
|
||||
if(fullTabs.indexOf(tabName) == -1) {
|
||||
contentClassName.push('FormView-nestedControl');
|
||||
} else {
|
||||
contentClassName.push('FormView-fullControl');
|
||||
let id = group.id.replace(' ', '');
|
||||
|
||||
return (
|
||||
<TabPanel
|
||||
key={id}
|
||||
value={tabValue}
|
||||
index={idx}
|
||||
classNameRoot={[
|
||||
group.className,
|
||||
(isNested ? 'FormView-nestedTabPanel' : null)
|
||||
].join(' ')}
|
||||
className={contentClassName.join(' ')}
|
||||
data-testid={group.id}>
|
||||
{
|
||||
group.isFullTab && group.field?.helpMessage ?
|
||||
<FormNote
|
||||
key={`note-${group.field.id}`}
|
||||
text={group.field.helpMessage}/> :
|
||||
<></>
|
||||
}
|
||||
{
|
||||
group.controls.map(
|
||||
(item, idx) => <FieldControl
|
||||
item={item} key={idx} schemaId={schema._id} />
|
||||
)
|
||||
}
|
||||
</TabPanel>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<TabPanel key={tabName} value={tabValue} index={i}
|
||||
classNameRoot={[
|
||||
tabsClassname[tabName],
|
||||
(isNested ? 'FormView-nestedTabPanel' : null)
|
||||
].join(' ')}
|
||||
className={contentClassName.join(' ')} data-testid={tabName}>
|
||||
{finalTabs[tabName]}
|
||||
</TabPanel>
|
||||
);
|
||||
})}
|
||||
</FormContentBox>
|
||||
{
|
||||
hasSQLTab &&
|
||||
<TabPanel
|
||||
key={'sql-tab'}
|
||||
value={tabValue}
|
||||
index={finalGroups.length}
|
||||
classNameRoot={'FormView-fullSpace'}
|
||||
data-testid={'SQL'}
|
||||
>
|
||||
<SQLTab
|
||||
active={(Object.keys(finalGroups).length === tabValue)}
|
||||
getSQLValue={getSQLValue}
|
||||
/>
|
||||
</TabPanel>
|
||||
}
|
||||
</FormContentBox>
|
||||
{ showError && <ErrorMessageBox /> }
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
let contentClassName = [
|
||||
isSingleCollection ? 'FormView-singleCollectionPanelContent' :
|
||||
isSingleCollection() ? 'FormView-singleCollectionPanelContent' :
|
||||
'FormView-nonTabPanelContent',
|
||||
(schemaState.errors?.message ? 'FormView-errorMargin' : null)
|
||||
];
|
||||
return (
|
||||
<FormContentBox height="100%" display="flex" flexDirection="column" className={className} ref={formRef} data-test="form-view">
|
||||
<TabPanel value={tabValue} index={0} classNameRoot={[isSingleCollection ? 'FormView-singleCollectionPanel' : 'FormView-nonTabPanel',className].join(' ')}
|
||||
className={contentClassName.join(' ')}>
|
||||
{Object.keys(finalTabs).map((tabName) => {
|
||||
return (
|
||||
<React.Fragment key={tabName}>
|
||||
{finalTabs[tabName]}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</TabPanel>
|
||||
</FormContentBox>
|
||||
<>
|
||||
<FormContentBox
|
||||
height="100%" display="flex" flexDirection="column"
|
||||
className={className}
|
||||
ref={formRef}
|
||||
data-test="form-view"
|
||||
>
|
||||
<TabPanel
|
||||
value={tabValue} index={0}
|
||||
classNameRoot={[
|
||||
isSingleCollection() ?
|
||||
'FormView-singleCollectionPanel' : 'FormView-nonTabPanel',
|
||||
className
|
||||
].join(' ')}
|
||||
className={contentClassName.join(' ')}>
|
||||
{
|
||||
finalGroups.map((group, idx) =>
|
||||
<React.Fragment key={idx}>{
|
||||
group.controls.map(
|
||||
(item, idx) => <FieldControl
|
||||
item={item} key={idx} schemaId={schema._id}
|
||||
/>
|
||||
)
|
||||
}</React.Fragment>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasSQLTab && <SQLTab active={true} getSQLValue={getSQLValue} />
|
||||
}
|
||||
</TabPanel>
|
||||
</FormContentBox>
|
||||
{ showError && <ErrorMessageBox /> }
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
FormView.propTypes = {
|
||||
value: PropTypes.any,
|
||||
schema: CustomPropTypes.schemaUI.isRequired,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
viewHelperProps: PropTypes.object,
|
||||
isNested: PropTypes.bool,
|
||||
isDataGridForm: PropTypes.bool,
|
||||
isTabView: PropTypes.bool,
|
||||
visible: PropTypes.oneOfType([
|
||||
PropTypes.bool, PropTypes.func,
|
||||
]),
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func,
|
||||
hasSQLTab: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func,
|
||||
onTabChange: PropTypes.func,
|
||||
firstEleRef: CustomPropTypes.ref,
|
||||
className: CustomPropTypes.className,
|
||||
field: PropTypes.object,
|
||||
showError: PropTypes.bool,
|
||||
};
|
||||
|
||||
registerView(FormView, 'FormView');
|
||||
|
56
web/pgadmin/static/js/SchemaView/InlineView.jsx
Normal file
56
web/pgadmin/static/js/SchemaView/InlineView.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { Grid } from '@mui/material';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import { useFieldOptions } from './hooks';
|
||||
import { registerView } from './registry';
|
||||
import { listenDepChanges } from './utils';
|
||||
|
||||
|
||||
// The first component of schema view form.
|
||||
export default function InlineView({
|
||||
accessPath, field, children, viewHelperProps
|
||||
}) {
|
||||
const { mode } = (viewHelperProps || {});
|
||||
const isPropertyMode = mode === 'properties';
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const { visible } =
|
||||
accessPath ? useFieldOptions(accessPath, schemaState) : { visible: true };
|
||||
|
||||
if (!accessPath || isPropertyMode)
|
||||
listenDepChanges(accessPath, field, visible, schemaState);
|
||||
|
||||
// Check whether form is kept hidden by visible prop.
|
||||
// We don't support inline-view in 'property' mode
|
||||
if((!_.isUndefined(visible) && !visible) || isPropertyMode) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Grid container spacing={0} className='FormView-controlRow' rowGap="8px">
|
||||
{children}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
InlineView.propTypes = {
|
||||
accessPath: PropTypes.array,
|
||||
field: PropTypes.object,
|
||||
children : PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node
|
||||
])
|
||||
};
|
||||
|
||||
registerView(InlineView, 'InlineView');
|
@@ -7,22 +7,38 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
|
||||
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString,
|
||||
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio, FormButton, InputTree
|
||||
} from '../components/FormComponents';
|
||||
import Privilege from '../components/Privilege';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import { SelectRefresh } from '../components/SelectRefresh';
|
||||
|
||||
import {
|
||||
FormButton, FormInputCheckbox, FormInputColor, FormInputDateTimePicker,
|
||||
FormInputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold,
|
||||
FormInputSQL, FormInputSelect, FormInputSelectThemes, FormInputSwitch,
|
||||
FormInputText, FormInputToggle, FormNote, InputCheckbox, InputDateTimePicker,
|
||||
InputFileSelect, InputRadio, InputSQL,InputSelect, InputSwitch, InputText,
|
||||
InputTree, PlainString,
|
||||
} from 'sources/components/FormComponents';
|
||||
import { SelectRefresh } from 'sources/components/SelectRefresh';
|
||||
import Privilege from 'sources/components/Privilege';
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import { isValueEqual } from './common';
|
||||
import {
|
||||
useFieldOptions, useFieldValue, useFieldError
|
||||
} from './hooks';
|
||||
import { listenDepChanges } from './utils';
|
||||
|
||||
|
||||
/* Control mapping for form view */
|
||||
function MappedFormControlBase({ type, value, id, onChange, className, visible, inputRef, noLabel, onClick, withContainer, controlGridBasis, ...props }) {
|
||||
const name = id;
|
||||
function MappedFormControlBase({
|
||||
id, type, state, onChange, className, inputRef, visible,
|
||||
withContainer, controlGridBasis, noLabel, ...props
|
||||
}) {
|
||||
let name = id;
|
||||
const onTextChange = useCallback((e) => {
|
||||
let val = e;
|
||||
if(e?.target) {
|
||||
@@ -30,6 +46,7 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible,
|
||||
}
|
||||
onChange?.(val);
|
||||
}, []);
|
||||
const value = state;
|
||||
|
||||
const onSqlChange = useCallback((changedValue) => {
|
||||
onChange?.(changedValue);
|
||||
@@ -43,58 +60,114 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible,
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (name && _.isNumber(name)) {
|
||||
name = String('name');
|
||||
}
|
||||
|
||||
/* The mapping uses Form* components as it comes with labels */
|
||||
switch (type) {
|
||||
case 'int':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='int' />;
|
||||
return <FormInputText
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props} type='int'
|
||||
/>;
|
||||
case 'numeric':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='numeric' />;
|
||||
return <FormInputText
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props} type='numeric'
|
||||
/>;
|
||||
case 'tel':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} type='tel' />;
|
||||
return <FormInputText
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props} type='tel'
|
||||
/>;
|
||||
case 'text':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />;
|
||||
return <FormInputText
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props}
|
||||
/>;
|
||||
case 'multiline':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} controlProps={{ multiline: true }} {...props} />;
|
||||
return <FormInputText
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} controlProps={{ multiline: true }} {...props}
|
||||
/>;
|
||||
case 'password':
|
||||
return <FormInputText name={name} value={value} onChange={onTextChange} className={className} type='password' inputRef={inputRef} {...props} />;
|
||||
return <FormInputText
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
type='password' inputRef={inputRef} {...props}
|
||||
/>;
|
||||
case 'select':
|
||||
return <FormInputSelect name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />;
|
||||
return <FormInputSelect
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props}
|
||||
/>;
|
||||
case 'select-refresh':
|
||||
return <SelectRefresh name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||
return <SelectRefresh
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
{...props}
|
||||
/>;
|
||||
case 'switch':
|
||||
return <FormInputSwitch name={name} value={value}
|
||||
onChange={(e) => onTextChange(e.target.checked, e.target.name)} className={className}
|
||||
return <FormInputSwitch
|
||||
name={name} value={value} className={className}
|
||||
onChange={(e) => onTextChange(e.target.checked, e.target.name)}
|
||||
withContainer={withContainer} controlGridBasis={controlGridBasis}
|
||||
{...props} />;
|
||||
{...props}
|
||||
/>;
|
||||
case 'checkbox':
|
||||
return <FormInputCheckbox name={name} value={value}
|
||||
onChange={(e) => onTextChange(e.target.checked, e.target.name)} className={className}
|
||||
{...props} />;
|
||||
return <FormInputCheckbox
|
||||
name={name} value={value} className={className}
|
||||
onChange={(e) => onTextChange(e.target.checked, e.target.name)}
|
||||
{...props}
|
||||
/>;
|
||||
case 'toggle':
|
||||
return <FormInputToggle name={name} value={value}
|
||||
onChange={onTextChange} className={className} inputRef={inputRef}
|
||||
{...props} />;
|
||||
return <FormInputToggle
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props}
|
||||
/>;
|
||||
case 'color':
|
||||
return <FormInputColor name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||
return <FormInputColor
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
{...props}
|
||||
/>;
|
||||
case 'file':
|
||||
return <FormInputFileSelect name={name} value={value} onChange={onTextChange} className={className} inputRef={inputRef} {...props} />;
|
||||
return <FormInputFileSelect
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
inputRef={inputRef} {...props}
|
||||
/>;
|
||||
case 'sql':
|
||||
return <FormInputSQL name={name} value={value} onChange={onSqlChange} className={className} noLabel={noLabel} inputRef={inputRef} {...props} />;
|
||||
return <FormInputSQL
|
||||
name={name} value={value} onChange={onSqlChange} className={className}
|
||||
noLabel={noLabel} inputRef={inputRef} {...props}
|
||||
/>;
|
||||
case 'note':
|
||||
return <FormNote className={className} {...props} />;
|
||||
case 'datetimepicker':
|
||||
return <FormInputDateTimePicker name={name} value={value} onChange={onTextChange} className={className} {...props} />;
|
||||
return <FormInputDateTimePicker
|
||||
name={name} value={value} onChange={onTextChange} className={className}
|
||||
{...props}
|
||||
/>;
|
||||
case 'keyboardShortcut':
|
||||
return <FormInputKeyboardShortcut name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||
return <FormInputKeyboardShortcut
|
||||
name={name} value={value} onChange={onTextChange} {...props}
|
||||
/>;
|
||||
case 'threshold':
|
||||
return <FormInputQueryThreshold name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||
return <FormInputQueryThreshold
|
||||
name={name} value={value} onChange={onTextChange} {...props}
|
||||
/>;
|
||||
case 'theme':
|
||||
return <FormInputSelectThemes name={name} value={value} onChange={onTextChange} {...props}/>;
|
||||
return <FormInputSelectThemes
|
||||
name={name} value={value} onChange={onTextChange} {...props}
|
||||
/>;
|
||||
case 'button':
|
||||
return <FormButton name={name} value={value} className={className} onClick={onClick} {...props} />;
|
||||
return <FormButton
|
||||
name={name} value={value} className={className} onClick={props.onClick}
|
||||
{...props}
|
||||
/>;
|
||||
case 'tree':
|
||||
return <InputTree name={name} treeData={props.treeData} onChange={onTreeSelection} {...props}/>;
|
||||
return <InputTree
|
||||
name={name} treeData={props.treeData} onChange={onTreeSelection}
|
||||
{...props}
|
||||
/>;
|
||||
default:
|
||||
return <PlainString value={value} {...props} />;
|
||||
}
|
||||
@@ -105,7 +178,7 @@ MappedFormControlBase.propTypes = {
|
||||
PropTypes.string, PropTypes.func,
|
||||
]).isRequired,
|
||||
value: PropTypes.any,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.object,
|
||||
@@ -116,12 +189,17 @@ MappedFormControlBase.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
withContainer: PropTypes.bool,
|
||||
controlGridBasis: PropTypes.number,
|
||||
treeData: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),
|
||||
treeData: PropTypes.oneOfType([
|
||||
PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]
|
||||
),
|
||||
};
|
||||
|
||||
/* Control mapping for grid cell view */
|
||||
function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, inputRef, ...props }) {
|
||||
const name = id;
|
||||
function MappedCellControlBase({
|
||||
cell, value, id, optionsLoaded, onCellChange, visible, reRenderRow, inputRef,
|
||||
...props
|
||||
}) {
|
||||
let name = id;
|
||||
const onTextChange = useCallback((e) => {
|
||||
let val = e;
|
||||
if (e?.target) {
|
||||
@@ -156,6 +234,10 @@ function MappedCellControlBase({ cell, value, id, optionsLoaded, onCellChange, v
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (name && _.isNumber(name)) {
|
||||
name = String('name');
|
||||
}
|
||||
|
||||
/* The mapping does not need Form* components as labels are not needed for grid cells */
|
||||
switch(cell) {
|
||||
case 'int':
|
||||
@@ -211,8 +293,9 @@ const ALLOWED_PROPS_FIELD_COMMON = [
|
||||
'mode', 'value', 'readonly', 'disabled', 'hasError', 'id',
|
||||
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
|
||||
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
|
||||
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName', 'hidden',
|
||||
'withContainer', 'controlGridBasis', 'hasCheckbox', 'treeData', 'labelTooltip'
|
||||
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton',
|
||||
'btnName', 'hidden', 'withContainer', 'controlGridBasis', 'hasCheckbox',
|
||||
'treeData', 'labelTooltip'
|
||||
];
|
||||
|
||||
const ALLOWED_PROPS_FIELD_FORM = [
|
||||
@@ -220,50 +303,128 @@ const ALLOWED_PROPS_FIELD_FORM = [
|
||||
];
|
||||
|
||||
const ALLOWED_PROPS_FIELD_CELL = [
|
||||
'cell', 'onCellChange', 'row', 'reRenderRow', 'validate', 'disabled', 'readonly', 'radioType', 'hideBrowseButton', 'hidden'
|
||||
'cell', 'onCellChange', 'reRenderRow', 'validate', 'disabled',
|
||||
'readonly', 'radioType', 'hideBrowseButton', 'hidden', 'row',
|
||||
];
|
||||
|
||||
export const StaticMappedFormControl = ({accessPath, field, ...props}) => {
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const state = schemaState.value(accessPath);
|
||||
const newProps = {
|
||||
...props,
|
||||
state,
|
||||
noLabel: field.isFullTab,
|
||||
...field,
|
||||
onChange: () => { /* Do nothing */ },
|
||||
};
|
||||
const visible = evalFunc(null, field.visible, state);
|
||||
|
||||
export const MappedFormControl = ({memoDeps, ...props}) => {
|
||||
let newProps = { ...props };
|
||||
let typeProps = evalFunc(null, newProps.type, newProps.state);
|
||||
if (typeof (typeProps) === 'object') {
|
||||
if (visible === false) return <></>;
|
||||
|
||||
return useMemo(
|
||||
() => <MappedFormControlBase
|
||||
{
|
||||
..._.pick(
|
||||
newProps,
|
||||
_.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM)
|
||||
)
|
||||
}
|
||||
/>, []
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const MappedFormControl = ({
|
||||
accessPath, dataDispatch, field, onChange, ...props
|
||||
}) => {
|
||||
const checkIsMounted = useIsMounted();
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const state = schemaState.data;
|
||||
const avoidRenderingWhenNotMounted = (newKey) => {
|
||||
if (checkIsMounted()) {
|
||||
setKey(newKey);
|
||||
}
|
||||
};
|
||||
const value = useFieldValue(
|
||||
accessPath, schemaState, key, avoidRenderingWhenNotMounted
|
||||
);
|
||||
const options = useFieldOptions(
|
||||
accessPath, schemaState, key, avoidRenderingWhenNotMounted
|
||||
);
|
||||
const { hasError } = useFieldError(
|
||||
accessPath, schemaState, key, avoidRenderingWhenNotMounted
|
||||
);
|
||||
|
||||
const origOnChange = onChange;
|
||||
|
||||
onChange = (changedValue) => {
|
||||
if (!origOnChange || !checkIsMounted()) return;
|
||||
|
||||
// We don't want the 'onChange' to be executed for the same value to avoid
|
||||
// rerendering of the control, top component may still be rerendered on the
|
||||
// change of the value.
|
||||
const currValue = schemaState.value(accessPath);
|
||||
|
||||
if (!isValueEqual(changedValue, currValue)) origOnChange(changedValue);
|
||||
};
|
||||
|
||||
listenDepChanges(accessPath, field, options.visible, schemaState);
|
||||
|
||||
let newProps = {
|
||||
...props,
|
||||
state: value,
|
||||
noLabel: field.isFullTab,
|
||||
...field,
|
||||
onChange: onChange,
|
||||
dataDispatch: dataDispatch,
|
||||
...options,
|
||||
hasError,
|
||||
};
|
||||
|
||||
if (typeof (field.type) === 'function') {
|
||||
const typeProps = evalFunc(null, field.type, state);
|
||||
newProps = {
|
||||
...newProps,
|
||||
...typeProps,
|
||||
};
|
||||
} else {
|
||||
newProps.type = typeProps;
|
||||
}
|
||||
|
||||
let origOnClick = newProps.onClick;
|
||||
newProps.onClick = ()=>{
|
||||
origOnClick?.();
|
||||
/* Consider on click as change for button.
|
||||
Just increase state val by 1 to inform the deps and self depChange */
|
||||
newProps.onChange?.((newProps.state[props.id]||0)+1);
|
||||
};
|
||||
// FIXME:: Get this list from the option registry.
|
||||
const memDeps = ['disabled', 'visible', 'readonly'].map(
|
||||
option => options[option]
|
||||
);
|
||||
memDeps.push(value);
|
||||
memDeps.push(hasError);
|
||||
memDeps.push(key);
|
||||
memDeps.push(JSON.stringify(accessPath));
|
||||
|
||||
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */
|
||||
return useMemo(()=><MappedFormControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM))} />, memoDeps??[]);
|
||||
// Filter out garbage props if any using ALLOWED_PROPS_FIELD.
|
||||
return useMemo(
|
||||
() => <MappedFormControlBase
|
||||
{
|
||||
..._.pick(
|
||||
newProps,
|
||||
_.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_FORM)
|
||||
)
|
||||
}
|
||||
/>, [...memDeps]
|
||||
);
|
||||
};
|
||||
|
||||
MappedFormControl.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
export const MappedCellControl = (props) => {
|
||||
let newProps = { ...props };
|
||||
let cellProps = evalFunc(null, newProps.cell, newProps.row.original);
|
||||
if (typeof (cellProps) === 'object') {
|
||||
newProps = {
|
||||
...newProps,
|
||||
...cellProps,
|
||||
};
|
||||
} else {
|
||||
newProps.cell = cellProps;
|
||||
}
|
||||
const newProps = _.pick(
|
||||
props, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL)
|
||||
);;
|
||||
|
||||
/* Filter out garbage props if any using ALLOWED_PROPS_FIELD */
|
||||
return <MappedCellControlBase {..._.pick(newProps, _.union(ALLOWED_PROPS_FIELD_COMMON, ALLOWED_PROPS_FIELD_CELL))} />;
|
||||
// Filter out garbage props if any using ALLOWED_PROPS_FIELD.
|
||||
return <MappedCellControlBase {...newProps}/>;
|
||||
};
|
||||
|
42
web/pgadmin/static/js/SchemaView/ResetButton.jsx
Normal file
42
web/pgadmin/static/js/SchemaView/ResetButton.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { DefaultButton } from 'sources/components/Buttons';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
|
||||
|
||||
export function ResetButton({label, Icon, onClick}) {
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const checkDisabled = (state) => (state.isSaving || !state.isDirty);
|
||||
const currState = schemaState.state();
|
||||
const isDisabled = checkDisabled(currState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!schemaState) return;
|
||||
|
||||
const refreshOnDisableStateChanged = (newState) => {
|
||||
if (isDisabled !== checkDisabled(newState)) setKey(Date.now());
|
||||
};
|
||||
|
||||
return schemaState.subscribe([], refreshOnDisableStateChanged, 'states');
|
||||
}, [key]);
|
||||
|
||||
return (
|
||||
<DefaultButton
|
||||
data-test='Reset' onClick={onClick}
|
||||
startIcon={Icon}
|
||||
disabled={isDisabled}
|
||||
className='Dialog-buttonMargin'>
|
||||
{ label }
|
||||
</DefaultButton>
|
||||
);
|
||||
}
|
45
web/pgadmin/static/js/SchemaView/SQLTab.jsx
Normal file
45
web/pgadmin/static/js/SchemaView/SQLTab.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { InputSQL } from 'sources/components/FormComponents';
|
||||
|
||||
|
||||
// Optional SQL tab.
|
||||
export function SQLTab({active, getSQLValue}) {
|
||||
const [sql, setSql] = useState('Loading...');
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
if(active) {
|
||||
setSql('Loading...');
|
||||
getSQLValue().then((value) => {
|
||||
if(!unmounted) {
|
||||
setSql(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return () => {unmounted=true;};
|
||||
}, [active]);
|
||||
|
||||
return <InputSQL
|
||||
value={sql}
|
||||
options={{
|
||||
readOnly: true,
|
||||
}}
|
||||
readonly={true}
|
||||
className='FormView-sqlTabInput'
|
||||
/>;
|
||||
}
|
||||
|
||||
SQLTab.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
getSQLValue: PropTypes.func.isRequired,
|
||||
};
|
50
web/pgadmin/static/js/SchemaView/SaveButton.jsx
Normal file
50
web/pgadmin/static/js/SchemaView/SaveButton.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { PrimaryButton } from 'sources/components/Buttons';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
|
||||
|
||||
export function SaveButton({
|
||||
label, Icon, checkDirtyOnEnableSave, onClick, mode,
|
||||
}) {
|
||||
const [key, setKey] = useState(0);
|
||||
const schemaState = useContext(SchemaStateContext);
|
||||
const checkDisabled = (state) => {
|
||||
const {isDirty, isSaving, errors} = state;
|
||||
return (
|
||||
isSaving ||
|
||||
!(mode === 'edit' || checkDirtyOnEnableSave ? isDirty : true) ||
|
||||
Boolean(errors.name)
|
||||
);
|
||||
};
|
||||
const currState = schemaState.state();
|
||||
const isDisabled = checkDisabled(currState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!schemaState) return;
|
||||
|
||||
const refreshOnDisableStateChanged = (newState) => {
|
||||
if (isDisabled !== checkDisabled(newState)) setKey(Date.now());
|
||||
};
|
||||
|
||||
return schemaState.subscribe([], refreshOnDisableStateChanged, 'states');
|
||||
}, [key]);
|
||||
|
||||
return (
|
||||
<PrimaryButton
|
||||
data-test='Save' onClick={onClick} startIcon={Icon}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{label}
|
||||
</PrimaryButton>
|
||||
);
|
||||
}
|
@@ -7,9 +7,7 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
@@ -25,23 +23,21 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { parseApiError } from 'sources/api_instance';
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import Loader from 'sources/components/Loader';
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
import {
|
||||
PrimaryButton, DefaultButton, PgIconButton
|
||||
DefaultButton, PgIconButton
|
||||
} from 'sources/components/Buttons';
|
||||
import {
|
||||
FormFooterMessage, MESSAGE_TYPE
|
||||
} from 'sources/components/FormComponents';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import { FormLoader } from './FormLoader';
|
||||
import FormView from './FormView';
|
||||
import { ResetButton } from './ResetButton';
|
||||
import { SaveButton } from './SaveButton';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import { StyledBox } from './StyledComponents';
|
||||
import { useSchemaState } from './useSchemaState';
|
||||
import {
|
||||
getForQueryParams, SchemaStateContext
|
||||
} from './common';
|
||||
import { useSchemaState } from './hooks';
|
||||
import { getForQueryParams } from './common';
|
||||
|
||||
|
||||
/* If its the dialog */
|
||||
@@ -50,30 +46,22 @@ export default function SchemaDialogView({
|
||||
isTabView=true, checkDirtyOnEnableSave=false, ...props
|
||||
}) {
|
||||
// View helper properties
|
||||
const { mode, keepCid } = viewHelperProps;
|
||||
const onDataChange = props.onDataChange;
|
||||
|
||||
// Message to the user on long running operations.
|
||||
const [loaderText, setLoaderText] = useState('');
|
||||
|
||||
// Schema data state manager
|
||||
const {schemaState, dataDispatch, sessData, reset} = useSchemaState({
|
||||
const {schemaState, dataDispatch, reset} = useSchemaState({
|
||||
schema: schema, getInitData: getInitData, immutableData: {},
|
||||
mode: mode, keepCid: keepCid, onDataChange: onDataChange,
|
||||
});
|
||||
|
||||
const [{isNew, isDirty, isReady, errors}, updateSchemaState] = useState({
|
||||
isNew: true, isDirty: false, isReady: false, errors: {}
|
||||
viewHelperProps: viewHelperProps, onDataChange: onDataChange,
|
||||
loadingText,
|
||||
});
|
||||
|
||||
// Is saving operation in progress?
|
||||
const [saving, setSaving] = useState(false);
|
||||
const setSaving = (val) => schemaState.isSaving = val;
|
||||
const setLoaderText = (val) => schemaState.setMessage(val);
|
||||
|
||||
// First element to be set by the FormView to set the focus after loading
|
||||
// the data.
|
||||
const firstEleRef = useRef();
|
||||
const checkIsMounted = useIsMounted();
|
||||
const [data, setData] = useState({});
|
||||
|
||||
// Notifier object.
|
||||
const pgAdmin = usePgAdmin();
|
||||
@@ -84,7 +72,6 @@ export default function SchemaDialogView({
|
||||
* Docker on load focusses itself, so our focus should execute later.
|
||||
*/
|
||||
let focusTimeout = setTimeout(()=>{
|
||||
firstEleRef.current?.focus();
|
||||
}, 250);
|
||||
|
||||
// Clear the focus timeout if unmounted.
|
||||
@@ -93,24 +80,13 @@ export default function SchemaDialogView({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoaderText(schemaState.message);
|
||||
}, [schemaState.message]);
|
||||
|
||||
useEffect(() => {
|
||||
setData(sessData);
|
||||
updateSchemaState(schemaState);
|
||||
}, [sessData.__changeId]);
|
||||
|
||||
useEffect(()=>{
|
||||
if (!props.resetKey) return;
|
||||
reset();
|
||||
}, [props.resetKey]);
|
||||
|
||||
|
||||
const onResetClick = () => {
|
||||
const resetIt = () => {
|
||||
firstEleRef.current?.focus();
|
||||
reset();
|
||||
return true;
|
||||
};
|
||||
@@ -128,7 +104,7 @@ export default function SchemaDialogView({
|
||||
};
|
||||
|
||||
const save = (changeData) => {
|
||||
props.onSave(isNew, changeData)
|
||||
props.onSave(schemaState.isNew, changeData)
|
||||
.then(()=>{
|
||||
if(schema.informText) {
|
||||
Notifier.alert(
|
||||
@@ -151,20 +127,23 @@ export default function SchemaDialogView({
|
||||
|
||||
const onSaveClick = () => {
|
||||
// Do nothing when there is no change or there is an error
|
||||
if (!schemaState.changes || errors.name) return;
|
||||
if (
|
||||
!schemaState._changes || Object.keys(schemaState._changes) === 0 ||
|
||||
schemaState.errors.name
|
||||
) return;
|
||||
|
||||
setSaving(true);
|
||||
setLoaderText('Saving...');
|
||||
|
||||
if (!schema.warningText) {
|
||||
save(schemaState.Changes(true));
|
||||
save(schemaState.changes(true));
|
||||
return;
|
||||
}
|
||||
|
||||
Notifier.confirm(
|
||||
gettext('Warning'),
|
||||
schema.warningText,
|
||||
()=> { save(schemaState.Changes(true)); },
|
||||
() => { save(schemaState.changes(true)); },
|
||||
() => {
|
||||
setSaving(false);
|
||||
setLoaderText('');
|
||||
@@ -173,29 +152,22 @@ export default function SchemaDialogView({
|
||||
);
|
||||
};
|
||||
|
||||
const onErrClose = useCallback(() => {
|
||||
const err = { ...errors, message: '' };
|
||||
// Unset the error message, but not the name.
|
||||
schemaState.setError(err);
|
||||
updateSchemaState({isNew, isDirty, isReady, errors: err});
|
||||
});
|
||||
|
||||
const getSQLValue = () => {
|
||||
// Called when SQL tab is active.
|
||||
if(!isDirty) {
|
||||
if(!schemaState.isDirty) {
|
||||
return Promise.resolve('-- ' + gettext('No updates.'));
|
||||
}
|
||||
|
||||
if(errors.name) {
|
||||
if(schemaState.errors.name) {
|
||||
return Promise.resolve('-- ' + gettext('Definition incomplete.'));
|
||||
}
|
||||
|
||||
const changeData = schemaState.changes;
|
||||
const changeData = schemaState._changes;
|
||||
/*
|
||||
* Call the passed incoming getSQLValue func to get the SQL
|
||||
* return of getSQLValue should be a promise.
|
||||
*/
|
||||
return props.getSQLValue(isNew, getForQueryParams(changeData));
|
||||
return props.getSQLValue(schemaState.isNew, getForQueryParams(changeData));
|
||||
};
|
||||
|
||||
const getButtonIcon = () => {
|
||||
@@ -207,29 +179,24 @@ export default function SchemaDialogView({
|
||||
return <SaveIcon />;
|
||||
};
|
||||
|
||||
const disableSaveBtn = saving ||
|
||||
!isReady ||
|
||||
!(mode === 'edit' || checkDirtyOnEnableSave ? isDirty : true) ||
|
||||
Boolean(errors.name && errors.name !== 'apierror');
|
||||
|
||||
let ButtonIcon = getButtonIcon();
|
||||
|
||||
/* I am Groot */
|
||||
return (
|
||||
return useMemo(() =>
|
||||
<StyledBox>
|
||||
<SchemaStateContext.Provider value={schemaState}>
|
||||
<Box className='Dialog-form'>
|
||||
<Loader message={loaderText || loadingText}/>
|
||||
<FormView value={data}
|
||||
<FormLoader/>
|
||||
<FormView
|
||||
viewHelperProps={viewHelperProps}
|
||||
schema={schema} accessPath={[]}
|
||||
dataDispatch={dataDispatch}
|
||||
hasSQLTab={props.hasSQL} getSQLValue={getSQLValue}
|
||||
firstEleRef={firstEleRef} isTabView={isTabView}
|
||||
className={props.formClassName} />
|
||||
<FormFooterMessage
|
||||
type={MESSAGE_TYPE.ERROR} message={errors?.message}
|
||||
onClose={onErrClose} />
|
||||
isTabView={isTabView}
|
||||
className={props.formClassName}
|
||||
showError={true} resetKey={props.resetKey}
|
||||
focusOnFirstInput={true}
|
||||
/>
|
||||
</Box>
|
||||
{showFooter &&
|
||||
<Box className='Dialog-footer'>
|
||||
@@ -237,13 +204,13 @@ export default function SchemaDialogView({
|
||||
(!props.disableSqlHelp || !props.disableDialogHelp) &&
|
||||
<Box>
|
||||
<PgIconButton data-test='sql-help'
|
||||
onClick={()=>props.onHelp(true, isNew)}
|
||||
onClick={()=>props.onHelp(true, schemaState.isNew)}
|
||||
icon={<InfoIcon />} disabled={props.disableSqlHelp}
|
||||
className='Dialog-buttonMargin'
|
||||
title={ gettext('SQL help for this object type.') }
|
||||
/>
|
||||
<PgIconButton data-test='dialog-help'
|
||||
onClick={()=>props.onHelp(false, isNew)}
|
||||
onClick={()=>props.onHelp(false, schemaState.isNew)}
|
||||
icon={<HelpIcon />} disabled={props.disableDialogHelp}
|
||||
title={ gettext('Help for this dialog.') }
|
||||
/>
|
||||
@@ -254,23 +221,21 @@ export default function SchemaDialogView({
|
||||
startIcon={<CloseIcon />} className='Dialog-buttonMargin'>
|
||||
{ gettext('Close') }
|
||||
</DefaultButton>
|
||||
<DefaultButton data-test='Reset' onClick={onResetClick}
|
||||
startIcon={<SettingsBackupRestoreIcon />}
|
||||
disabled={(!isDirty) || saving }
|
||||
className='Dialog-buttonMargin'>
|
||||
{ gettext('Reset') }
|
||||
</DefaultButton>
|
||||
<PrimaryButton data-test='Save' onClick={onSaveClick}
|
||||
startIcon={ButtonIcon}
|
||||
disabled={disableSaveBtn}>{
|
||||
props.customSaveBtnName || gettext('Save')
|
||||
}
|
||||
</PrimaryButton>
|
||||
<ResetButton
|
||||
onClick={onResetClick}
|
||||
icon={<SettingsBackupRestoreIcon />}
|
||||
label={ gettext('Reset') }/>
|
||||
<SaveButton
|
||||
onClick={onSaveClick} Icon={ButtonIcon}
|
||||
label={props.customSaveBtnName || gettext('Save')}
|
||||
checkDirtyOnEnableSave={checkDirtyOnEnableSave}
|
||||
mode={viewHelperProps.mode}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
</SchemaStateContext.Provider>
|
||||
</StyledBox>
|
||||
</StyledBox>, [schema._id, viewHelperProps.mode]
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import InfoIcon from '@mui/icons-material/InfoRounded';
|
||||
@@ -16,179 +16,95 @@ import Box from '@mui/material/Box';
|
||||
import Accordion from '@mui/material/Accordion';
|
||||
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { usePgAdmin } from 'sources/BrowserComponent';
|
||||
import gettext from 'sources/gettext';
|
||||
import Loader from 'sources/components/Loader';
|
||||
import { PgIconButton, PgButtonGroup } from 'sources/components/Buttons';
|
||||
import CustomPropTypes from 'sources/custom_prop_types';
|
||||
|
||||
import DataGridView from './DataGridView';
|
||||
import FieldSetView from './FieldSetView';
|
||||
import { MappedFormControl } from './MappedControl';
|
||||
import { useSchemaState } from './useSchemaState';
|
||||
import { getFieldMetaData } from './common';
|
||||
|
||||
import { FieldControl } from './FieldControl';
|
||||
import { FormLoader } from './FormLoader';
|
||||
import { SchemaStateContext } from './SchemaState';
|
||||
import { StyledBox } from './StyledComponents';
|
||||
import { useSchemaState } from './hooks';
|
||||
import { createFieldControls } from './utils';
|
||||
|
||||
|
||||
/* If its the properties tab */
|
||||
export default function SchemaPropertiesView({
|
||||
getInitData, viewHelperProps, schema={}, updatedData, ...props
|
||||
}) {
|
||||
let defaultTab = 'General';
|
||||
let tabs = {};
|
||||
let tabsClassname = {};
|
||||
let groupLabels = {};
|
||||
const [loaderText, setLoaderText] = useState('');
|
||||
|
||||
const pgAdmin = usePgAdmin();
|
||||
const Notifier = pgAdmin.Browser.notifier;
|
||||
const { mode, keepCid } = viewHelperProps;
|
||||
|
||||
// Schema data state manager
|
||||
const {schemaState, sessData} = useSchemaState({
|
||||
const {schemaState} = useSchemaState({
|
||||
schema: schema, getInitData: getInitData, immutableData: updatedData,
|
||||
mode: mode, keepCid: keepCid, onDataChange: null,
|
||||
viewHelperProps: viewHelperProps, onDataChange: null,
|
||||
});
|
||||
const [data, setData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if (schemaState.errors?.response)
|
||||
Notifier.pgRespErrorNotify(schemaState.errors.response);
|
||||
}, [schemaState.errors?.name]);
|
||||
|
||||
useEffect(() => {
|
||||
setData(sessData);
|
||||
}, [sessData.__changeId]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoaderText(schemaState.message);
|
||||
}, [schemaState.message]);
|
||||
|
||||
/* A simple loop to get all the controls for the fields */
|
||||
schema.fields.forEach((field) => {
|
||||
let {group} = field;
|
||||
const {
|
||||
visible, disabled, readonly, modeSupported
|
||||
} = getFieldMetaData(field, schema, data, viewHelperProps);
|
||||
group = group || defaultTab;
|
||||
|
||||
if(field.isFullTab) {
|
||||
tabsClassname[group] = 'Properties-noPadding';
|
||||
}
|
||||
|
||||
if(!modeSupported) return;
|
||||
|
||||
group = groupLabels[group] || group || defaultTab;
|
||||
if (field.helpMessageMode?.indexOf(viewHelperProps.mode) == -1)
|
||||
field.helpMessage = '';
|
||||
|
||||
if(!tabs[group]) tabs[group] = [];
|
||||
|
||||
if(field && field.type === 'nested-fieldset') {
|
||||
tabs[group].push(
|
||||
<FieldSetView
|
||||
key={`nested${tabs[group].length}`}
|
||||
value={data}
|
||||
viewHelperProps={viewHelperProps}
|
||||
schema={field.schema}
|
||||
accessPath={[]}
|
||||
controlClassName='Properties-controlRow'
|
||||
{...field}
|
||||
visible={visible}
|
||||
/>
|
||||
);
|
||||
} else if(field.type === 'collection') {
|
||||
tabs[group].push(
|
||||
<DataGridView
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
name={field.id}
|
||||
value={data[field.id] || []}
|
||||
schema={field.schema}
|
||||
accessPath={[field.id]}
|
||||
containerClassName='Properties-controlRow'
|
||||
canAdd={false}
|
||||
canEdit={false}
|
||||
canDelete={false}
|
||||
visible={visible}
|
||||
/>
|
||||
);
|
||||
} else if(field.type === 'group') {
|
||||
groupLabels[field.id] = field.label;
|
||||
|
||||
if(!visible) {
|
||||
schema.filterGroups.push(field.label);
|
||||
}
|
||||
} else {
|
||||
tabs[group].push(
|
||||
<MappedFormControl
|
||||
key={field.id}
|
||||
viewHelperProps={viewHelperProps}
|
||||
state={sessData}
|
||||
name={field.id}
|
||||
value={data[field.id]}
|
||||
{...field}
|
||||
readonly={readonly}
|
||||
disabled={disabled}
|
||||
visible={visible}
|
||||
className={field.isFullTab ? null :'Properties-controlRow'}
|
||||
noLabel={field.isFullTab}
|
||||
memoDeps={[
|
||||
data[field.id],
|
||||
'Properties-controlRow',
|
||||
field.isFullTab
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let finalTabs = _.pickBy(
|
||||
tabs, (v, tabName) => schema.filterGroups.indexOf(tabName) <= -1
|
||||
const finalTabs = useMemo(
|
||||
() => createFieldControls({
|
||||
schema, schemaState, viewHelperProps, dataDispatch: null, accessPath: []
|
||||
}),
|
||||
[schema._id, schemaState, viewHelperProps]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledBox>
|
||||
<Loader message={loaderText}/>
|
||||
<Box className='Properties-toolbar'>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton
|
||||
data-test="help" onClick={() => props.onHelp(true, false)}
|
||||
icon={<InfoIcon />} disabled={props.disableSqlHelp}
|
||||
title="SQL help for this object type." />
|
||||
<PgIconButton data-test="edit"
|
||||
onClick={props.onEdit} icon={<EditIcon />}
|
||||
title={gettext('Edit object...')} />
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
<Box className={'Properties-form'}>
|
||||
<Box>
|
||||
{Object.keys(finalTabs).map((tabName)=>{
|
||||
let id = tabName.replace(' ', '');
|
||||
return (
|
||||
<Accordion key={id}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`${id}-content`}
|
||||
id={`${id}-header`}
|
||||
>
|
||||
{tabName}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={tabsClassname[tabName]}>
|
||||
<Box style={{width: '100%'}}>
|
||||
{finalTabs[tabName]}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
if (!finalTabs) return <></>;
|
||||
|
||||
return useMemo(
|
||||
() => <StyledBox>
|
||||
<SchemaStateContext.Provider value={schemaState}>
|
||||
<FormLoader/>
|
||||
<Box className='Properties-toolbar'>
|
||||
<PgButtonGroup size="small">
|
||||
<PgIconButton
|
||||
data-test="help" onClick={() => props.onHelp(true, false)}
|
||||
icon={<InfoIcon />} disabled={props.disableSqlHelp}
|
||||
title="SQL help for this object type." />
|
||||
<PgIconButton data-test="edit"
|
||||
onClick={props.onEdit} icon={<EditIcon />}
|
||||
title={gettext('Edit object...')} />
|
||||
</PgButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledBox>
|
||||
<Box className={'Properties-form'}>
|
||||
<Box>
|
||||
{finalTabs.map((group)=>{
|
||||
let id = group.id.replace(' ', '');
|
||||
return (
|
||||
<Accordion key={id}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`${id}-content`}
|
||||
id={`${id}-header`}
|
||||
>
|
||||
{group.label}
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={group.className}>
|
||||
<Box style={{width: '100%'}}>
|
||||
{
|
||||
group.controls.map(
|
||||
(item, idx) => <FieldControl
|
||||
item={item} key={idx} schemaId={schema._id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</SchemaStateContext.Provider>
|
||||
</StyledBox>,
|
||||
[schema._id]
|
||||
);
|
||||
}
|
||||
|
||||
|
330
web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js
Normal file
330
web/pgadmin/static/js/SchemaView/SchemaState/SchemaState.js
Normal file
@@ -0,0 +1,330 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { parseApiError } from 'sources/api_instance';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import { prepareData } from '../common';
|
||||
import { DepListener } from '../DepListener';
|
||||
import { FIELD_OPTIONS, schemaOptionsEvalulator } from '../options';
|
||||
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS,
|
||||
flatPathGenerator,
|
||||
getSchemaDataDiff,
|
||||
validateSchema,
|
||||
} from './common';
|
||||
import { createStore } from './store';
|
||||
|
||||
|
||||
export const LOADING_STATE = {
|
||||
INIT: 'initialising',
|
||||
LOADING: 'loading',
|
||||
LOADED: 'loaded',
|
||||
ERROR: 'Error'
|
||||
};
|
||||
|
||||
const PATH_SEPARATOR = '/';
|
||||
|
||||
export class SchemaState extends DepListener {
|
||||
constructor(
|
||||
schema, getInitData, immutableData, onDataChange, viewHelperProps,
|
||||
loadingText
|
||||
) {
|
||||
super();
|
||||
|
||||
////// Helper variables
|
||||
|
||||
// BaseUISchema instance
|
||||
this.schema = schema;
|
||||
this.viewHelperProps = viewHelperProps;
|
||||
// Current mode of operation ('create', 'edit', 'properties')
|
||||
this.mode = viewHelperProps.mode;
|
||||
// Keep the 'cid' object during diff calculations.
|
||||
this.keepcid = viewHelperProps.keepCid;
|
||||
// Initialization callback
|
||||
this.getInitData = getInitData;
|
||||
// Data change callback
|
||||
this.onDataChange = onDataChange;
|
||||
|
||||
////// State variables
|
||||
|
||||
// Diff between the current snapshot and initial data.
|
||||
// Internal state for keeping the changes
|
||||
this._changes = {};
|
||||
// Current Loading state
|
||||
this.loadingState = LOADING_STATE.INIT;
|
||||
this.customLoadingText = loadingText;
|
||||
|
||||
////// Schema instance data
|
||||
|
||||
// Initial data after the ready state
|
||||
this.initData = {};
|
||||
|
||||
// Immutable data
|
||||
this.immutableData = immutableData;
|
||||
// Pre-ready queue
|
||||
this.preReadyQueue = [];
|
||||
|
||||
this.optionStore = createStore({});
|
||||
this.dataStore = createStore({});
|
||||
this.stateStore = createStore({
|
||||
isNew: true, isDirty: false, isReady: false,
|
||||
isSaving: false, errors: {},
|
||||
message: '',
|
||||
});
|
||||
|
||||
// Memoize the path using flatPathGenerator
|
||||
this.__pathGenerator = flatPathGenerator(PATH_SEPARATOR);
|
||||
|
||||
this._id = Date.now();
|
||||
}
|
||||
|
||||
updateOptions() {
|
||||
let options = _.cloneDeep(this.optionStore.getState());
|
||||
|
||||
schemaOptionsEvalulator({
|
||||
schema: this.schema, data: this.data, options: options,
|
||||
viewHelperProps: this.viewHelperProps,
|
||||
});
|
||||
|
||||
this.optionStore.setState(options);
|
||||
}
|
||||
|
||||
setState(state, value) {
|
||||
this.stateStore.set((prev) => _.set(prev, [].concat(state), value));
|
||||
}
|
||||
|
||||
setError(err) {
|
||||
this.setState('errors', err);
|
||||
}
|
||||
|
||||
get errors() {
|
||||
return this.stateStore.get(['errors']);
|
||||
}
|
||||
|
||||
set errors(val) {
|
||||
throw new Error('Property \'errors\' is readonly.', val);
|
||||
}
|
||||
|
||||
get isReady() {
|
||||
return this.stateStore.get(['isReady']);
|
||||
}
|
||||
|
||||
setReady(val) {
|
||||
this.setState('isReady', val);
|
||||
}
|
||||
|
||||
get isSaving() {
|
||||
return this.stateStore.get(['isSaving']);
|
||||
}
|
||||
|
||||
set isSaving(val) {
|
||||
this.setState('isSaving', val);
|
||||
}
|
||||
|
||||
get loadingMessage() {
|
||||
return this.stateStore.get(['message']);
|
||||
}
|
||||
|
||||
setLoadingState(loadingState) {
|
||||
this.loadingState = loadingState;
|
||||
}
|
||||
|
||||
setMessage(msg) {
|
||||
this.setState('message', msg);
|
||||
}
|
||||
|
||||
// Initialise the data, and fetch the data from the backend (if required).
|
||||
// 'force' flag can be used for reloading the data from the backend.
|
||||
initialise(dataDispatch, force) {
|
||||
let state = this;
|
||||
|
||||
// Don't attempt to initialize again (if it's already in progress).
|
||||
if (
|
||||
state.loadingState !== LOADING_STATE.INIT ||
|
||||
(force && state.loadingState === LOADING_STATE.LOADING)
|
||||
) return;
|
||||
|
||||
state.setLoadingState(LOADING_STATE.LOADING);
|
||||
state.setMessage(state.customLoadingText || gettext('Loading...'));
|
||||
|
||||
/*
|
||||
* Fetch the data using getInitData(..) callback.
|
||||
* `getInitData(..)` must be present in 'edit' mode.
|
||||
*/
|
||||
if(state.mode === 'edit' && !state.getInitData) {
|
||||
throw new Error('getInitData must be passed for edit');
|
||||
}
|
||||
|
||||
const initDataPromise = state.getInitData?.() ||
|
||||
Promise.resolve({});
|
||||
|
||||
initDataPromise.then((data) => {
|
||||
data = data || {};
|
||||
|
||||
if(state.mode === 'edit') {
|
||||
// Set the origData to incoming data, useful for comparing.
|
||||
state.initData = prepareData({...data, ...state.immutableData});
|
||||
} else {
|
||||
// In create mode, merge with defaults.
|
||||
state.initData = prepareData({
|
||||
...state.schema.defaults, ...data, ...state.immutableData
|
||||
}, true);
|
||||
}
|
||||
|
||||
state.schema.initialise(state.initData);
|
||||
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: state.initData,
|
||||
});
|
||||
|
||||
state.setLoadingState(LOADING_STATE.LOADED);
|
||||
state.setMessage('');
|
||||
state.setReady(true);
|
||||
state.setState('isNew', state.schema.isNew(state.initData));
|
||||
}).catch((err) => {
|
||||
state.setMessage('');
|
||||
state.setError({
|
||||
name: 'apierror',
|
||||
response: err,
|
||||
message: _.escape(parseApiError(err)),
|
||||
});
|
||||
state.setLoadingState(LOADING_STATE.ERROR);
|
||||
state.setReady(true);
|
||||
});
|
||||
}
|
||||
|
||||
validate(sessData) {
|
||||
let state = this,
|
||||
schema = state.schema;
|
||||
|
||||
// If schema does not have the data or does not have any 'onDataChange'
|
||||
// callback, there is no need to validate the current data.
|
||||
if(!state.isReady) return;
|
||||
|
||||
if(
|
||||
!validateSchema(schema, sessData, (path, message) => {
|
||||
message && state.setError({
|
||||
name: state.accessPath(path), message: _.escape(message)
|
||||
});
|
||||
})
|
||||
) state.setError({});
|
||||
|
||||
state.data = sessData;
|
||||
state._changes = state.changes();
|
||||
state.updateOptions();
|
||||
state.onDataChange && state.onDataChange(state.isDirty, state._changes);
|
||||
}
|
||||
|
||||
changes(includeSkipChange=false) {
|
||||
const state = this;
|
||||
const sessData = state.data;
|
||||
const schema = state.schema;
|
||||
|
||||
// Check if anything changed.
|
||||
let dataDiff = getSchemaDataDiff(
|
||||
schema, state.initData, sessData,
|
||||
state.mode, state.keepCid, false, includeSkipChange
|
||||
);
|
||||
|
||||
const isDirty = Object.keys(dataDiff).length > 0;
|
||||
state.setState('isDirty', isDirty);
|
||||
|
||||
|
||||
// Inform the callbacks about change in the data.
|
||||
if(state.mode !== 'edit') {
|
||||
// Merge the changed data with origData in 'create' mode.
|
||||
dataDiff = _.assign({}, state.initData, dataDiff);
|
||||
|
||||
// Remove internal '__changeId' attribute.
|
||||
delete dataDiff.__changeId;
|
||||
|
||||
// In case of 'non-edit' mode, changes are always there.
|
||||
return dataDiff;
|
||||
}
|
||||
|
||||
if (!isDirty) return {};
|
||||
|
||||
const idAttr = schema.idAttribute;
|
||||
const idVal = state.initData[idAttr];
|
||||
|
||||
// Append 'idAttr' only if it actually exists
|
||||
if (idVal) dataDiff[idAttr] = idVal;
|
||||
|
||||
return dataDiff;
|
||||
}
|
||||
|
||||
get isNew() {
|
||||
return this.stateStore.get(['isNew']);
|
||||
}
|
||||
|
||||
set isNew(val) {
|
||||
throw new Error('Property \'isNew\' is readonly.', val);
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this.stateStore.get(['isDirty']);
|
||||
}
|
||||
|
||||
set isDirty(val) {
|
||||
throw new Error('Property \'isDirty\' is readonly.', val);
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.dataStore.getState();
|
||||
}
|
||||
|
||||
set data(_data) {
|
||||
this.dataStore.setState(_data);
|
||||
}
|
||||
|
||||
accessPath(path=[], key) {
|
||||
return this.__pathGenerator.cached(
|
||||
_.isUndefined(key) ? path : path.concat(key)
|
||||
);
|
||||
}
|
||||
|
||||
value(path) {
|
||||
if (!path || !path.length) return this.data;
|
||||
return _.get(this.data, path);
|
||||
}
|
||||
|
||||
options(path) {
|
||||
return this.optionStore.get(path.concat(FIELD_OPTIONS));
|
||||
}
|
||||
|
||||
state(_state) {
|
||||
return _state ?
|
||||
this.stateStore.get([].concat(_state)) : this.stateStore.getState();
|
||||
}
|
||||
|
||||
subscribe(path, listener, kind='options') {
|
||||
switch(kind) {
|
||||
case 'options':
|
||||
return this.optionStore.subscribeForPath(
|
||||
path.concat(FIELD_OPTIONS), listener
|
||||
);
|
||||
case 'states':
|
||||
return this.stateStore.subscribeForPath(path, listener);
|
||||
default:
|
||||
return this.dataStore.subscribeForPath(path, listener);
|
||||
}
|
||||
}
|
||||
|
||||
subscribeOption(option, path, listener) {
|
||||
return this.optionStore.subscribeForPath(
|
||||
path.concat(FIELD_OPTIONS, option), listener
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -11,13 +11,27 @@ import diffArray from 'diff-arrays-of-objects';
|
||||
import _ from 'lodash';
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
import { memoizeFn } from 'sources/utils';
|
||||
import {
|
||||
minMaxValidator, numberValidator, integerValidator, emptyValidator,
|
||||
checkUniqueCol, isEmptyString
|
||||
} from 'sources/validators';
|
||||
|
||||
import BaseUISchema from './base_schema.ui';
|
||||
import { isModeSupportedByField, isObjectEqual, isValueEqual } from './common';
|
||||
import BaseUISchema from '../base_schema.ui';
|
||||
import { isModeSupportedByField, isObjectEqual, isValueEqual } from '../common';
|
||||
|
||||
|
||||
export const SCHEMA_STATE_ACTIONS = {
|
||||
INIT: 'init',
|
||||
SET_VALUE: 'set_value',
|
||||
ADD_ROW: 'add_row',
|
||||
DELETE_ROW: 'delete_row',
|
||||
MOVE_ROW: 'move_row',
|
||||
RERENDER: 'rerender',
|
||||
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
|
||||
DEFERRED_DEPCHANGE: 'deferred_depchange',
|
||||
BULK_UPDATE: 'bulk_update',
|
||||
};
|
||||
|
||||
// Remove cid key added by prepareData
|
||||
const cleanCid = (coll, keepCid=false) => (
|
||||
@@ -276,9 +290,10 @@ export function validateSchema(
|
||||
if(schema.idAttribute === field.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the field is has nested schema, then validate the child schema.
|
||||
if(field.schema && (field.schema instanceof BaseUISchema)) {
|
||||
if (!field.schema.top) field.schema.top = schema;
|
||||
|
||||
// A collection is an array.
|
||||
if(field.type === 'collection') {
|
||||
if (validateCollectionSchema(field, sessData, accessPath, setError))
|
||||
@@ -331,3 +346,40 @@ export function validateSchema(
|
||||
sessData, (id, message) => setError(accessPath.concat(id), message)
|
||||
);
|
||||
}
|
||||
|
||||
export const getDepChange = (currPath, newState, oldState, action) => {
|
||||
if(action.depChange) {
|
||||
newState = action.depChange(currPath, newState, {
|
||||
type: action.type,
|
||||
path: action.path,
|
||||
value: action.value,
|
||||
oldState: _.cloneDeep(oldState),
|
||||
listener: action.listener,
|
||||
});
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
|
||||
// It will help us generating the flat path, and it will return the same
|
||||
// object for the same path, which will help with the React componet rendering,
|
||||
// as it uses `Object.is(...)` for the comparison of the arguments.
|
||||
export const flatPathGenerator = (separator = '.' ) => {
|
||||
const flatPathMap = new Map;
|
||||
|
||||
const setter = memoizeFn((path) => {
|
||||
const flatPath = path.join(separator);
|
||||
flatPathMap.set(flatPath, path);
|
||||
return flatPath;
|
||||
});
|
||||
|
||||
const getter = (flatPath) => {
|
||||
return flatPathMap.get(flatPath);
|
||||
};
|
||||
|
||||
return {
|
||||
flatPath: setter,
|
||||
path: getter,
|
||||
// Get the same object every time.
|
||||
cached: (path) => (getter(setter(path))),
|
||||
};
|
||||
};
|
12
web/pgadmin/static/js/SchemaView/SchemaState/context.js
Normal file
12
web/pgadmin/static/js/SchemaView/SchemaState/context.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export const SchemaStateContext = React.createContext();
|
21
web/pgadmin/static/js/SchemaView/SchemaState/index.js
Normal file
21
web/pgadmin/static/js/SchemaView/SchemaState/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { SchemaState } from './SchemaState';
|
||||
import { SchemaStateContext } from './context';
|
||||
import { SCHEMA_STATE_ACTIONS } from './common';
|
||||
import { sessDataReducer } from './reducer';
|
||||
|
||||
|
||||
export {
|
||||
SCHEMA_STATE_ACTIONS,
|
||||
SchemaState,
|
||||
SchemaStateContext,
|
||||
sessDataReducer,
|
||||
};
|
123
web/pgadmin/static/js/SchemaView/SchemaState/reducer.js
Normal file
123
web/pgadmin/static/js/SchemaView/SchemaState/reducer.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS, getDepChange,
|
||||
} from './common';
|
||||
|
||||
const getDeferredDepChange = (currPath, newState, oldState, action) => {
|
||||
if(action.deferredDepChange) {
|
||||
return action.deferredDepChange(currPath, newState, {
|
||||
type: action.type,
|
||||
path: action.path,
|
||||
value: action.value,
|
||||
depChange: action.depChange,
|
||||
oldState: _.cloneDeep(oldState),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The main function which manipulates the session state based on actions.
|
||||
*
|
||||
* The state is managed based on path array of a particular key.
|
||||
* For Eg. if the state is
|
||||
* {
|
||||
* key1: {
|
||||
* ckey1: [
|
||||
* {a: 0, b: 0},
|
||||
* {a: 1, b: 1}
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The path for b in first row will be '[key1, ckey1, 0, b]'.
|
||||
* The path for second row of ckey1 will be '[key1, ckey1, 1]'.
|
||||
*
|
||||
* The path for key1 is '[key1]'.
|
||||
* The state starts with path '[]'.
|
||||
*/
|
||||
export const sessDataReducer = (state, action) => {
|
||||
let data = _.cloneDeep(state);
|
||||
let rows, cid, deferredList;
|
||||
data.__deferred__ = data.__deferred__ || [];
|
||||
|
||||
switch(action.type) {
|
||||
case SCHEMA_STATE_ACTIONS.INIT:
|
||||
data = action.payload;
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.BULK_UPDATE:
|
||||
rows = _.get(data, action.path) || [];
|
||||
rows.forEach((row) => { row[action.id] = false; });
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.SET_VALUE:
|
||||
_.set(data, action.path, action.value);
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
deferredList = getDeferredDepChange(action.path, data, state, action);
|
||||
data.__deferred__ = deferredList || [];
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.ADD_ROW:
|
||||
// Create id to identify a row uniquely, usefull when getting diff.
|
||||
cid = _.uniqueId('c');
|
||||
action.value['cid'] = cid;
|
||||
|
||||
if (action.addOnTop) {
|
||||
rows = [].concat(action.value).concat(_.get(data, action.path) || []);
|
||||
} else {
|
||||
rows = (_.get(data, action.path) || []).concat(action.value);
|
||||
}
|
||||
|
||||
_.set(data, action.path, rows);
|
||||
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.DELETE_ROW:
|
||||
rows = _.get(data, action.path)||[];
|
||||
rows.splice(action.value, 1);
|
||||
|
||||
_.set(data, action.path, rows);
|
||||
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.MOVE_ROW:
|
||||
rows = _.get(data, action.path)||[];
|
||||
var row = rows[action.oldIndex];
|
||||
rows.splice(action.oldIndex, 1);
|
||||
rows.splice(action.newIndex, 0, row);
|
||||
|
||||
_.set(data, action.path, rows);
|
||||
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE:
|
||||
data.__deferred__ = [];
|
||||
return data;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE:
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
break;
|
||||
}
|
||||
|
||||
data.__changeId = (data.__changeId || 0) + 1;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
80
web/pgadmin/static/js/SchemaView/SchemaState/store.js
Normal file
80
web/pgadmin/static/js/SchemaView/SchemaState/store.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { isValueEqual } from '../common';
|
||||
import { flatPathGenerator } from './common';
|
||||
|
||||
|
||||
export const createStore = (initialState) => {
|
||||
let state = initialState;
|
||||
|
||||
const listeners = new Set();
|
||||
const gen = flatPathGenerator('/');
|
||||
const pathListeners = new Set();
|
||||
|
||||
// Exposed functions
|
||||
// Don't attempt to manipulate the state directly.
|
||||
const getState = () => state;
|
||||
const setState = (nextState) => {
|
||||
const prevState = state;
|
||||
state = _.clone(nextState);
|
||||
|
||||
if (isValueEqual(state, prevState)) return;
|
||||
|
||||
listeners.forEach((listener) => {
|
||||
listener();
|
||||
});
|
||||
|
||||
const changeMemo = new Map();
|
||||
|
||||
pathListeners.forEach((pathListener) => {
|
||||
const [ path, listener ] = pathListener;
|
||||
const flatPath = gen.flatPath(path);
|
||||
|
||||
if (!changeMemo.has(flatPath)) {
|
||||
const pathNextValue =
|
||||
flatPath == '' ? nextState : _.get(nextState, path, undefined);
|
||||
const pathPrevValue =
|
||||
flatPath == '' ? prevState : _.get(prevState, path, undefined);
|
||||
|
||||
changeMemo.set(flatPath, [
|
||||
isValueEqual(pathNextValue, pathPrevValue),
|
||||
pathNextValue,
|
||||
pathPrevValue,
|
||||
]);
|
||||
}
|
||||
|
||||
const [isSame, pathNextValue, pathPrevValue] = changeMemo.get(flatPath);
|
||||
|
||||
if (!isSame) {
|
||||
listener(pathNextValue, pathPrevValue);
|
||||
}
|
||||
});
|
||||
};
|
||||
const get = (path = []) => (_.get(state, path));
|
||||
const set = (arg) => {
|
||||
let nextState = _.isFunction(arg) ? arg(_.cloneDeep(state)) : arg;
|
||||
setState(nextState);
|
||||
};
|
||||
const subscribe = (listener) => {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
};
|
||||
const subscribeForPath = (path, listner) => {
|
||||
const data = [path, listner];
|
||||
|
||||
pathListeners.add(data);
|
||||
|
||||
return () => {
|
||||
return pathListeners.delete(data);
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getState,
|
||||
setState,
|
||||
get,
|
||||
set,
|
||||
subscribe,
|
||||
subscribeForPath,
|
||||
};
|
||||
};
|
@@ -15,6 +15,7 @@ import ErrorBoundary from 'sources/helpers/ErrorBoundary';
|
||||
|
||||
import SchemaDialogView from './SchemaDialogView';
|
||||
import SchemaPropertiesView from './SchemaPropertiesView';
|
||||
import { registerView } from './registry';
|
||||
|
||||
|
||||
export default function SchemaView({formType, ...props}) {
|
||||
@@ -32,3 +33,5 @@ export default function SchemaView({formType, ...props}) {
|
||||
SchemaView.propTypes = {
|
||||
formType: PropTypes.oneOf(['tab', 'dialog']),
|
||||
};
|
||||
|
||||
registerView(SchemaView, 'SchemaView');
|
||||
|
@@ -41,7 +41,7 @@ export const StyledBox = styled(Box)(({theme}) => ({
|
||||
padding: theme.spacing(1),
|
||||
overflow: 'auto',
|
||||
flexGrow: 1,
|
||||
'& .Properties-controlRow': {
|
||||
'& .Properties-controlRow:not(:last-child)': {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
|
@@ -9,6 +9,8 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { memoizeFn } from 'sources/utils';
|
||||
|
||||
/* This is the base schema class for SchemaView.
|
||||
* A UI schema must inherit this to use SchemaView for UI.
|
||||
*/
|
||||
@@ -63,11 +65,11 @@ export default class BaseUISchema {
|
||||
|
||||
/*
|
||||
* The session data, can be useful but setting this will not affect UI.
|
||||
* this._sessData is set by SchemaView directly. set sessData should not be
|
||||
* this.sessData is set by SchemaView directly. set sessData should not be
|
||||
* allowed anywhere.
|
||||
*/
|
||||
get sessData() {
|
||||
return this._sessData || {};
|
||||
return this.state?.data;
|
||||
}
|
||||
|
||||
set sessData(val) {
|
||||
@@ -93,19 +95,28 @@ export default class BaseUISchema {
|
||||
concat base fields with extraFields.
|
||||
*/
|
||||
get fields() {
|
||||
return this.baseFields
|
||||
.filter((field)=>{
|
||||
let retval;
|
||||
if (!this.__filteredFields) {
|
||||
// Memoize the results
|
||||
this.__filteredFields = memoizeFn(
|
||||
(baseFields, keys, filterGroups) => baseFields.filter((field) => {
|
||||
let retval;
|
||||
|
||||
/* If any groups are to be filtered */
|
||||
retval = this.filterGroups.indexOf(field.group) == -1;
|
||||
// If any groups are to be filtered.
|
||||
retval = filterGroups.indexOf(field.group) == -1;
|
||||
|
||||
/* Select only keys, if specified */
|
||||
if(this.keys) {
|
||||
retval = retval && this.keys.indexOf(field.id) > -1;
|
||||
}
|
||||
return retval;
|
||||
});
|
||||
// Select only keys, if specified.
|
||||
if(retval && keys) {
|
||||
retval = keys.indexOf(field.id) > -1;
|
||||
}
|
||||
|
||||
return retval;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return this.__filteredFields(
|
||||
this.baseFields, this.keys, this.filterGroups
|
||||
);
|
||||
}
|
||||
|
||||
initialise() {
|
||||
@@ -190,4 +201,8 @@ export default class BaseUISchema {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this._id;
|
||||
}
|
||||
}
|
||||
|
@@ -7,32 +7,16 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
|
||||
export const SCHEMA_STATE_ACTIONS = {
|
||||
INIT: 'init',
|
||||
SET_VALUE: 'set_value',
|
||||
ADD_ROW: 'add_row',
|
||||
DELETE_ROW: 'delete_row',
|
||||
MOVE_ROW: 'move_row',
|
||||
RERENDER: 'rerender',
|
||||
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
|
||||
DEFERRED_DEPCHANGE: 'deferred_depchange',
|
||||
BULK_UPDATE: 'bulk_update',
|
||||
};
|
||||
|
||||
export const SchemaStateContext = React.createContext();
|
||||
|
||||
export function generateTimeBasedRandomNumberString() {
|
||||
return new Date().getTime() + '' + Math.floor(Math.random() * 1000001);
|
||||
}
|
||||
|
||||
export function isModeSupportedByField(field, helperProps) {
|
||||
if (!field || !field.mode) return true;
|
||||
return (field.mode.indexOf(helperProps.mode) > -1);
|
||||
}
|
||||
export const isModeSupportedByField = (field, helperProps) => (
|
||||
!field.mode || field.mode.indexOf(helperProps.mode) > -1
|
||||
);
|
||||
|
||||
export function getFieldMetaData(
|
||||
field, schema, value, viewHelperProps
|
||||
@@ -81,13 +65,14 @@ export function getFieldMetaData(
|
||||
retData.editable = !(
|
||||
viewHelperProps.inCatalog || (viewHelperProps.mode == 'properties')
|
||||
);
|
||||
|
||||
if(retData.editable) {
|
||||
retData.editable = evalFunc(
|
||||
schema, (_.isUndefined(editable) ? true : editable), value
|
||||
);
|
||||
}
|
||||
|
||||
let {canAdd, canEdit, canDelete, canReorder, canAddRow } = field;
|
||||
let {canAdd, canEdit, canDelete, canAddRow } = field;
|
||||
retData.canAdd =
|
||||
_.isUndefined(canAdd) ? retData.canAdd : evalFunc(schema, canAdd, value);
|
||||
retData.canAdd = !retData.disabled && retData.canAdd;
|
||||
@@ -99,10 +84,6 @@ export function getFieldMetaData(
|
||||
schema, canDelete, value
|
||||
);
|
||||
retData.canDelete = !retData.disabled && retData.canDelete;
|
||||
retData.canReorder =
|
||||
_.isUndefined(canReorder) ? retData.canReorder : evalFunc(
|
||||
schema, canReorder, value
|
||||
);
|
||||
retData.canAddRow =
|
||||
_.isUndefined(canAddRow) ? retData.canAddRow : evalFunc(
|
||||
schema, canAddRow, value
|
||||
@@ -165,3 +146,38 @@ export function getForQueryParams(data) {
|
||||
});
|
||||
return retData;
|
||||
}
|
||||
|
||||
export function prepareData(val, createMode=false) {
|
||||
if(_.isPlainObject(val)) {
|
||||
_.forIn(val, function (el) {
|
||||
if (_.isObject(el)) {
|
||||
prepareData(el, createMode);
|
||||
}
|
||||
});
|
||||
} else if(_.isArray(val)) {
|
||||
val.forEach(function(el) {
|
||||
if (_.isPlainObject(el)) {
|
||||
/* The each row in collection need to have an id to identify them uniquely
|
||||
This helps in easily getting what has changed */
|
||||
/* Nested collection rows may or may not have idAttribute.
|
||||
So to decide whether row is new or not set, the cid starts with
|
||||
nn (not new) for existing rows. Newly added will start with 'c' (created)
|
||||
*/
|
||||
el['cid'] = createMode ? _.uniqueId('c') : _.uniqueId('nn');
|
||||
prepareData(el, createMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export const flatternObject = (obj, base=[]) => Object.keys(obj).sort().reduce(
|
||||
(r, k) => {
|
||||
r = r.concat(k);
|
||||
const value = obj[k];
|
||||
if (_.isFunction(value)) return r;
|
||||
if (_.isArray(value)) return r.concat(...value);
|
||||
if (_.isPlainObject(value)) return flatternObject(value, r);
|
||||
return r.concat(value);
|
||||
}, base
|
||||
);
|
||||
|
23
web/pgadmin/static/js/SchemaView/hooks/index.js
Normal file
23
web/pgadmin/static/js/SchemaView/hooks/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useFieldError } from './useFieldError';
|
||||
import { useFieldOptions } from './useFieldOptions';
|
||||
import { useFieldValue } from './useFieldValue';
|
||||
import { useSchemaState } from './useSchemaState';
|
||||
import { useFieldSchema } from './useFieldSchema';
|
||||
|
||||
|
||||
export {
|
||||
useFieldError,
|
||||
useFieldOptions,
|
||||
useFieldValue,
|
||||
useFieldSchema,
|
||||
useSchemaState,
|
||||
};
|
34
web/pgadmin/static/js/SchemaView/hooks/useFieldError.js
Normal file
34
web/pgadmin/static/js/SchemaView/hooks/useFieldError.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
|
||||
export const useFieldError = (
|
||||
path, schemaState, key, setRefreshKey
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!schemaState || !setRefreshKey) return;
|
||||
|
||||
const checkPathError = (newState, prevState) => {
|
||||
if (prevState.name !== path && newState.name !== path) return;
|
||||
// We don't need to redraw the control on message change.
|
||||
if (prevState.name === newState.name) return;
|
||||
|
||||
setRefreshKey({id: Date.now()});
|
||||
};
|
||||
|
||||
return schemaState.subscribe(['errors'], checkPathError, 'states');
|
||||
}, [key, schemaState?._id]);
|
||||
|
||||
const errors = schemaState?.errors || {};
|
||||
const error = errors.name === path ? errors.message : null;
|
||||
|
||||
return {hasError: !_.isNull(error), error};
|
||||
};
|
25
web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js
Normal file
25
web/pgadmin/static/js/SchemaView/hooks/useFieldOptions.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
|
||||
export const useFieldOptions = (
|
||||
path, schemaState, key, setRefreshKey
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!schemaState) return;
|
||||
|
||||
return schemaState.subscribe(
|
||||
path, () => setRefreshKey?.({id: Date.now()}), 'options'
|
||||
);
|
||||
}, [key, schemaState?._id]);
|
||||
|
||||
return schemaState?.options(path) || {visible: true};
|
||||
};
|
56
web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js
Normal file
56
web/pgadmin/static/js/SchemaView/hooks/useFieldSchema.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { booleanEvaluator } from '../options';
|
||||
|
||||
|
||||
export const useFieldSchema = (
|
||||
field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!schemaState || !field) return;
|
||||
|
||||
// It already has 'id', 'options' is already evaluated.
|
||||
if (field.id)
|
||||
return schemaState.subscribe(
|
||||
accessPath, () => setRefreshKey?.({id: Date.now()}), 'options'
|
||||
);
|
||||
|
||||
// There are no dependencies.
|
||||
if (!_.isArray(field?.deps)) return;
|
||||
|
||||
// Subscribe to all the dependents.
|
||||
const unsubscribers = field.deps.map((dep) => (
|
||||
schemaState.subscribe(
|
||||
accessPath.concat(dep), () => setRefreshKey?.({id: Date.now()}),
|
||||
'value'
|
||||
)
|
||||
));
|
||||
|
||||
return () => {
|
||||
unsubscribers.forEach(unsubscribe => unsubscribe());
|
||||
};
|
||||
}, [key, schemaState?._id]);
|
||||
|
||||
if (!field) return { visible: true };
|
||||
if (field.id) return schemaState?.options(accessPath);
|
||||
if (!field.schema) return { visible: true };
|
||||
|
||||
value = value || {};
|
||||
|
||||
return {
|
||||
visible: booleanEvaluator({
|
||||
schema: field.schema, field, option: 'visible', value, viewHelperProps,
|
||||
defaultVal: true,
|
||||
}),
|
||||
};
|
||||
};
|
25
web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js
Normal file
25
web/pgadmin/static/js/SchemaView/hooks/useFieldValue.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
|
||||
export const useFieldValue = (
|
||||
path, schemaState, key, setRefreshKey
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!schemaState || !setRefreshKey) return;
|
||||
|
||||
return schemaState.subscribe(
|
||||
path, () => setRefreshKey({id: Date.now()}), 'value'
|
||||
);
|
||||
}, [key, schemaState?._id]);
|
||||
|
||||
return schemaState?.value(path);
|
||||
};
|
143
web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js
Normal file
143
web/pgadmin/static/js/SchemaView/hooks/useSchemaState.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { prepareData } from '../common';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS,
|
||||
SchemaState,
|
||||
sessDataReducer,
|
||||
} from '../SchemaState';
|
||||
|
||||
|
||||
export const useSchemaState = ({
|
||||
schema, getInitData, immutableData, onDataChange, viewHelperProps,
|
||||
loadingText,
|
||||
}) => {
|
||||
|
||||
if (!schema)
|
||||
return {
|
||||
schemaState: null,
|
||||
dataDispatch: null,
|
||||
reset: null,
|
||||
};
|
||||
|
||||
let state = schema.state;
|
||||
|
||||
if (!state) {
|
||||
schema.state = state = new SchemaState(
|
||||
schema, getInitData, immutableData, onDataChange, viewHelperProps,
|
||||
loadingText,
|
||||
);
|
||||
state.updateOptions();
|
||||
}
|
||||
|
||||
const [sessData, sessDispatch] = useReducer(
|
||||
sessDataReducer, {...(_.cloneDeep(state.data)), __changeId: 0}
|
||||
);
|
||||
|
||||
const sessDispatchWithListener = (action) => {
|
||||
let dispatchPayload = {
|
||||
...action,
|
||||
depChange: (...args) => state.getDepChange(...args),
|
||||
deferredDepChange: (...args) => state.getDeferredDepChange(...args),
|
||||
};
|
||||
/*
|
||||
* All the session changes coming before init should be queued up.
|
||||
* They will be processed later when form is ready.
|
||||
*/
|
||||
let preReadyQueue = state.preReadyQueue;
|
||||
|
||||
preReadyQueue ?
|
||||
preReadyQueue.push(dispatchPayload) :
|
||||
sessDispatch(dispatchPayload);
|
||||
};
|
||||
|
||||
state.setUnpreparedData = (path, value) => {
|
||||
if(path) {
|
||||
let data = prepareData(value);
|
||||
_.set(schema.initData, path, data);
|
||||
sessDispatchWithListener({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: path,
|
||||
value: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
const initData = _.cloneDeep(state.initData);
|
||||
initData.__changeId = sessData.__changeId;
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: initData,
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
state.initialise(sessDispatch, true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
state.initialise(sessDispatch);
|
||||
}, [state.loadingState]);
|
||||
|
||||
useEffect(() => {
|
||||
let preReadyQueue = state.preReadyQueue;
|
||||
|
||||
if (!state.isReady || !preReadyQueue) return;
|
||||
|
||||
for (const payload of preReadyQueue) {
|
||||
sessDispatch(payload);
|
||||
}
|
||||
|
||||
// Destroy the queue so that no one uses it.
|
||||
state.preReadyQueue = null;
|
||||
}, [state.isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
// Validate the schema on the change of the data.
|
||||
if (state.isReady) state.validate(sessData);
|
||||
}, [state.isReady, sessData.__changeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = sessData.__deferred__ || [];
|
||||
|
||||
if (items.length == 0) return;
|
||||
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE,
|
||||
});
|
||||
|
||||
items.forEach((item) => {
|
||||
item.promise.then((resFunc) => {
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE,
|
||||
path: item.action.path,
|
||||
depChange: item.action.depChange,
|
||||
listener: {
|
||||
...item.listener,
|
||||
callback: resFunc,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [sessData.__deferred__?.length]);
|
||||
|
||||
state.reload = reload;
|
||||
state.reset = resetData;
|
||||
|
||||
return {
|
||||
schemaState: state,
|
||||
dataDispatch: sessDispatchWithListener,
|
||||
reset: resetData,
|
||||
};
|
||||
};
|
@@ -7,43 +7,46 @@
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import BaseUISchema from './base_schema.ui';
|
||||
import DataGridView from './DataGridView';
|
||||
import FieldSetView from './FieldSetView';
|
||||
import FormView from './FormView';
|
||||
import InlineView from './InlineView';
|
||||
import SchemaDialogView from './SchemaDialogView';
|
||||
import SchemaPropertiesView from './SchemaPropertiesView';
|
||||
import SchemaView from './SchemaView';
|
||||
import BaseUISchema from './base_schema.ui';
|
||||
import { useSchemaState } from './useSchemaState';
|
||||
import { useSchemaState, useFieldState } from './hooks';
|
||||
import {
|
||||
generateTimeBasedRandomNumberString,
|
||||
isValueEqual,
|
||||
isObjectEqual,
|
||||
getForQueryParams,
|
||||
prepareData,
|
||||
} from './common';
|
||||
import {
|
||||
SCHEMA_STATE_ACTIONS,
|
||||
SchemaStateContext,
|
||||
generateTimeBasedRandomNumberString,
|
||||
isModeSupportedByField,
|
||||
getFieldMetaData,
|
||||
isValueEqual,
|
||||
isObjectEqual,
|
||||
getForQueryParams
|
||||
} from './common';
|
||||
} from './SchemaState';
|
||||
|
||||
|
||||
export default SchemaView;
|
||||
|
||||
export {
|
||||
SCHEMA_STATE_ACTIONS,
|
||||
BaseUISchema,
|
||||
DataGridView,
|
||||
FieldSetView,
|
||||
FormView,
|
||||
InlineView,
|
||||
SchemaDialogView,
|
||||
SchemaPropertiesView,
|
||||
SchemaView,
|
||||
BaseUISchema,
|
||||
useSchemaState,
|
||||
SCHEMA_STATE_ACTIONS,
|
||||
SchemaStateContext,
|
||||
getForQueryParams,
|
||||
generateTimeBasedRandomNumberString,
|
||||
isModeSupportedByField,
|
||||
getFieldMetaData,
|
||||
isValueEqual,
|
||||
isObjectEqual,
|
||||
getForQueryParams
|
||||
prepareData,
|
||||
useFieldState,
|
||||
useSchemaState,
|
||||
};
|
||||
|
40
web/pgadmin/static/js/SchemaView/options/common.js
Normal file
40
web/pgadmin/static/js/SchemaView/options/common.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
|
||||
export const FIELD_OPTIONS = '__fieldOptions';
|
||||
|
||||
export const booleanEvaluator = ({
|
||||
schema, field, option, value, viewHelperProps, options, defaultVal,
|
||||
}) => (
|
||||
_.isUndefined(field?.[option]) ? defaultVal :
|
||||
Boolean(evalFunc(schema, field[option], value, viewHelperProps, options))
|
||||
);
|
||||
|
||||
export const evalIfNotDisabled = ({ options, ...params }) => (
|
||||
!options.disabled &&
|
||||
booleanEvaluator({ options, ...params })
|
||||
);
|
||||
|
||||
export const canAddOrDelete = ({
|
||||
options, viewHelperProps, field, ...params
|
||||
}) => (
|
||||
viewHelperProps?.mode != 'properties' &&
|
||||
!(field?.fixedRow) &&
|
||||
!options.disabled &&
|
||||
booleanEvaluator({ options, viewHelperProps, field, ...params })
|
||||
);
|
||||
|
||||
export const evalInNonPropertyMode = ({ viewHelperProps, ...params }) => (
|
||||
viewHelperProps?.mode != 'properties' &&
|
||||
booleanEvaluator({ viewHelperProps, ...params })
|
||||
);
|
176
web/pgadmin/static/js/SchemaView/options/index.js
Normal file
176
web/pgadmin/static/js/SchemaView/options/index.js
Normal file
@@ -0,0 +1,176 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { evalFunc } from 'sources/utils';
|
||||
import {
|
||||
booleanEvaluator,
|
||||
canAddOrDelete,
|
||||
evalIfNotDisabled,
|
||||
evalInNonPropertyMode,
|
||||
FIELD_OPTIONS
|
||||
} from './common';
|
||||
import {
|
||||
evaluateFieldOptions,
|
||||
evaluateFieldsOption,
|
||||
registerOptionEvaluator,
|
||||
schemaOptionsEvalulator,
|
||||
} from './registry';
|
||||
|
||||
export {
|
||||
FIELD_OPTIONS,
|
||||
booleanEvaluator,
|
||||
canAddOrDelete,
|
||||
evaluateFieldOptions,
|
||||
evaluateFieldsOption,
|
||||
evalIfNotDisabled,
|
||||
registerOptionEvaluator,
|
||||
schemaOptionsEvalulator,
|
||||
};
|
||||
|
||||
const VISIBLE = 'visible';
|
||||
|
||||
// Default evaluators
|
||||
// 1. disabled
|
||||
// 2. visible (It also checks for the supported mode)
|
||||
// 3. readonly
|
||||
|
||||
registerOptionEvaluator('disabled');
|
||||
registerOptionEvaluator(
|
||||
VISIBLE,
|
||||
// Evaluator
|
||||
({schema, field, value, viewHelperProps}) => (
|
||||
(
|
||||
!field.mode || field.mode.indexOf(viewHelperProps.mode) > -1
|
||||
) && (
|
||||
// serverInfo not found
|
||||
_.isUndefined(viewHelperProps.serverInfo) ||
|
||||
// serverInfo found and it's within range
|
||||
((
|
||||
_.isUndefined(field.server_type) ? true :
|
||||
(viewHelperProps.serverInfo.type in field.server_type)
|
||||
) && (
|
||||
_.isUndefined(field.min_version) ? true :
|
||||
(viewHelperProps.serverInfo.version >= field.min_version)
|
||||
) && (
|
||||
_.isUndefined(field.max_version) ? true :
|
||||
(viewHelperProps.serverInfo.version <= field.max_version)
|
||||
))
|
||||
) && (
|
||||
_.isUndefined(field[VISIBLE]) ? true :
|
||||
Boolean(evalFunc(schema, field[VISIBLE], value))
|
||||
)),
|
||||
);
|
||||
|
||||
registerOptionEvaluator(
|
||||
'readonly',
|
||||
// Evaluator
|
||||
({viewHelperProps, ...args}) => (
|
||||
viewHelperProps.inCatalog ||
|
||||
viewHelperProps.mode === 'properties' ||
|
||||
booleanEvaluator({viewHelperProps, ...args })
|
||||
),
|
||||
// Default value
|
||||
false
|
||||
);
|
||||
|
||||
|
||||
// Collection evaluators
|
||||
// 1. canAdd
|
||||
// 2. canEdit
|
||||
// 3. canAddRow
|
||||
// 4. expandEditOnAdd
|
||||
// 5. addOnTop
|
||||
// 6. canSearch
|
||||
registerOptionEvaluator(
|
||||
'canAdd',
|
||||
// Evaluator
|
||||
canAddOrDelete,
|
||||
// Default value
|
||||
true,
|
||||
['collection']
|
||||
);
|
||||
|
||||
registerOptionEvaluator(
|
||||
'canEdit',
|
||||
// Evaluator
|
||||
({viewHelperProps, options, ...args}) => (
|
||||
!viewHelperProps.inCatalog &&
|
||||
viewHelperProps.mode !== 'properties' &&
|
||||
!options.disabled &&
|
||||
booleanEvaluator({viewHelperProps, options, ...args })
|
||||
),
|
||||
// Default value
|
||||
false,
|
||||
['collection']
|
||||
);
|
||||
|
||||
registerOptionEvaluator(
|
||||
'canAddRow',
|
||||
// Evaluator
|
||||
({options, ...args}) => (
|
||||
options.canAdd &&
|
||||
booleanEvaluator({options, ...args })
|
||||
),
|
||||
// Default value
|
||||
true,
|
||||
['collection']
|
||||
);
|
||||
|
||||
registerOptionEvaluator(
|
||||
'expandEditOnAdd',
|
||||
// Evaluator
|
||||
evalInNonPropertyMode,
|
||||
// Default value
|
||||
false,
|
||||
['collection']
|
||||
);
|
||||
|
||||
registerOptionEvaluator(
|
||||
'addOnTop',
|
||||
// Evaluator
|
||||
evalInNonPropertyMode,
|
||||
// Default value
|
||||
false,
|
||||
['collection']
|
||||
);
|
||||
|
||||
registerOptionEvaluator(
|
||||
'canSearch',
|
||||
// Evaluator
|
||||
evalInNonPropertyMode,
|
||||
// Default value
|
||||
false,
|
||||
['collection']
|
||||
);
|
||||
|
||||
// Row evaluators
|
||||
// 1. canEditRow
|
||||
registerOptionEvaluator(
|
||||
'canEditRow',
|
||||
// Evaluator
|
||||
evalInNonPropertyMode,
|
||||
// Default value
|
||||
true,
|
||||
['row']
|
||||
);
|
||||
|
||||
// Grid cell evaluatiors
|
||||
// 1. editable
|
||||
registerOptionEvaluator(
|
||||
'editable',
|
||||
// Evaluator
|
||||
({viewHelperProps, ...args}) => (
|
||||
!viewHelperProps.inCatalog &&
|
||||
viewHelperProps.mode !== 'properties' &&
|
||||
booleanEvaluator({viewHelperProps, ...args })
|
||||
),
|
||||
// Default value
|
||||
true,
|
||||
['cell']
|
||||
);
|
156
web/pgadmin/static/js/SchemaView/options/registry.js
Normal file
156
web/pgadmin/static/js/SchemaView/options/registry.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
import { isModeSupportedByField } from '../common';
|
||||
import { FIELD_OPTIONS, booleanEvaluator } from './common';
|
||||
|
||||
|
||||
const COMMON_OPTIONS = '__common';
|
||||
const _optionEvaluators = { };
|
||||
|
||||
|
||||
export function registerOptionEvaluator(option, evaluator, defaultVal, types) {
|
||||
types = types || [COMMON_OPTIONS];
|
||||
evaluator = evaluator || booleanEvaluator;
|
||||
defaultVal = _.isUndefined(defaultVal) ? false : defaultVal;
|
||||
|
||||
types.forEach((type) => {
|
||||
const evaluators = _optionEvaluators[type] =
|
||||
(_optionEvaluators[type] || []);
|
||||
|
||||
evaluators.push([option, evaluator, defaultVal]);
|
||||
});
|
||||
}
|
||||
|
||||
export function evaluateFieldOption({
|
||||
option, schema, value, viewHelperProps, field, options, parentOptions,
|
||||
}) {
|
||||
if (option && option in _optionEvaluators) {
|
||||
const evaluators = _optionEvaluators[option];
|
||||
evaluators?.forEach(([option, evaluator, defaultVal]) => {
|
||||
options[option] = evaluator({
|
||||
schema, field, option, value, viewHelperProps, options, defaultVal,
|
||||
parentOptions
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateFieldOptions({
|
||||
schema, value, viewHelperProps, field, options={}, parentOptions=null
|
||||
}) {
|
||||
evaluateFieldOption({
|
||||
option: COMMON_OPTIONS, schema, value, viewHelperProps, field, options,
|
||||
parentOptions
|
||||
});
|
||||
evaluateFieldOption({
|
||||
option: field.type, schema, value, viewHelperProps, field, options,
|
||||
parentOptions
|
||||
});
|
||||
}
|
||||
|
||||
export function schemaOptionsEvalulator({
|
||||
schema, data, accessPath=[], viewHelperProps, options, parentOptions=null,
|
||||
inGrid=false
|
||||
}) {
|
||||
schema?.fields?.forEach((field) => {
|
||||
// We could have multiple entries of same `field.id` for each mode, hence -
|
||||
// we should process the options only if the current field is support for
|
||||
// the given mode.
|
||||
if (!isModeSupportedByField(field, viewHelperProps)) return;
|
||||
|
||||
switch (field.type) {
|
||||
case 'nested-tab':
|
||||
case 'nested-fieldset':
|
||||
case 'inline-groups':
|
||||
{
|
||||
if (!field.schema) return;
|
||||
if (!field.schema.top) field.schema.top = schema.top || schema;
|
||||
|
||||
const path = field.id ? [...accessPath, field.id] : accessPath;
|
||||
|
||||
schemaOptionsEvalulator({
|
||||
schema: field.schema, data, path, viewHelperProps, options,
|
||||
parentOptions
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'collection':
|
||||
{
|
||||
if (!field.schema) return;
|
||||
if (!field.schema.top) field.schema.top = schema.top || schema;
|
||||
|
||||
const fieldPath = [...accessPath, field.id];
|
||||
const fieldOptionsPath = [...fieldPath, FIELD_OPTIONS];
|
||||
const fieldOptions = _.get(options, fieldOptionsPath, {});
|
||||
const rows = data[field.id];
|
||||
|
||||
evaluateFieldOptions({
|
||||
schema, value: data, viewHelperProps, field,
|
||||
options: fieldOptions, parentOptions,
|
||||
});
|
||||
|
||||
_.set(options, fieldOptionsPath, fieldOptions);
|
||||
|
||||
const rowIndexes = [FIELD_OPTIONS];
|
||||
|
||||
rows?.forEach((row, idx) => {
|
||||
const schemaPath = [...fieldPath, idx];
|
||||
const schemaOptions = _.get(options, schemaPath, {});
|
||||
|
||||
_.set(options, schemaPath, schemaOptions);
|
||||
|
||||
schemaOptionsEvalulator({
|
||||
schema: field.schema, data: row, accessPath: [],
|
||||
viewHelperProps, options: schemaOptions,
|
||||
parentOptions: fieldOptions, inGrid: true
|
||||
});
|
||||
|
||||
const rowPath = [...schemaPath, FIELD_OPTIONS];
|
||||
const rowOptions = _.get(options, rowPath, {});
|
||||
_.set(options, rowPath, rowOptions);
|
||||
|
||||
evaluateFieldOption({
|
||||
option: 'row', schema: field.schema, value: row, viewHelperProps,
|
||||
field, options: rowOptions, parentOptions: fieldOptions
|
||||
});
|
||||
|
||||
rowIndexes.push(idx);
|
||||
});
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
const fieldPath = [...accessPath, field.id];
|
||||
const fieldOptionsPath = [...fieldPath, FIELD_OPTIONS];
|
||||
const fieldOptions = _.get(options, fieldOptionsPath, {});
|
||||
|
||||
evaluateFieldOptions({
|
||||
schema, value: data, viewHelperProps, field, options: fieldOptions,
|
||||
parentOptions,
|
||||
});
|
||||
|
||||
if (inGrid) {
|
||||
evaluateFieldOption({
|
||||
option: 'cell', schema, value: data, viewHelperProps, field,
|
||||
options: fieldOptions, parentOptions,
|
||||
});
|
||||
}
|
||||
|
||||
_.set(options, fieldOptionsPath, fieldOptions);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
42
web/pgadmin/static/js/SchemaView/registry.js
Normal file
42
web/pgadmin/static/js/SchemaView/registry.js
Normal file
@@ -0,0 +1,42 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
* Using the factory pattern (registry) to avoid circular imports of the views.
|
||||
*/
|
||||
const _views = {};
|
||||
|
||||
export function registerView(viewFunc, name) {
|
||||
name = name || viewFunc.name;
|
||||
|
||||
if (name in _views) {
|
||||
throw new Error(
|
||||
`View type '${name}' is alredy registered.`
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof viewFunc !== 'function') {
|
||||
throw new Error(
|
||||
`View '${name}' must be a function.`
|
||||
);
|
||||
}
|
||||
|
||||
_views[name] = viewFunc;
|
||||
}
|
||||
|
||||
export function View(name) {
|
||||
const view = _views[name];
|
||||
|
||||
if (view) return view;
|
||||
throw new Error(`View ${name} is not found in the registry.`);
|
||||
}
|
||||
|
||||
export function hasView(name) {
|
||||
return (name in _views);
|
||||
}
|
@@ -1,489 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { parseApiError } from 'sources/api_instance';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import { DepListener } from './DepListener';
|
||||
import {
|
||||
getSchemaDataDiff,
|
||||
validateSchema,
|
||||
} from './schemaUtils';
|
||||
|
||||
|
||||
export const SchemaStateContext = React.createContext();
|
||||
|
||||
export const SCHEMA_STATE_ACTIONS = {
|
||||
INIT: 'init',
|
||||
SET_VALUE: 'set_value',
|
||||
ADD_ROW: 'add_row',
|
||||
DELETE_ROW: 'delete_row',
|
||||
MOVE_ROW: 'move_row',
|
||||
RERENDER: 'rerender',
|
||||
CLEAR_DEFERRED_QUEUE: 'clear_deferred_queue',
|
||||
DEFERRED_DEPCHANGE: 'deferred_depchange',
|
||||
BULK_UPDATE: 'bulk_update',
|
||||
};
|
||||
|
||||
const getDepChange = (currPath, newState, oldState, action) => {
|
||||
if(action.depChange) {
|
||||
newState = action.depChange(currPath, newState, {
|
||||
type: action.type,
|
||||
path: action.path,
|
||||
value: action.value,
|
||||
oldState: _.cloneDeep(oldState),
|
||||
listener: action.listener,
|
||||
});
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
|
||||
const getDeferredDepChange = (currPath, newState, oldState, action) => {
|
||||
if(action.deferredDepChange) {
|
||||
return action.deferredDepChange(currPath, newState, {
|
||||
type: action.type,
|
||||
path: action.path,
|
||||
value: action.value,
|
||||
depChange: action.depChange,
|
||||
oldState: _.cloneDeep(oldState),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* The main function which manipulates the session state based on actions.
|
||||
*
|
||||
* The state is managed based on path array of a particular key.
|
||||
* For Eg. if the state is
|
||||
* {
|
||||
* key1: {
|
||||
* ckey1: [
|
||||
* {a: 0, b: 0},
|
||||
* {a: 1, b: 1}
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* The path for b in first row will be '[key1, ckey1, 0, b]'.
|
||||
* The path for second row of ckey1 will be '[key1, ckey1, 1]'.
|
||||
*
|
||||
* The path for key1 is '[key1]'.
|
||||
* The state starts with path '[]'.
|
||||
*/
|
||||
const sessDataReducer = (state, action) => {
|
||||
let data = _.cloneDeep(state);
|
||||
let rows, cid, deferredList;
|
||||
data.__deferred__ = data.__deferred__ || [];
|
||||
|
||||
switch(action.type) {
|
||||
case SCHEMA_STATE_ACTIONS.INIT:
|
||||
data = action.payload;
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.BULK_UPDATE:
|
||||
rows = (_.get(data, action.path)||[]);
|
||||
rows.forEach((row) => { row[action.id] = false; });
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.SET_VALUE:
|
||||
_.set(data, action.path, action.value);
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
deferredList = getDeferredDepChange(action.path, data, state, action);
|
||||
data.__deferred__ = deferredList || [];
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.ADD_ROW:
|
||||
// Create id to identify a row uniquely, usefull when getting diff.
|
||||
cid = _.uniqueId('c');
|
||||
action.value['cid'] = cid;
|
||||
if (action.addOnTop) {
|
||||
rows = [].concat(action.value).concat(_.get(data, action.path)||[]);
|
||||
} else {
|
||||
rows = (_.get(data, action.path)||[]).concat(action.value);
|
||||
}
|
||||
_.set(data, action.path, rows);
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.DELETE_ROW:
|
||||
rows = _.get(data, action.path)||[];
|
||||
rows.splice(action.value, 1);
|
||||
_.set(data, action.path, rows);
|
||||
// If there is any dep listeners get the changes.
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.MOVE_ROW:
|
||||
rows = _.get(data, action.path)||[];
|
||||
var row = rows[action.oldIndex];
|
||||
rows.splice(action.oldIndex, 1);
|
||||
rows.splice(action.newIndex, 0, row);
|
||||
_.set(data, action.path, rows);
|
||||
break;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE:
|
||||
data.__deferred__ = [];
|
||||
return data;
|
||||
|
||||
case SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE:
|
||||
data = getDepChange(action.path, data, state, action);
|
||||
break;
|
||||
}
|
||||
|
||||
data.__changeId = (data.__changeId || 0) + 1;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
function prepareData(val, createMode=false) {
|
||||
if(_.isPlainObject(val)) {
|
||||
_.forIn(val, function (el) {
|
||||
if (_.isObject(el)) {
|
||||
prepareData(el, createMode);
|
||||
}
|
||||
});
|
||||
} else if(_.isArray(val)) {
|
||||
val.forEach(function(el) {
|
||||
if (_.isPlainObject(el)) {
|
||||
/* The each row in collection need to have an id to identify them uniquely
|
||||
This helps in easily getting what has changed */
|
||||
/* Nested collection rows may or may not have idAttribute.
|
||||
So to decide whether row is new or not set, the cid starts with
|
||||
nn (not new) for existing rows. Newly added will start with 'c' (created)
|
||||
*/
|
||||
el['cid'] = createMode ? _.uniqueId('c') : _.uniqueId('nn');
|
||||
prepareData(el, createMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
const LOADING_STATE = {
|
||||
INIT: 'initializing',
|
||||
LOADING: 'loading',
|
||||
LOADED: 'loaded',
|
||||
ERROR: 'Error'
|
||||
};
|
||||
|
||||
export class SchemaState extends DepListener {
|
||||
|
||||
constructor(
|
||||
schema, getInitData, immutableData, mode, keepCid, onDataChange
|
||||
) {
|
||||
super();
|
||||
|
||||
////// Helper variables
|
||||
|
||||
// BaseUISchema instance
|
||||
this.schema = schema;
|
||||
// Current mode of operation ('create', 'edit', 'properties')
|
||||
this.mode = mode;
|
||||
// Keep the 'cid' object during diff calculations.
|
||||
this.keepcid = keepCid;
|
||||
// Initialization callback
|
||||
this.getInitData = getInitData;
|
||||
// Data change callback
|
||||
this.onDataChange = onDataChange;
|
||||
|
||||
////// State variables
|
||||
|
||||
// Is is ready to be consumed?
|
||||
this.isReady = false;
|
||||
// Diff between the current snapshot and initial data.
|
||||
this.changes = null;
|
||||
// Loading message (if any)
|
||||
this.message = null;
|
||||
// Current Loading state
|
||||
this.loadingState = LOADING_STATE.INIT;
|
||||
this.hasChanges = false;
|
||||
|
||||
////// Schema instance data
|
||||
|
||||
// Initial data after the ready state
|
||||
this.initData = {};
|
||||
// Current state of the data
|
||||
this.data = {};
|
||||
// Immutable data
|
||||
this.immutableData = immutableData;
|
||||
// Current error
|
||||
this.errors = {};
|
||||
// Pre-ready queue
|
||||
this.preReadyQueue = [];
|
||||
|
||||
this._id = Date.now();
|
||||
}
|
||||
|
||||
setError(err) {
|
||||
this.errors = err;
|
||||
}
|
||||
|
||||
setReady(state) {
|
||||
this.isReady = state;
|
||||
}
|
||||
|
||||
setLoadingState(loadingState) {
|
||||
this.loadingState = loadingState;
|
||||
}
|
||||
|
||||
setLoadingMessage(msg) {
|
||||
this.message = msg;
|
||||
}
|
||||
|
||||
// Initialise the data, and fetch the data from the backend (if required).
|
||||
// 'force' flag can be used for reloading the data from the backend.
|
||||
initialise(dataDispatch, force) {
|
||||
let state = this;
|
||||
|
||||
// Don't attempt to initialize again (if it's already in progress).
|
||||
if (
|
||||
state.loadingState !== LOADING_STATE.INIT ||
|
||||
(force && state.loadingState === LOADING_STATE.LOADING)
|
||||
) return;
|
||||
|
||||
state.setLoadingState(LOADING_STATE.LOADING);
|
||||
state.setLoadingMessage(gettext('Loading...'));
|
||||
|
||||
/*
|
||||
* Fetch the data using getInitData(..) callback.
|
||||
* `getInitData(..)` must be present in 'edit' mode.
|
||||
*/
|
||||
if(state.mode === 'edit' && !state.getInitData) {
|
||||
throw new Error('getInitData must be passed for edit');
|
||||
}
|
||||
|
||||
const initDataPromise = state.getInitData?.() ||
|
||||
Promise.resolve({});
|
||||
|
||||
initDataPromise.then((data) => {
|
||||
data = data || {};
|
||||
|
||||
if(state.mode === 'edit') {
|
||||
// Set the origData to incoming data, useful for comparing.
|
||||
state.initData = prepareData({...data, ...state.immutableData});
|
||||
} else {
|
||||
// In create mode, merge with defaults.
|
||||
state.initData = prepareData({
|
||||
...state.schema.defaults, ...data, ...state.immutableData
|
||||
}, true);
|
||||
}
|
||||
|
||||
state.schema.initialise(state.initData);
|
||||
|
||||
dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: state.initData,
|
||||
});
|
||||
|
||||
state.setLoadingState(LOADING_STATE.LOADED);
|
||||
state.setLoadingMessage('');
|
||||
state.setReady(true);
|
||||
}).catch((err) => {
|
||||
state.setLoadingMessage('');
|
||||
state.setError({
|
||||
name: 'apierror',
|
||||
response: err,
|
||||
message: _.escape(parseApiError(err)),
|
||||
});
|
||||
state.setLoadingState(LOADING_STATE.ERROR);
|
||||
state.setReady(true);
|
||||
});
|
||||
}
|
||||
|
||||
validate(sessData) {
|
||||
let state = this,
|
||||
schema = state.schema;
|
||||
|
||||
// If schema does not have the data or does not have any 'onDataChange'
|
||||
// callback, there is no need to validate the current data.
|
||||
if(!state.isReady) return;
|
||||
|
||||
if(
|
||||
!validateSchema(schema, sessData, (path, message) => {
|
||||
message && state.setError({ name: path, message: _.escape(message) });
|
||||
})
|
||||
) state.setError({});
|
||||
|
||||
state.data = sessData;
|
||||
state.changes = state.Changes();
|
||||
state.onDataChange && state.onDataChange(state.hasChanges, state.changes);
|
||||
}
|
||||
|
||||
Changes(includeSkipChange=false) {
|
||||
const state = this;
|
||||
const sessData = this.data;
|
||||
const schema = state.schema;
|
||||
|
||||
// Check if anything changed.
|
||||
let dataDiff = getSchemaDataDiff(
|
||||
schema, state.initData, sessData,
|
||||
state.mode, state.keepCid, false, includeSkipChange
|
||||
);
|
||||
state.hasChanges = Object.keys(dataDiff).length > 0;
|
||||
|
||||
// Inform the callbacks about change in the data.
|
||||
if(state.mode !== 'edit') {
|
||||
// Merge the changed data with origData in 'create' mode.
|
||||
dataDiff = _.assign({}, state.initData, dataDiff);
|
||||
|
||||
// Remove internal '__changeId' attribute.
|
||||
delete dataDiff.__changeId;
|
||||
|
||||
// In case of 'non-edit' mode, changes are always there.
|
||||
return dataDiff;
|
||||
} else if (state.hasChanges) {
|
||||
const idAttr = schema.idAttribute;
|
||||
const idVal = state.initData[idAttr];
|
||||
// Append 'idAttr' only if it actually exists
|
||||
if (idVal) dataDiff[idAttr] = idVal;
|
||||
|
||||
return dataDiff;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
get isNew() {
|
||||
return this.schema.isNew(this.initData);
|
||||
}
|
||||
|
||||
set isNew(val) {
|
||||
throw new Error('Property \'isNew\' is readonly.', val);
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this.hasChanges;
|
||||
}
|
||||
|
||||
set isDirty(val) {
|
||||
throw new Error('Property \'isDirty\' is readonly.', val);
|
||||
}
|
||||
}
|
||||
|
||||
export const useSchemaState = ({
|
||||
schema, getInitData, immutableData, mode, keepCid, onDataChange,
|
||||
}) => {
|
||||
let schemaState = schema.state;
|
||||
|
||||
if (!schemaState) {
|
||||
schemaState = new SchemaState(
|
||||
schema, getInitData, immutableData, mode, keepCid, onDataChange
|
||||
);
|
||||
schema.state = schemaState;
|
||||
}
|
||||
|
||||
const [sessData, sessDispatch] = useReducer(
|
||||
sessDataReducer, {...(_.cloneDeep(schemaState.data)), __changeId: 0}
|
||||
);
|
||||
|
||||
const sessDispatchWithListener = (action) => {
|
||||
let dispatchPayload = {
|
||||
...action,
|
||||
depChange: (...args) => schemaState.getDepChange(...args),
|
||||
deferredDepChange: (...args) => schemaState.getDeferredDepChange(...args),
|
||||
};
|
||||
/*
|
||||
* All the session changes coming before init should be queued up.
|
||||
* They will be processed later when form is ready.
|
||||
*/
|
||||
let preReadyQueue = schemaState.preReadyQueue;
|
||||
|
||||
preReadyQueue ?
|
||||
preReadyQueue.push(dispatchPayload) :
|
||||
sessDispatch(dispatchPayload);
|
||||
};
|
||||
|
||||
schemaState.setUnpreparedData = (path, value) => {
|
||||
if(path) {
|
||||
let data = prepareData(value);
|
||||
_.set(schema.initData, path, data);
|
||||
sessDispatchWithListener({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: path,
|
||||
value: data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetData = () => {
|
||||
const initData = _.cloneDeep(schemaState.initData);
|
||||
initData.__changeId = sessData.__changeId;
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.INIT,
|
||||
payload: initData,
|
||||
});
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
schemaState.initialise(sessDispatch, true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
schemaState.initialise(sessDispatch);
|
||||
}, [schemaState.loadingState]);
|
||||
|
||||
useEffect(() => {
|
||||
let preReadyQueue = schemaState.preReadyQueue;
|
||||
|
||||
if (!schemaState.isReady || !preReadyQueue) return;
|
||||
|
||||
for (const payload of preReadyQueue) {
|
||||
sessDispatch(payload);
|
||||
}
|
||||
|
||||
// Destroy the queue so that no one uses it.
|
||||
schemaState.preReadyQueue = null;
|
||||
}, [schemaState.isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
// Validate the schema on the change of the data.
|
||||
schemaState.validate(sessData);
|
||||
}, [schemaState.isReady, sessData.__changeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = sessData.__deferred__ || [];
|
||||
|
||||
if (items.length == 0) return;
|
||||
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.CLEAR_DEFERRED_QUEUE,
|
||||
});
|
||||
|
||||
items.forEach((item) => {
|
||||
item.promise.then((resFunc) => {
|
||||
sessDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.DEFERRED_DEPCHANGE,
|
||||
path: item.action.path,
|
||||
depChange: item.action.depChange,
|
||||
listener: {
|
||||
...item.listener,
|
||||
callback: resFunc,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [sessData.__deferred__?.length]);
|
||||
|
||||
schemaState.reload = reload;
|
||||
schemaState.reset = resetData;
|
||||
|
||||
return {
|
||||
schemaState,
|
||||
dataDispatch: sessDispatchWithListener,
|
||||
sessData,
|
||||
reset: resetData,
|
||||
};
|
||||
};
|
205
web/pgadmin/static/js/SchemaView/utils/createFieldControls.jsx
Normal file
205
web/pgadmin/static/js/SchemaView/utils/createFieldControls.jsx
Normal file
@@ -0,0 +1,205 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
import { SCHEMA_STATE_ACTIONS } from '../SchemaState';
|
||||
import { isModeSupportedByField } from '../common';
|
||||
import { View, hasView } from '../registry';
|
||||
import { StaticMappedFormControl, MappedFormControl } from '../MappedControl';
|
||||
|
||||
|
||||
const DEFAULT_TAB = 'general';
|
||||
|
||||
export const createFieldControls = ({
|
||||
schema, schemaState, accessPath, viewHelperProps, dataDispatch
|
||||
}) => {
|
||||
|
||||
const { mode } = (viewHelperProps || {});
|
||||
const isPropertyMode = mode === 'properties';
|
||||
const groups = [];
|
||||
const groupsById = {};
|
||||
let currentGroup = null;
|
||||
|
||||
const createGroup = (id, label, visible, field, isFullTab) => {
|
||||
const group = {
|
||||
id: id,
|
||||
label: label,
|
||||
visible: visible,
|
||||
field: field,
|
||||
className: isFullTab ? (
|
||||
isPropertyMode ? 'Properties-noPadding' : 'FormView-fullSpace'
|
||||
) : '',
|
||||
controls: [],
|
||||
inlineGroups: {},
|
||||
isFullTab: isFullTab
|
||||
};
|
||||
|
||||
groups.push(group);
|
||||
groupsById[id] = group;
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
// Create default group - 'General'.
|
||||
createGroup(DEFAULT_TAB, gettext('General'), true);
|
||||
|
||||
schema?.fields?.forEach((field) => {
|
||||
if (!isModeSupportedByField(field, viewHelperProps)) return;
|
||||
|
||||
let inlineGroup = null;
|
||||
const inlineGroupId = field[inlineGroup];
|
||||
|
||||
if(field.type === 'group') {
|
||||
|
||||
if (!field.id || (field.id in groups)) {
|
||||
throw new Error('Group-id must be unique within a schema.');
|
||||
}
|
||||
|
||||
const { visible } = schemaState.options(accessPath.concat(field.id));
|
||||
createGroup(field.id, field.label, visible, field);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.isFullTab) {
|
||||
if (field.type === inlineGroup)
|
||||
throw new Error('Inline group can not be full tab control');
|
||||
|
||||
const { visible } = schemaState.options(accessPath.concat(field.id));
|
||||
currentGroup = createGroup(
|
||||
field.id, field.label, visible, field, true
|
||||
);
|
||||
} else {
|
||||
const { group } = field;
|
||||
|
||||
currentGroup = groupsById[group || DEFAULT_TAB];
|
||||
|
||||
if (!currentGroup) {
|
||||
const newGroup = createGroup(group, group, true);
|
||||
currentGroup = newGroup;
|
||||
}
|
||||
|
||||
// Generate inline-view if necessary, or use existing one.
|
||||
if (inlineGroupId) {
|
||||
inlineGroup = currentGroup.inlineGroups[inlineGroupId];
|
||||
if (!inlineGroup) {
|
||||
inlineGroup = currentGroup.inlineGroups[inlineGroupId] = {
|
||||
control: View('InlineView'),
|
||||
controlProps: {
|
||||
viewHelperProps: viewHelperProps,
|
||||
field: null,
|
||||
},
|
||||
controls: [],
|
||||
};
|
||||
currentGroup.controls.push(inlineGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === inlineGroup) {
|
||||
if (inlineGroupId) {
|
||||
throw new Error('inline-group can not be created within inline-group');
|
||||
}
|
||||
inlineGroup = currentGroup.inlineGroups[inlineGroupId];
|
||||
if (inlineGroup) {
|
||||
throw new Error('inline-group must be unique-id within a tab group');
|
||||
}
|
||||
inlineGroup = currentGroup.inlineGroups[inlineGroupId] = {
|
||||
control: View('InlineView'),
|
||||
controlProps: {
|
||||
accessPath: schemaState.accessPath(accessPath, field.id),
|
||||
viewHelperProps: viewHelperProps,
|
||||
field: field,
|
||||
},
|
||||
controls: [],
|
||||
};
|
||||
currentGroup.controls.push(inlineGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
let control = null;
|
||||
const controlProps = {
|
||||
key: field.id,
|
||||
accessPath: schemaState.accessPath(accessPath, field.id),
|
||||
viewHelperProps: viewHelperProps,
|
||||
dataDispatch: dataDispatch,
|
||||
field: field,
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
case 'nested-tab':
|
||||
// We don't support nested-tab in 'properties' mode.
|
||||
if (isPropertyMode) return;
|
||||
|
||||
control = View('FormView');
|
||||
controlProps['isNested'] = true;
|
||||
break;
|
||||
case 'nested-fieldset':
|
||||
control = View('FieldSetView');
|
||||
controlProps['controlClassName'] =
|
||||
isPropertyMode ? 'Properties-controlRow' : 'FormView-controlRow';
|
||||
break;
|
||||
case 'collection':
|
||||
control = View('DataGridView');
|
||||
controlProps['containerClassName'] =
|
||||
isPropertyMode ? 'Properties-controlRow' : 'FormView-controlRow';
|
||||
break;
|
||||
default:
|
||||
{
|
||||
control = (
|
||||
hasView(field.type) ? View(field.type) : (
|
||||
field.id ? MappedFormControl : StaticMappedFormControl
|
||||
)
|
||||
);
|
||||
|
||||
if (inlineGroup) {
|
||||
controlProps['withContainer'] = false;
|
||||
controlProps['controlGridBasis'] = 3;
|
||||
}
|
||||
|
||||
controlProps['className'] = field.isFullTab ? '' : (
|
||||
isPropertyMode ? 'Properties-controlRow' : 'FormView-controlRow'
|
||||
);
|
||||
|
||||
if (field.id) {
|
||||
controlProps['id'] = field.id;
|
||||
controlProps['onChange'] = (changeValue) => {
|
||||
// Get the changes on dependent fields as well.
|
||||
dataDispatch?.({
|
||||
type: SCHEMA_STATE_ACTIONS.SET_VALUE,
|
||||
path: controlProps.accessPath,
|
||||
value: changeValue,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Use custom control over the standard one.
|
||||
if (field.CustomControl) {
|
||||
control = field.CustomControl;
|
||||
}
|
||||
|
||||
if (isPropertyMode) field.helpMessage = '';
|
||||
|
||||
// Its a form control.
|
||||
if (_.isEqual(accessPath.concat(field.id), schemaState.errors?.name))
|
||||
currentGroup.hasError = true;
|
||||
|
||||
(inlineGroup || currentGroup).controls.push({control, controlProps});
|
||||
});
|
||||
|
||||
return groups.filter(
|
||||
(group) => (group.visible && group.controls.length)
|
||||
);
|
||||
};
|
17
web/pgadmin/static/js/SchemaView/utils/index.js
Normal file
17
web/pgadmin/static/js/SchemaView/utils/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { createFieldControls } from './createFieldControls';
|
||||
import { listenDepChanges } from './listenDepChanges';
|
||||
|
||||
|
||||
export {
|
||||
createFieldControls,
|
||||
listenDepChanges,
|
||||
};
|
57
web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js
Normal file
57
web/pgadmin/static/js/SchemaView/utils/listenDepChanges.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { evalFunc } from 'sources/utils';
|
||||
|
||||
|
||||
export const listenDepChanges = (accessPath, field, visible, schemaState) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !schemaState || !field) return;
|
||||
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState.addDepListener(
|
||||
accessPath, accessPath,
|
||||
field.depChange, field.deferredDepChange
|
||||
);
|
||||
}
|
||||
|
||||
if (field.deps) {
|
||||
const parentPath = [...accessPath];
|
||||
|
||||
// Remove the last element.
|
||||
if (field.id && field.id === parentPath[parentPath.length - 1]) {
|
||||
parentPath.pop();
|
||||
}
|
||||
|
||||
(evalFunc(null, field.deps) || []).forEach((dep) => {
|
||||
|
||||
// When dep is a string then prepend the complete accessPath,
|
||||
// but - when dep is an array, then the intention is to provide
|
||||
// the exact accesspath.
|
||||
let source = _.isArray(dep) ? dep : parentPath.concat(dep);
|
||||
|
||||
if(field.depChange || field.deferredDepChange) {
|
||||
schemaState.addDepListener(
|
||||
source, accessPath, field.depChange, field.deferredDepChange
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Cleanup the listeners when unmounting.
|
||||
schemaState.removeDepListener(accessPath);
|
||||
};
|
||||
}, []);
|
||||
|
||||
};
|
@@ -80,6 +80,9 @@ const Root = styled('div')(({theme}) => ({
|
||||
backgroundColor: theme.otherVars.borderColor,
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
'& .Form-plainstring': {
|
||||
padding: theme.spacing(0.5),
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -351,12 +354,23 @@ export const InputText = forwardRef(({
|
||||
cid, helpid, readonly, disabled, value, onChange, controlProps, type, size, inputStyle, ...props }, ref) => {
|
||||
|
||||
const maxlength = typeof(controlProps?.maxLength) != 'undefined' ? controlProps.maxLength : 255;
|
||||
|
||||
const patterns = {
|
||||
'numeric': '^-?[0-9]\\d*\\.?\\d*$',
|
||||
'int': '^-?[0-9]\\d*$',
|
||||
};
|
||||
let onChangeFinal = (e) => {
|
||||
|
||||
let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;
|
||||
|
||||
if (controlProps?.formatter) {
|
||||
finalValue = controlProps.formatter.fromRaw(finalValue);
|
||||
}
|
||||
|
||||
if (_.isNull(finalValue) || _.isUndefined(finalValue)) finalValue = '';
|
||||
|
||||
const [val, setVal] = useState(finalValue);
|
||||
|
||||
useEffect(() => setVal(finalValue), [finalValue]);
|
||||
const onChangeFinal = (e) => {
|
||||
let changeVal = e.target.value;
|
||||
|
||||
/* For type number, we set type as tel with number regex to get validity.*/
|
||||
@@ -368,14 +382,10 @@ export const InputText = forwardRef(({
|
||||
if (controlProps?.formatter) {
|
||||
changeVal = controlProps.formatter.toRaw(changeVal);
|
||||
}
|
||||
setVal(changeVal);
|
||||
onChange?.(changeVal);
|
||||
};
|
||||
|
||||
let finalValue = (_.isNull(value) || _.isUndefined(value)) ? '' : value;
|
||||
|
||||
if (controlProps?.formatter) {
|
||||
finalValue = controlProps.formatter.fromRaw(finalValue);
|
||||
}
|
||||
|
||||
const filteredProps = _.pickBy(props, (_v, key)=>(
|
||||
/* When used in ButtonGroup, following props should be skipped */
|
||||
@@ -403,7 +413,7 @@ export const InputText = forwardRef(({
|
||||
disabled={Boolean(disabled)}
|
||||
rows={4}
|
||||
notched={false}
|
||||
value={(_.isNull(finalValue) || _.isUndefined(finalValue)) ? '' : finalValue}
|
||||
value={val}
|
||||
onChange={onChangeFinal}
|
||||
{
|
||||
...(controlProps?.onKeyDown && { onKeyDown: controlProps.onKeyDown })
|
||||
@@ -626,7 +636,6 @@ export function InputRadio({ helpid, value, onChange, controlProps, readonly, la
|
||||
inputProps={{ 'aria-label': value, 'aria-describedby': helpid }}
|
||||
style={{ padding: 0 }}
|
||||
disableRipple
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
label={controlProps.label}
|
||||
@@ -1110,7 +1119,9 @@ export function PlainString({ controlProps, value }) {
|
||||
if (controlProps?.formatter) {
|
||||
finalValue = controlProps.formatter.fromRaw(finalValue);
|
||||
}
|
||||
return <span>{finalValue}</span>;
|
||||
return <Root>
|
||||
<div className="Form-plainstring">{finalValue}</div>
|
||||
</Root>;
|
||||
}
|
||||
PlainString.propTypes = {
|
||||
controlProps: PropTypes.object,
|
||||
|
@@ -14,7 +14,6 @@ import PropTypes from 'prop-types';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
||||
import { PgIconButton } from './Buttons';
|
||||
@@ -441,18 +440,6 @@ export function getCheckboxHeaderCell({title}) {
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export function getReorderCell() {
|
||||
const Cell = () => {
|
||||
return <div className='reorder-cell'>
|
||||
<DragIndicatorRoundedIcon fontSize="small" />
|
||||
</div>;
|
||||
};
|
||||
|
||||
Cell.displayName = 'ReorderCell';
|
||||
|
||||
return Cell;
|
||||
}
|
||||
|
||||
export function getEditCell({isDisabled, title}) {
|
||||
const Cell = ({ row }) => {
|
||||
return <PgIconButton data-test="expand-row" title={title} icon={<EditRoundedIcon fontSize="small" />} className='pgrt-cell-button'
|
||||
|
@@ -8,7 +8,10 @@
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
@@ -24,29 +27,29 @@ import {
|
||||
keepPreviousData,
|
||||
} from '@tanstack/react-query';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import { InputText } from './FormComponents';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {
|
||||
BaseUISchema, FormView, SchemaStateContext, useSchemaState, prepareData,
|
||||
} from 'sources/SchemaView';
|
||||
import gettext from 'sources/gettext';
|
||||
import SchemaView from '../SchemaView';
|
||||
|
||||
import EmptyPanelMessage from './EmptyPanelMessage';
|
||||
import { InputText } from './FormComponents';
|
||||
import { PgReactTable, PgReactTableBody, PgReactTableCell, PgReactTableHeader, PgReactTableRow, PgReactTableRowContent, PgReactTableRowExpandContent, getCheckboxCell, getCheckboxHeaderCell } from './PgReactTableStyled';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
|
||||
const ROW_HEIGHT = 30;
|
||||
function TableRow({ index, style, schema, row, measureElement}) {
|
||||
const [expandComplete, setExpandComplete] = React.useState(false);
|
||||
|
||||
function TableRow({index, style, schema, row, measureElement}) {
|
||||
const rowRef = React.useRef();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (rowRef.current) {
|
||||
if (!expandComplete && rowRef.current.style.height == `${ROW_HEIGHT}px`) {
|
||||
return;
|
||||
}
|
||||
if (rowRef.current.style.height == `${ROW_HEIGHT}px`) return;
|
||||
measureElement(rowRef.current);
|
||||
}
|
||||
}, [row.getIsExpanded(), expandComplete]);
|
||||
}, [row.getIsExpanded()]);
|
||||
|
||||
return (
|
||||
<PgReactTableRow data-index={index} ref={rowRef} style={style}>
|
||||
@@ -62,12 +65,10 @@ function TableRow({ index, style, schema, row, measureElement}) {
|
||||
})}
|
||||
</PgReactTableRowContent>
|
||||
<PgReactTableRowExpandContent row={row}>
|
||||
<SchemaView
|
||||
getInitData={() => Promise.resolve(row.original)}
|
||||
viewHelperProps={{ mode: 'properties' }}
|
||||
<FormView
|
||||
accessPath={['data', index]}
|
||||
schema={schema}
|
||||
showFooter={false}
|
||||
onDataChange={() => { setExpandComplete(true); }}
|
||||
viewHelperProps={{ mode: 'properties' }}
|
||||
/>
|
||||
</PgReactTableRowExpandContent>
|
||||
</PgReactTableRow>
|
||||
@@ -81,7 +82,43 @@ TableRow.propTypes = {
|
||||
measureElement: PropTypes.func,
|
||||
};
|
||||
|
||||
export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableProps, searchVal, loadNextPage, ...props }) {
|
||||
|
||||
class TableUISchema extends BaseUISchema {
|
||||
constructor(rowSchema) {
|
||||
super();
|
||||
this.rowSchema = rowSchema;
|
||||
}
|
||||
|
||||
get baseFields() {
|
||||
return [{
|
||||
id: 'data', type: 'collection', mode: ['properties'],
|
||||
schema: this.rowSchema,
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
const getTableSchema = (schema) => {;
|
||||
if (!schema) return null;
|
||||
if (!schema.top) schema.top = new TableUISchema(schema);
|
||||
return schema.top;
|
||||
};
|
||||
|
||||
export function Table({
|
||||
columns, data, hasSelectRow, schema, sortOptions, tableProps, searchVal,
|
||||
loadNextPage, ...props
|
||||
}) {
|
||||
const { schemaState } = useSchemaState({
|
||||
schema: getTableSchema(schema),
|
||||
getInitData: null,
|
||||
viewHelperProps: {mode: 'properties'},
|
||||
});
|
||||
|
||||
// We don't care about validation in static table, hence - initialising the
|
||||
// data directly.
|
||||
if (data.length && schemaState) {
|
||||
schemaState.initData = schemaState.data = prepareData({'data': data});
|
||||
}
|
||||
|
||||
const defaultColumn = React.useMemo(
|
||||
() => ({
|
||||
size: 150,
|
||||
@@ -103,11 +140,15 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
|
||||
enableResizing: false,
|
||||
maxSize: 35,
|
||||
}] : []).concat(
|
||||
columns.filter((c)=>_.isUndefined(c.enableVisibility) ? true : c.enableVisibility).map((c)=>({
|
||||
columns.filter(
|
||||
(c) => _.isUndefined(c.enableVisibility) ? true : c.enableVisibility
|
||||
).map((c) => ({
|
||||
...c,
|
||||
// if data is null then global search doesn't work
|
||||
// Use accessorFn to return empty string if data is null.
|
||||
accessorFn: c.accessorFn ?? (c.accessorKey ? (row)=>row[c.accessorKey] ?? '' : undefined),
|
||||
accessorFn: c.accessorFn ?? (
|
||||
c.accessorKey ? (row) => row[c.accessorKey] ?? '' : undefined
|
||||
),
|
||||
}))
|
||||
), [hasSelectRow, columns]);
|
||||
|
||||
@@ -118,24 +159,24 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
|
||||
let totalFetched = 0;
|
||||
let totalDBRowCount = 0;
|
||||
|
||||
//Infinite scrolling
|
||||
const { _data, fetchNextPage, isFetching } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ['logs'],
|
||||
queryFn: async () => {
|
||||
const fetchedData = loadNextPage ? await loadNextPage() : [];
|
||||
return fetchedData;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastGroup, groups) => groups.length,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
// Infinite scrolling
|
||||
const { _data, fetchNextPage, isFetching } = useInfiniteQuery({
|
||||
queryKey: ['logs'],
|
||||
queryFn: async () => {
|
||||
const fetchedData = loadNextPage ? await loadNextPage() : [];
|
||||
return fetchedData;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (_lastGroup, groups) => groups.length,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
flatData = _data || [];
|
||||
totalFetched = flatData.length;
|
||||
|
||||
//called on scroll and possibly on mount to fetch more data as the user scrolls and reaches bottom of table
|
||||
// Called on scroll and possibly on mount to fetch more data as the user
|
||||
// scrolls and reaches bottom of table.
|
||||
fetchMoreOnBottomReached = React.useCallback(
|
||||
(containerRefElement = HTMLDivElement | null) => {
|
||||
if (containerRefElement) {
|
||||
@@ -194,22 +235,31 @@ export function Table({ columns, data, hasSelectRow, schema, sortOptions, tableP
|
||||
});
|
||||
|
||||
return (
|
||||
<PgReactTable ref={tableRef} table={table} onScrollFunc={loadNextPage?fetchMoreOnBottomReached: null }>
|
||||
<PgReactTableHeader table={table} />
|
||||
{rows.length == 0 ?
|
||||
<EmptyPanelMessage text={gettext('No rows found')} style={{height:'auto'}} /> :
|
||||
<PgReactTableBody style={{ height: virtualizer.getTotalSize() + 'px'}}>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
return <TableRow index={virtualRow.index} key={virtualRow.index} row={row} schema={schema}
|
||||
measureElement={virtualizer.measureElement}
|
||||
style={{
|
||||
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
|
||||
}}
|
||||
/>;
|
||||
})}
|
||||
</PgReactTableBody>}
|
||||
</PgReactTable>
|
||||
<SchemaStateContext.Provider value={schemaState}>
|
||||
<PgReactTable
|
||||
ref={tableRef} table={table}
|
||||
onScrollFunc={loadNextPage?fetchMoreOnBottomReached: null }
|
||||
>
|
||||
<PgReactTableHeader table={table} />
|
||||
{rows.length == 0 ? <EmptyPanelMessage
|
||||
text={gettext('No rows found')} style={{height:'auto'}}/> :
|
||||
<PgReactTableBody
|
||||
style={{ height: virtualizer.getTotalSize() + 'px'}}>
|
||||
{virtualizer.getVirtualItems().map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
return <TableRow
|
||||
index={virtualRow.index} key={virtualRow.index}
|
||||
row={row} schema={schema}
|
||||
measureElement={virtualizer.measureElement}
|
||||
style={{
|
||||
// This should always be a `style` as it changes on scroll.
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
}}
|
||||
/>;
|
||||
})}
|
||||
</PgReactTableBody>}
|
||||
</PgReactTable>
|
||||
</SchemaStateContext.Provider>
|
||||
);
|
||||
}
|
||||
Table.propTypes = {
|
||||
|
@@ -10,7 +10,10 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import PropTypes from 'prop-types';
|
||||
import { checkTrojanSource } from '../../../utils';
|
||||
|
||||
import { useIsMounted } from 'sources/custom_hooks';
|
||||
|
||||
import { checkTrojanSource } from 'sources/utils';
|
||||
import usePreferences from '../../../../../preferences/static/js/store';
|
||||
import KeyboardArrowRightRoundedIcon from '@mui/icons-material/KeyboardArrowRightRounded';
|
||||
import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded';
|
||||
@@ -147,9 +150,12 @@ const defaultExtensions = [
|
||||
];
|
||||
|
||||
export default function Editor({
|
||||
currEditor, name, value, options, onCursorActivity, onChange, readonly, disabled, autocomplete = false,
|
||||
breakpoint = false, onBreakPointChange, showActiveLine=false,
|
||||
keepHistory = true, cid, helpid, labelledBy, customKeyMap, language='pgsql'}) {
|
||||
currEditor, name, value, options, onCursorActivity, onChange, readonly,
|
||||
disabled, autocomplete = false, breakpoint = false, onBreakPointChange,
|
||||
showActiveLine=false, keepHistory = true, cid, helpid, labelledBy,
|
||||
customKeyMap, language='pgsql'
|
||||
}) {
|
||||
const checkIsMounted = useIsMounted();
|
||||
|
||||
const editorContainerRef = useRef();
|
||||
const editor = useRef();
|
||||
@@ -166,6 +172,7 @@ export default function Editor({
|
||||
const editableConfig = useRef(new Compartment());
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkIsMounted()) return;
|
||||
const finalOptions = { ...defaultOptions, ...options };
|
||||
const finalExtns = [
|
||||
(language == 'json') ? json() : sql({dialect: PgSQL}),
|
||||
@@ -248,6 +255,7 @@ export default function Editor({
|
||||
}, []);
|
||||
|
||||
useMemo(() => {
|
||||
if (!checkIsMounted()) return;
|
||||
if(editor.current) {
|
||||
if(value != editor.current.getValue()) {
|
||||
if(!_.isEmpty(value)) {
|
||||
@@ -259,14 +267,19 @@ export default function Editor({
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(()=>{
|
||||
const keys = keymap.of([customKeyMap??[], defaultKeymap, closeBracketsKeymap, historyKeymap, foldKeymap, completionKeymap].flat());
|
||||
useEffect(() => {
|
||||
if (!checkIsMounted()) return;
|
||||
const keys = keymap.of([
|
||||
customKeyMap??[], defaultKeymap, closeBracketsKeymap, historyKeymap,
|
||||
foldKeymap, completionKeymap
|
||||
].flat());
|
||||
editor.current?.dispatch({
|
||||
effects: shortcuts.current.reconfigure(keys)
|
||||
});
|
||||
}, [customKeyMap]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkIsMounted()) return;
|
||||
let pref = preferencesStore.getPreferencesForModule('sqleditor');
|
||||
let newConfigExtn = [];
|
||||
|
||||
@@ -361,6 +374,7 @@ export default function Editor({
|
||||
}, [preferencesStore]);
|
||||
|
||||
useMemo(() => {
|
||||
if (!checkIsMounted()) return;
|
||||
if (editor.current) {
|
||||
if (value != editor.current.getValue()) {
|
||||
editor.current.dispatch({
|
||||
@@ -371,6 +385,7 @@ export default function Editor({
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkIsMounted()) return;
|
||||
editor.current?.dispatch({
|
||||
effects: editableConfig.current.reconfigure([
|
||||
EditorView.editable.of(editable),
|
||||
@@ -379,7 +394,7 @@ export default function Editor({
|
||||
});
|
||||
}, [readonly, disabled, keepHistory]);
|
||||
|
||||
return useMemo(()=>(
|
||||
return useMemo(() => (
|
||||
<div style={{ height: '100%' }} ref={editorContainerRef} name={name}></div>
|
||||
), []);
|
||||
}
|
||||
|
49
web/pgadmin/static/js/components/SearchInputText.jsx
Normal file
49
web/pgadmin/static/js/components/SearchInputText.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { InputText } from 'sources/components/FormComponents';
|
||||
import gettext from 'sources/gettext';
|
||||
|
||||
|
||||
export const SEARCH_INPUT_SIZE = {
|
||||
FULL: 'full',
|
||||
HALF: 'half',
|
||||
};
|
||||
|
||||
export const SEARCH_INPUT_ALIGNMENT = {
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right'
|
||||
};
|
||||
|
||||
export const SearchInputText = ({
|
||||
searchText, onChange, placeholder, size, alignment
|
||||
}) => {
|
||||
const props = {
|
||||
placeholder: placeholder || gettext('Search'),
|
||||
style: {
|
||||
width: size == SEARCH_INPUT_SIZE.FULL ? '100%' : '50%',
|
||||
float: alignment == SEARCH_INPUT_ALIGNMENT.RIGHT ? 'right' : 'left',
|
||||
},
|
||||
value: searchText,
|
||||
onChange,
|
||||
};
|
||||
|
||||
return <InputText {...props}/>;
|
||||
};
|
||||
|
||||
SearchInputText.propTypes = {
|
||||
searchText: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
size: PropTypes.oneOf(Object.values(SEARCH_INPUT_SIZE)),
|
||||
alignment: PropTypes.oneOf(Object.values(SEARCH_INPUT_ALIGNMENT)),
|
||||
};
|
@@ -1,103 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { Box } from '@mui/material';
|
||||
import DataGridView, { DataGridHeader } from '../SchemaView/DataGridView';
|
||||
import SchemaView, { SCHEMA_STATE_ACTIONS } from '../SchemaView';
|
||||
import { DefaultButton } from '../components/Buttons';
|
||||
import { evalFunc } from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import CustomPropTypes from '../custom_prop_types';
|
||||
import _ from 'lodash';
|
||||
|
||||
const StyledBox = styled(Box)(({theme}) => ({
|
||||
'& .DataGridViewWithHeaderForm-border': {
|
||||
...theme.mixins.panelBorder,
|
||||
borderBottom: 0,
|
||||
'& .DataGridViewWithHeaderForm-body': {
|
||||
padding: '0.25rem',
|
||||
'& .DataGridViewWithHeaderForm-addBtn': {
|
||||
marginLeft: 'auto',
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export default function DataGridViewWithHeaderForm(props) {
|
||||
let {containerClassName, headerSchema, headerVisible, ...otherProps} = props;
|
||||
|
||||
const headerFormData = useRef({});
|
||||
const schemaRef = useRef(otherProps.schema);
|
||||
const [addDisabled, setAddDisabled] = useState(true);
|
||||
const [headerFormResetKey, setHeaderFormResetKey] = useState(0);
|
||||
const onAddClick = useCallback(()=>{
|
||||
if(!otherProps.canAddRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newRow = headerSchema.getNewData(headerFormData.current);
|
||||
otherProps.dataDispatch({
|
||||
type: SCHEMA_STATE_ACTIONS.ADD_ROW,
|
||||
path: otherProps.accessPath,
|
||||
value: newRow,
|
||||
});
|
||||
setHeaderFormResetKey((preVal)=>preVal+1);
|
||||
}, []);
|
||||
|
||||
useEffect(()=>{
|
||||
headerSchema.top = schemaRef.current.top;
|
||||
}, []);
|
||||
|
||||
let state = schemaRef.current.top ? _.get(schemaRef.current.top.sessData, _.slice(otherProps.accessPath, 0, -1))
|
||||
: _.get(schemaRef.current.sessData);
|
||||
|
||||
headerVisible = headerVisible && evalFunc(null, headerVisible, state);
|
||||
return (
|
||||
<StyledBox className={containerClassName}>
|
||||
<Box className='DataGridViewWithHeaderForm-border'>
|
||||
{props.label && <DataGridHeader label={props.label} />}
|
||||
{headerVisible && <Box className='DataGridViewWithHeaderForm-body'>
|
||||
<SchemaView
|
||||
formType={'dialog'}
|
||||
getInitData={()=>Promise.resolve({})}
|
||||
schema={headerSchema}
|
||||
viewHelperProps={props.viewHelperProps}
|
||||
showFooter={false}
|
||||
onDataChange={(isDataChanged, dataChanged)=>{
|
||||
headerFormData.current = dataChanged;
|
||||
setAddDisabled(headerSchema.addDisabled(headerFormData.current));
|
||||
}}
|
||||
hasSQL={false}
|
||||
isTabView={false}
|
||||
resetKey={headerFormResetKey}
|
||||
/>
|
||||
<Box display="flex">
|
||||
<DefaultButton className='DataGridViewWithHeaderForm-addBtn' onClick={onAddClick} disabled={addDisabled}>Add</DefaultButton>
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>
|
||||
<DataGridView {...otherProps} label="" canAdd={false}/>
|
||||
</StyledBox>
|
||||
);
|
||||
}
|
||||
|
||||
DataGridViewWithHeaderForm.propTypes = {
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.array,
|
||||
viewHelperProps: PropTypes.object,
|
||||
formErr: PropTypes.object,
|
||||
headerSchema: CustomPropTypes.schemaUI.isRequired,
|
||||
headerVisible: PropTypes.func,
|
||||
schema: CustomPropTypes.schemaUI,
|
||||
accessPath: PropTypes.array.isRequired,
|
||||
dataDispatch: PropTypes.func.isRequired,
|
||||
containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||
};
|
@@ -23,7 +23,7 @@ export default function withStandardTabInfo(Component, tabId) {
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const layoutDocker = useContext(LayoutDockerContext);
|
||||
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
const i = pgAdmin.Browser.tree?.selected();
|
||||
if(i) {
|
||||
setNodeInfo([true, i, pgAdmin.Browser.tree.itemData(i)]);
|
||||
@@ -38,22 +38,24 @@ export default function withStandardTabInfo(Component, tabId) {
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const onUpdate = (item, data)=>{
|
||||
setNodeInfo([true, item, data]);
|
||||
const onUpdate = () => {
|
||||
// Only use the selected tree node item.
|
||||
const item = pgAdmin.Browser.tree?.selected();
|
||||
setNodeInfo([
|
||||
true, item, item && pgAdmin.Browser.tree.itemData(item)
|
||||
]);
|
||||
};
|
||||
|
||||
let destroyTree = pgAdmin.Browser.Events.on('pgadmin-browser:tree:destroyed', onUpdate);
|
||||
let deregisterTree = pgAdmin.Browser.Events.on('pgadmin-browser:node:selected', onUpdate);
|
||||
let deregisterTreeUpdate = pgAdmin.Browser.Events.on('pgadmin-browser:tree:updated', onUpdate);
|
||||
let deregisterDbConnected = pgAdmin.Browser.Events.on('pgadmin:database:connected', onUpdate);
|
||||
let deregisterServerConnected = pgAdmin.Browser.Events.on('pgadmin:server:connected', (_sid, item, data)=>{
|
||||
setNodeInfo([true, item, data]);
|
||||
});
|
||||
let deregisterServerConnected = pgAdmin.Browser.Events.on('pgadmin:server:connected', onUpdate);
|
||||
let deregisterActive = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.ACTIVE, onTabActive);
|
||||
// if there is any dock changes to the tab and it appears to be active/inactive
|
||||
let deregisterChange = layoutDocker.eventBus.registerListener(LAYOUT_EVENTS.CHANGE, onTabActive);
|
||||
|
||||
return ()=>{
|
||||
return () => {
|
||||
onTabActive?.cancel();
|
||||
destroyTree();
|
||||
deregisterTree();
|
||||
|
@@ -677,3 +677,62 @@ export function getChartColor(index, theme='light', colorPalette=CHART_THEME_COL
|
||||
// loop back if out of index;
|
||||
return palette[index % palette.length];
|
||||
}
|
||||
|
||||
// Using this function instead of 'btoa' directly.
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
|
||||
function stringToBase64(str) {
|
||||
return btoa(
|
||||
Array.from(
|
||||
new TextEncoder().encode(str),
|
||||
(byte) => String.fromCodePoint(byte),
|
||||
).join('')
|
||||
);
|
||||
}
|
||||
|
||||
/************************************
|
||||
*
|
||||
* Memoization of a function.
|
||||
*
|
||||
* NOTE: Please don't use the function, when:
|
||||
* - One of the parameter in the arguments could have a 'circular' dependency.
|
||||
* NOTE: We use `JSON.stringify(...)` for all the arguments.`You could
|
||||
* introduce 'Object.prototype.toJSON(...)' function for the object
|
||||
* with circular dependency, which should return a JSON object without
|
||||
* it.
|
||||
* - It returns a Promise object (asynchronous functions).
|
||||
*
|
||||
* Consider to use 'https://github.com/sindresorhus/p-memoize' for an
|
||||
* asychronous functions.
|
||||
*
|
||||
**/
|
||||
export const memoizeFn = fn => new Proxy(fn, {
|
||||
cache: new Map(),
|
||||
apply (target, thisArg, argsList) {
|
||||
let cacheKey = stringToBase64(JSON.stringify(argsList));
|
||||
if(!this.cache.has(cacheKey)) {
|
||||
this.cache.set(cacheKey, target.apply(thisArg, argsList));
|
||||
}
|
||||
return this.cache.get(cacheKey);
|
||||
}
|
||||
});
|
||||
|
||||
export const memoizeTimeout = (fn, time) => new Proxy(fn, {
|
||||
cache: new Map(),
|
||||
apply (target, thisArg, argsList) {
|
||||
const cacheKey = stringToBase64(JSON.stringify(argsList));
|
||||
const cached = this.cache.get(cacheKey);
|
||||
const timeoutId = setTimeout(() => (this.cache.delete(cacheKey)), time);
|
||||
|
||||
if (cached) {
|
||||
clearInterval(cached.timeoutId);
|
||||
cached.timeoutId = timeoutId;
|
||||
|
||||
return cached.result;
|
||||
}
|
||||
|
||||
const result = target.apply(thisArg, argsList);
|
||||
this.cache.set(cacheKey, {result, timeoutId});
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
@@ -40,7 +40,7 @@ export class SectionSchema extends BaseUISchema {
|
||||
state.only_tablespaces ||
|
||||
state.only_roles;
|
||||
},
|
||||
inlineNext: true,
|
||||
inlineGroup: 'section',
|
||||
}, {
|
||||
id: 'data',
|
||||
label: gettext('Data'),
|
||||
@@ -53,6 +53,7 @@ export class SectionSchema extends BaseUISchema {
|
||||
state.only_tablespaces ||
|
||||
state.only_roles;
|
||||
},
|
||||
inlineGroup: 'section',
|
||||
}, {
|
||||
id: 'post_data',
|
||||
label: gettext('Post-data'),
|
||||
@@ -65,6 +66,7 @@ export class SectionSchema extends BaseUISchema {
|
||||
state.only_tablespaces ||
|
||||
state.only_roles;
|
||||
},
|
||||
inlineGroup: 'section',
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -105,7 +107,7 @@ export class TypeObjSchema extends BaseUISchema {
|
||||
state.only_tablespaces ||
|
||||
state.only_roles;
|
||||
},
|
||||
inlineNext: true,
|
||||
inlineGroup: 'type_of_objects',
|
||||
}, {
|
||||
id: 'only_schema',
|
||||
label: gettext('Only schemas'),
|
||||
@@ -121,7 +123,7 @@ export class TypeObjSchema extends BaseUISchema {
|
||||
state.only_tablespaces ||
|
||||
state.only_roles;
|
||||
},
|
||||
inlineNext: true,
|
||||
inlineGroup: 'type_of_objects',
|
||||
}, {
|
||||
id: 'only_tablespaces',
|
||||
label: gettext('Only tablespaces'),
|
||||
@@ -137,8 +139,8 @@ export class TypeObjSchema extends BaseUISchema {
|
||||
state.only_schema ||
|
||||
state.only_roles;
|
||||
},
|
||||
visible: isVisibleForObjectBackup(obj?._top?.backupType),
|
||||
inlineNext: true,
|
||||
visible: isVisibleForObjectBackup(obj?.top?.backupType),
|
||||
inlineGroup: 'type_of_objects',
|
||||
}, {
|
||||
id: 'only_roles',
|
||||
label: gettext('Only roles'),
|
||||
@@ -146,6 +148,7 @@ export class TypeObjSchema extends BaseUISchema {
|
||||
group: gettext('Type of objects'),
|
||||
deps: ['pre_data', 'data', 'post_data', 'only_data', 'only_schema',
|
||||
'only_tablespaces'],
|
||||
inlineGroup: 'type_of_objects',
|
||||
disabled: function(state) {
|
||||
return state.pre_data ||
|
||||
state.data ||
|
||||
@@ -154,14 +157,15 @@ export class TypeObjSchema extends BaseUISchema {
|
||||
state.only_schema ||
|
||||
state.only_tablespaces;
|
||||
},
|
||||
visible: isVisibleForObjectBackup(obj?._top?.backupType)
|
||||
visible: isVisibleForObjectBackup(obj?.top?.backupType)
|
||||
}, {
|
||||
id: 'blobs',
|
||||
label: gettext('Blobs'),
|
||||
type: 'switch',
|
||||
group: gettext('Type of objects'),
|
||||
inlineGroup: 'type_of_objects',
|
||||
visible: function(state) {
|
||||
if (!isVisibleForServerBackup(obj?._top?.backupType)) {
|
||||
if (!isVisibleForServerBackup(obj?.top?.backupType)) {
|
||||
state.blobs = false;
|
||||
return false;
|
||||
}
|
||||
@@ -200,43 +204,43 @@ export class SaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
}, {
|
||||
id: 'dns_no_role_passwords',
|
||||
label: gettext('Role passwords'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
visible: isVisibleForObjectBackup(obj?._top?.backupType),
|
||||
inlineNext: true,
|
||||
visible: isVisibleForObjectBackup(obj?.top?.backupType),
|
||||
inlineGroup: 'do_not_save',
|
||||
}, {
|
||||
id: 'dns_privilege',
|
||||
label: gettext('Privileges'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
}, {
|
||||
id: 'dns_tablespace',
|
||||
label: gettext('Tablespaces'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
}, {
|
||||
id: 'dns_unlogged_tbl_data',
|
||||
label: gettext('Unlogged table data'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
}, {
|
||||
id: 'dns_comments',
|
||||
label: gettext('Comments'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_publications',
|
||||
@@ -244,7 +248,7 @@ export class SaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_subscriptions',
|
||||
@@ -252,7 +256,7 @@ export class SaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_security_labels',
|
||||
@@ -260,7 +264,7 @@ export class SaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_toast_compression',
|
||||
@@ -268,7 +272,7 @@ export class SaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
min_version: 140000
|
||||
}, {
|
||||
id: 'dns_table_access_method',
|
||||
@@ -276,7 +280,7 @@ export class SaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'do_not_save',
|
||||
min_version: 150000
|
||||
}];
|
||||
}
|
||||
@@ -322,13 +326,14 @@ export class DisabledOptionSchema extends BaseUISchema {
|
||||
disabled: function(state) {
|
||||
return !(state.only_data);
|
||||
},
|
||||
inlineNext: true,
|
||||
inlineGroup: 'disable',
|
||||
}, {
|
||||
id: 'disable_quoting',
|
||||
label: gettext('$ quoting'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Disable'),
|
||||
inlineGroup: 'disable',
|
||||
}];
|
||||
}
|
||||
}
|
||||
@@ -364,28 +369,29 @@ export class MiscellaneousSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Miscellaneous'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'dqoute',
|
||||
label: gettext('Force double quote on identifiers'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Miscellaneous'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'use_set_session_auth',
|
||||
label: gettext('Use SET SESSION AUTHORIZATION'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Miscellaneous'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'exclude_schema',
|
||||
label: gettext('Exclude schema'),
|
||||
type: 'select',
|
||||
disabled: false,
|
||||
group: gettext('Miscellaneous'),
|
||||
visible: isVisibleForServerBackup(obj?._top?.backupType),
|
||||
inlineGroup: 'miscellaneous',
|
||||
visible: isVisibleForServerBackup(obj?.top?.backupType),
|
||||
controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' }
|
||||
}, {
|
||||
id: 'exclude_database',
|
||||
@@ -394,7 +400,7 @@ export class MiscellaneousSchema extends BaseUISchema {
|
||||
disabled: false,
|
||||
min_version: 160000,
|
||||
group: gettext('Miscellaneous'),
|
||||
visible: isVisibleForObjectBackup(obj?._top?.backupType),
|
||||
visible: isVisibleForObjectBackup(obj?.top?.backupType),
|
||||
controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' }
|
||||
}, {
|
||||
id: 'extra_float_digits',
|
||||
@@ -440,7 +446,7 @@ export class ExcludePatternsSchema extends BaseUISchema {
|
||||
type: 'select',
|
||||
disabled: false,
|
||||
group: gettext('Table Options'),
|
||||
visible: isVisibleForServerBackup(obj?._top?.backupType),
|
||||
visible: isVisibleForServerBackup(obj?.top?.backupType),
|
||||
controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' }
|
||||
}, {
|
||||
id: 'exclude_table_data',
|
||||
@@ -448,7 +454,7 @@ export class ExcludePatternsSchema extends BaseUISchema {
|
||||
type: 'select',
|
||||
disabled: false,
|
||||
group: gettext('Table Options'),
|
||||
visible: isVisibleForServerBackup(obj?._top?.backupType),
|
||||
visible: isVisibleForServerBackup(obj?.top?.backupType),
|
||||
controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' }
|
||||
}, {
|
||||
id: 'exclude_table_and_children',
|
||||
@@ -457,7 +463,7 @@ export class ExcludePatternsSchema extends BaseUISchema {
|
||||
disabled: false,
|
||||
group: gettext('Table Options'),
|
||||
min_version: 160000,
|
||||
visible: isVisibleForServerBackup(obj?._top?.backupType),
|
||||
visible: isVisibleForServerBackup(obj?.top?.backupType),
|
||||
controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' }
|
||||
}, {
|
||||
id: 'exclude_table_data_and_children',
|
||||
@@ -466,7 +472,7 @@ export class ExcludePatternsSchema extends BaseUISchema {
|
||||
disabled: false,
|
||||
group: gettext('Table Options'),
|
||||
min_version: 160000,
|
||||
visible: isVisibleForServerBackup(obj?._top?.backupType),
|
||||
visible: isVisibleForServerBackup(obj?.top?.backupType),
|
||||
controlProps: { multiple: true, allowClear: false, creatable: true, noDropdown: true, placeholder: ' ' }
|
||||
}];
|
||||
}
|
||||
@@ -638,7 +644,7 @@ export default class BackupSchema extends BaseUISchema {
|
||||
state.on_conflict_do_nothing = false;
|
||||
return true;
|
||||
},
|
||||
inlineNext: obj.backupType !== 'server',
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'include_create_database',
|
||||
label: gettext('Include CREATE DATABASE statement'),
|
||||
@@ -646,7 +652,7 @@ export default class BackupSchema extends BaseUISchema {
|
||||
disabled: false,
|
||||
group: gettext('Query Options'),
|
||||
visible: isVisibleForServerBackup(obj.backupType),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'include_drop_database',
|
||||
label: gettext('Include DROP DATABASE statement'),
|
||||
@@ -660,7 +666,7 @@ export default class BackupSchema extends BaseUISchema {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
inlineNext: true,
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'if_exists',
|
||||
label: gettext('Include IF EXISTS clause'),
|
||||
@@ -674,6 +680,7 @@ export default class BackupSchema extends BaseUISchema {
|
||||
state.if_exists = false;
|
||||
return true;
|
||||
},
|
||||
inlineGroup: 'miscellaneous',
|
||||
}, {
|
||||
id: 'use_column_inserts',
|
||||
label: gettext('Use Column INSERTS'),
|
||||
|
@@ -818,9 +818,9 @@ export default function DebuggerArgumentComponent({ debuggerInfo, restartDebug,
|
||||
onDataChange={(isChanged, changedData) => {
|
||||
let isValid = false;
|
||||
let skipStep = false;
|
||||
if ('_sessData' in debuggerArgsSchema.current) {
|
||||
if ('sessData' in debuggerArgsSchema.current) {
|
||||
isValid = true;
|
||||
debuggerArgsSchema.current._sessData.aregsCollection.forEach((data) => {
|
||||
debuggerArgsSchema.current.sessData.aregsCollection.forEach((data) => {
|
||||
|
||||
if (skipStep) { return; }
|
||||
|
||||
|
@@ -37,7 +37,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('FULL'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -53,7 +53,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('FREEZE'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -69,7 +69,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('ANALYZE'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -85,7 +85,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op', 'vacuum_full'],
|
||||
type: 'switch',
|
||||
label: gettext('DISABLE PAGE SKIPPING'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
disabled: function(state) {
|
||||
if (!obj.isApplicableForVacuum(state) || state.vacuum_full) {
|
||||
state.vacuum_disable_page_skipping = false;
|
||||
@@ -101,7 +101,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('SKIP LOCKED'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return state?.op ? (state.op == 'VACUUM' || state.op == 'ANALYZE') : false;
|
||||
},
|
||||
@@ -118,7 +118,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op', 'vacuum_full'],
|
||||
type: 'switch',
|
||||
label: gettext('TRUNCATE'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
disabled: function(state) {
|
||||
if (!obj.isApplicableForVacuum(state) || state.vacuum_full) {
|
||||
state.vacuum_truncate = false;
|
||||
@@ -135,7 +135,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('PROCESS TOAST'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -152,7 +152,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('PROCESS MAIN'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -169,7 +169,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('SKIP DATABASE STATS'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -186,7 +186,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
deps: ['op'],
|
||||
type: 'switch',
|
||||
label: gettext('ONLY DATABASE STATS'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'operations',
|
||||
visible: function(state) {
|
||||
return obj.isApplicableForVacuum(state);
|
||||
},
|
||||
@@ -202,6 +202,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
id: 'vacuum_index_cleanup',
|
||||
deps: ['op', 'vacuum_full'],
|
||||
type: 'select',
|
||||
inlineGroup: 'operations',
|
||||
label: gettext('INDEX CLEANUP'),
|
||||
controlProps: { allowClear: false, width: '100%' },
|
||||
options: function () {
|
||||
@@ -277,7 +278,7 @@ export class VacuumSchema extends BaseUISchema {
|
||||
return obj.isApplicableForReindex(state);
|
||||
},
|
||||
disabled: function(state) {
|
||||
if (!obj.isApplicableForReindex(state) || obj?._top?.nodeInfo?.schema) {
|
||||
if (!obj.isApplicableForReindex(state) || obj?.top?.nodeInfo?.schema) {
|
||||
state.reindex_system = false;
|
||||
return true;
|
||||
}
|
||||
|
@@ -42,7 +42,7 @@ export class RestoreSectionSchema extends BaseUISchema {
|
||||
label: gettext('Pre-data'),
|
||||
type: 'switch',
|
||||
group: gettext('Sections'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'sections',
|
||||
deps: ['only_data', 'only_schema'],
|
||||
disabled: function(state) {
|
||||
return obj.isDisabled(state);
|
||||
@@ -52,7 +52,7 @@ export class RestoreSectionSchema extends BaseUISchema {
|
||||
label: gettext('Data'),
|
||||
type: 'switch',
|
||||
group: gettext('Sections'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'sections',
|
||||
deps: ['only_data', 'only_schema'],
|
||||
disabled: function(state) {
|
||||
return obj.isDisabled(state);
|
||||
@@ -62,6 +62,7 @@ export class RestoreSectionSchema extends BaseUISchema {
|
||||
label: gettext('Post-data'),
|
||||
type: 'switch',
|
||||
group: gettext('Sections'),
|
||||
inlineGroup: 'sections',
|
||||
deps: ['only_data', 'only_schema'],
|
||||
disabled: function(state) {
|
||||
return obj.isDisabled(state);
|
||||
@@ -97,7 +98,7 @@ export class RestoreTypeObjSchema extends BaseUISchema {
|
||||
label: gettext('Only data'),
|
||||
type: 'switch',
|
||||
group: gettext('Type of objects'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'types_of_data',
|
||||
deps: ['pre_data', 'data', 'post_data', 'only_schema'],
|
||||
disabled: function(state) {
|
||||
if(obj.selectedNodeType == 'table') {
|
||||
@@ -115,6 +116,7 @@ export class RestoreTypeObjSchema extends BaseUISchema {
|
||||
label: gettext('Only schema'),
|
||||
type: 'switch',
|
||||
group: gettext('Type of objects'),
|
||||
inlineGroup: 'types_of_data',
|
||||
deps: ['pre_data', 'data', 'post_data', 'only_data'],
|
||||
disabled: function(state) {
|
||||
if(obj.selectedNodeType == 'index' || obj.selectedNodeType == 'function') {
|
||||
@@ -159,28 +161,28 @@ export class RestoreSaveOptSchema extends BaseUISchema {
|
||||
label: gettext('Owner'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
group: gettext('Do not save'),
|
||||
}, {
|
||||
id: 'dns_privilege',
|
||||
label: gettext('Privileges'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
group: gettext('Do not save'),
|
||||
}, {
|
||||
id: 'dns_tablespace',
|
||||
label: gettext('Tablespaces'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
group: gettext('Do not save'),
|
||||
}, {
|
||||
id: 'dns_comments',
|
||||
label: gettext('Comments'),
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
group: gettext('Do not save'),
|
||||
min_version: 110000
|
||||
}, {
|
||||
@@ -189,7 +191,7 @@ export class RestoreSaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_subscriptions',
|
||||
@@ -197,7 +199,7 @@ export class RestoreSaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_security_labels',
|
||||
@@ -205,7 +207,7 @@ export class RestoreSaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
min_version: 110000
|
||||
}, {
|
||||
id: 'dns_table_access_method',
|
||||
@@ -213,7 +215,7 @@ export class RestoreSaveOptSchema extends BaseUISchema {
|
||||
type: 'switch',
|
||||
disabled: false,
|
||||
group: gettext('Do not save'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'save_options',
|
||||
min_version: 150000
|
||||
}];
|
||||
}
|
||||
@@ -419,7 +421,7 @@ export default class RestoreSchema extends BaseUISchema {
|
||||
label: gettext('Clean before restore'),
|
||||
type: 'switch',
|
||||
group: gettext('Query Options'),
|
||||
inlineNext: true,
|
||||
inlineGroup: 'clean',
|
||||
disabled: function(state) {
|
||||
if(obj.selectedNodeType === 'function' || obj.selectedNodeType === 'trigger_function') {
|
||||
state.clean = true;
|
||||
@@ -431,6 +433,7 @@ export default class RestoreSchema extends BaseUISchema {
|
||||
label: gettext('Include IF EXISTS clause'),
|
||||
type: 'switch',
|
||||
group: gettext('Query Options'),
|
||||
inlineGroup: 'clean',
|
||||
deps: ['clean'],
|
||||
disabled: function(state) {
|
||||
if (state.clean) {
|
||||
|
@@ -27,7 +27,9 @@ class MacrosCollection extends BaseUISchema {
|
||||
}
|
||||
|
||||
/* Returns the new data row for the schema based on defaults and input */
|
||||
getNewData(current_macros, data={}) {
|
||||
getNewData(data={}) {
|
||||
const current_macros = this?.top?.sessData.macro;
|
||||
|
||||
let newRow = {};
|
||||
this.fields.forEach((field)=>{
|
||||
newRow[field.id] = this.defaults[field.id];
|
||||
@@ -36,7 +38,8 @@ class MacrosCollection extends BaseUISchema {
|
||||
...newRow,
|
||||
...data,
|
||||
};
|
||||
if (current_macros){
|
||||
|
||||
if (current_macros) {
|
||||
// Extract an array of existing names from the 'macro' collection
|
||||
const existingNames = current_macros.map(macro => macro.name);
|
||||
const newName = getRandomName(existingNames);
|
||||
|
@@ -161,7 +161,7 @@ function showFilterDialog(pgBrowser, item, queryToolMod, transId,
|
||||
let helpUrl = url_for('help.static', {'filename': 'viewdata_filter.html'});
|
||||
|
||||
let okCallback = function() {
|
||||
queryToolMod.launch(transId, gridUrl, false, queryToolTitle, {sql_filter: schema._sessData.filter_sql});
|
||||
queryToolMod.launch(transId, gridUrl, false, queryToolTitle, {sql_filter: schema.sessData.filter_sql});
|
||||
};
|
||||
|
||||
pgBrowser.Events.trigger('pgadmin:utility:show', item,
|
||||
|
@@ -166,8 +166,8 @@ class UserManagementCollection extends BaseUISchema {
|
||||
}
|
||||
|
||||
if (state.auth_source != AUTH_METHODS['INTERNAL']) {
|
||||
if (obj.isNew(state) && obj.top?._sessData?.userManagement) {
|
||||
for (let user of obj.top._sessData.userManagement) {
|
||||
if (obj.isNew(state) && obj.top?.sessData?.userManagement) {
|
||||
for (let user of obj.top.sessData.userManagement) {
|
||||
if (user?.id &&
|
||||
user.username.toLowerCase() == state.username.toLowerCase() &&
|
||||
user.auth_source == state.auth_source) {
|
||||
@@ -193,8 +193,8 @@ class UserManagementCollection extends BaseUISchema {
|
||||
setError('email', null);
|
||||
}
|
||||
|
||||
if (obj.isNew(state) && obj.top?._sessData?.userManagement) {
|
||||
for (let user of obj.top._sessData.userManagement) {
|
||||
if (obj.isNew(state) && obj.top?.sessData?.userManagement) {
|
||||
for (let user of obj.top.sessData.userManagement) {
|
||||
if (user?.id &&
|
||||
user.email?.toLowerCase() == state.email?.toLowerCase()) {
|
||||
msg = gettext('Email address \'%s\' already exists', state.email);
|
||||
@@ -303,6 +303,7 @@ class UserManagementSchema extends BaseUISchema {
|
||||
},
|
||||
{
|
||||
id: 'refreshBrowserTree', visible: false, type: 'switch',
|
||||
mode: ['non_supported'],
|
||||
deps: ['userManagement'], depChange: ()=> {
|
||||
return { refreshBrowserTree: this.changeOwnership };
|
||||
}
|
||||
|
@@ -142,7 +142,7 @@ Python Tests:
|
||||
'pgadmin4/web/pgadmin/utils/test.py' file.
|
||||
|
||||
- To run Feature Tests in parallel using selenoid(grid + docker), selenoid
|
||||
need to be installed nad should be run only with SERVER_MODE=True.
|
||||
need to be installed and should be run only with SERVER_MODE=True.
|
||||
Steps to install selenoid -
|
||||
|
||||
- Install & Start docker
|
||||
|
@@ -73,7 +73,7 @@ class CheckRoleMembershipControlFeatureTest(BaseFeatureTest):
|
||||
edit_object = self.wait.until(EC.visibility_of_element_located(
|
||||
(By.CSS_SELECTOR, NavMenuLocators.edit_obj_css)))
|
||||
edit_object.click()
|
||||
membership_tab = WebDriverWait(self.page.driver, 4).until(
|
||||
membership_tab = WebDriverWait(self.page.driver, 2).until(
|
||||
EC.presence_of_element_located((
|
||||
By.XPATH, "//button[normalize-space(text())='Membership']")))
|
||||
membership_tab.click()
|
||||
|
@@ -1147,29 +1147,39 @@ class PgadminPage:
|
||||
bottom_ele = self.driver.find_element(
|
||||
By.XPATH,
|
||||
"//div[@id='id-object-explorer']"
|
||||
"/div/div/div/div/div[last()]")
|
||||
bottom_ele_location = int(
|
||||
bottom_ele.value_of_css_property('top').split("px")[0])
|
||||
"/div/div/div/div/div/div[last()]")
|
||||
bottom_ele_top = bottom_ele.value_of_css_property('top')
|
||||
bottom_ele_location = 1
|
||||
|
||||
if (bottom_ele_top != 'auto'):
|
||||
bottom_ele_location = int(
|
||||
bottom_ele_top.split("px")[0]
|
||||
)
|
||||
|
||||
if tree_height - bottom_ele_location < 25:
|
||||
f_scroll = 0
|
||||
f_scroll = bottom_ele_location - 25
|
||||
else:
|
||||
self.driver.execute_script(
|
||||
self.js_executor_scrollintoview_arg, bottom_ele)
|
||||
self.js_executor_scrollintoview_arg, bottom_ele
|
||||
)
|
||||
f_scroll -= 1
|
||||
elif r_scroll > 0:
|
||||
top_el = self.driver.find_element(
|
||||
By.XPATH,
|
||||
"//div[@id='id-object-explorer']"
|
||||
"/div/div/div/div/div[1]")
|
||||
top_el_location = int(
|
||||
top_el.value_of_css_property('top').split("px")[0])
|
||||
top_el_top = top_el.value_of_css_property('top')
|
||||
top_el_location = 0
|
||||
|
||||
if (top_el_top != 'auto'):
|
||||
top_el_location = int(top_el_top.split("px")[0])
|
||||
|
||||
if (tree_height - top_el_location) == tree_height:
|
||||
r_scroll = 0
|
||||
else:
|
||||
webdriver.ActionChains(self.driver).move_to_element(
|
||||
top_el).perform()
|
||||
self.driver.execute_script(
|
||||
self.js_executor_scrollintoview_arg, top_el
|
||||
)
|
||||
r_scroll -= 1
|
||||
else:
|
||||
break
|
||||
|
157
web/regression/javascript/SchemaView/store.spec.js
Normal file
157
web/regression/javascript/SchemaView/store.spec.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
// pgAdmin 4 - PostgreSQL Tools
|
||||
//
|
||||
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
||||
// This software is released under the PostgreSQL Licence
|
||||
//
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
import { isValueEqual } from '../../../pgadmin/static/js/SchemaView/common';
|
||||
import {
|
||||
createStore
|
||||
} from '../../../pgadmin/static/js/SchemaView/SchemaState/store';
|
||||
|
||||
const initData = {
|
||||
id: 1,
|
||||
field1: 'field1val',
|
||||
field2: 1,
|
||||
fieldcoll: [
|
||||
{field3: 1, field4: 'field4val1', field5: 'field5val1'},
|
||||
{field3: 2, field4: 'field4val2', field5: 'field5val2'},
|
||||
],
|
||||
field3: 3,
|
||||
field4: 'field4val',
|
||||
};
|
||||
|
||||
describe('store', ()=>{
|
||||
describe('', () => {
|
||||
|
||||
it('getState', () => {
|
||||
const store = createStore(initData);
|
||||
const data = store.getState();
|
||||
expect(isValueEqual(data, initData)).toBe(true);
|
||||
});
|
||||
|
||||
it('get', () => {
|
||||
const store = createStore(initData);
|
||||
|
||||
const firstField3 = store.get(['fieldcoll', 0, 'field3']);
|
||||
expect(firstField3 == 1).toBe(true);
|
||||
|
||||
const firstFieldCollRow = store.get(['fieldcoll', '0']);
|
||||
// Sending a copy of the data, and not itself.
|
||||
expect(isValueEqual(firstFieldCollRow, initData.fieldcoll[0])).toBe(true);
|
||||
});
|
||||
|
||||
it('setState', () => {
|
||||
const store = createStore(initData);
|
||||
const newData = {a: 1};
|
||||
|
||||
store.setState(newData);
|
||||
|
||||
const newState = store.getState();
|
||||
expect(Object.is(newState, newData)).toBe(false);
|
||||
expect(isValueEqual(newState, newData)).toBe(true);
|
||||
});
|
||||
|
||||
it ('set', () => {
|
||||
const store = createStore(initData);
|
||||
const newData = {a: 1};
|
||||
|
||||
store.set(newData);
|
||||
|
||||
let newState = store.getState();
|
||||
expect(Object.is(newState, newData)).toBe(false);
|
||||
expect(isValueEqual(newState, newData)).toBe(true);
|
||||
|
||||
store.set((prevState) => ({...prevState, initData}));
|
||||
|
||||
newState = store.getState();
|
||||
expect(Object.is(newState, initData)).toBe(false);
|
||||
expect(isValueEqual(newState, initData)).toBe(false);
|
||||
|
||||
delete newState['a'];
|
||||
|
||||
store.set(() => (newState));
|
||||
|
||||
newState = store.getState();
|
||||
expect(isValueEqual(newState, initData)).toBe(false);
|
||||
});
|
||||
|
||||
it ('subscribe', () => {
|
||||
const store = createStore(initData);
|
||||
const listener = jest.fn();
|
||||
|
||||
const unsubscribe1 = store.subscribe(listener);
|
||||
store.set((prevState) => (prevState));
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
store.set((prevState) => {
|
||||
prevState.id = 2;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
expect(listener).toHaveBeenCalled();
|
||||
|
||||
const listenForFirstField3 = jest.fn();
|
||||
const unsubscribe2 = store.subscribeForPath(
|
||||
['fieldcoll', '0', 'field3'], listenForFirstField3
|
||||
);
|
||||
const listenForSecondField3 = jest.fn();
|
||||
const unsubscribe3 = store.subscribeForPath(
|
||||
['fieldcoll', '1', 'field3'], listenForSecondField3
|
||||
);
|
||||
let changeTo = 10;
|
||||
|
||||
store.set((prevState) => {
|
||||
prevState.fieldcoll[0].field3 = changeTo;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
expect(listenForFirstField3).toHaveBeenCalled();
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
expect(listenForSecondField3).not.toHaveBeenCalled();
|
||||
|
||||
store.set((prevState) => {
|
||||
// There is no actual change from previous state.
|
||||
prevState.fieldcoll[0].field3 = 10;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
// Not expecting it be called.
|
||||
expect(listenForFirstField3).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
expect(listenForSecondField3).not.toHaveBeenCalled();
|
||||
|
||||
unsubscribe1();
|
||||
|
||||
store.set((prevState) => {
|
||||
prevState.fieldcoll[0].field3 = 50;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
// Don't expect this to be called again.
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
// Expect this one to be called
|
||||
expect(listenForFirstField3).toHaveBeenCalledTimes(2);
|
||||
expect(listenForSecondField3).not.toHaveBeenCalled();
|
||||
|
||||
unsubscribe2();
|
||||
|
||||
store.set((prevState) => {
|
||||
prevState.fieldcoll[0].field3 = 100;
|
||||
return prevState;
|
||||
});
|
||||
|
||||
// Don't expect any of them to be called.
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
expect(listenForFirstField3).toHaveBeenCalledTimes(2);
|
||||
expect(listenForSecondField3).not.toHaveBeenCalled();
|
||||
|
||||
unsubscribe3();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '
|
||||
|
||||
describe('AggregateSchema', ()=>{
|
||||
|
||||
let schemaObj = new AggregateSchema();
|
||||
let createSchemaObj = () => new AggregateSchema();
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ describe('AggregateSchema', ()=>{
|
||||
});
|
||||
|
||||
it('create', async ()=>{
|
||||
await getCreateView(schemaObj);
|
||||
await getCreateView(createSchemaObj());
|
||||
});
|
||||
|
||||
it('edit', async ()=>{
|
||||
await getEditView(schemaObj, getInitData);
|
||||
await getEditView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('properties', async ()=>{
|
||||
await getPropertiesView(schemaObj, getInitData);
|
||||
await getPropertiesView(createSchemaObj(), getInitData);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -14,12 +14,13 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '
|
||||
|
||||
describe('CastSchema', ()=>{
|
||||
|
||||
let schemaObj = new CastSchema(
|
||||
let createSchemaObj = () => new CastSchema(
|
||||
{
|
||||
getTypeOptions: ()=>[],
|
||||
getFuncOptions: ()=>[],
|
||||
},
|
||||
);
|
||||
const schemaObj = createSchemaObj();
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
|
||||
@@ -29,15 +30,15 @@ describe('CastSchema', ()=>{
|
||||
});
|
||||
|
||||
it('create', async ()=>{
|
||||
await getCreateView(schemaObj);
|
||||
await getCreateView(createSchemaObj());
|
||||
});
|
||||
|
||||
it('edit', async ()=>{
|
||||
await getEditView(schemaObj, getInitData);
|
||||
await getEditView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('properties', async ()=>{
|
||||
await getPropertiesView(schemaObj, getInitData);
|
||||
await getPropertiesView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('srctyp depChange', ()=>{
|
||||
|
@@ -13,7 +13,7 @@ import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '
|
||||
|
||||
describe('CatalogSchema', ()=>{
|
||||
|
||||
let catalogObj = new CatalogSchema(
|
||||
let createCatalogObj = () => new CatalogSchema(
|
||||
{
|
||||
namespaceowner: '',
|
||||
}
|
||||
@@ -29,15 +29,15 @@ describe('CatalogSchema', ()=>{
|
||||
});
|
||||
|
||||
it('create', async ()=>{
|
||||
await getCreateView(catalogObj);
|
||||
await getCreateView(createCatalogObj());
|
||||
});
|
||||
|
||||
it('edit', async ()=>{
|
||||
await getEditView(catalogObj, getInitData);
|
||||
await getEditView(createCatalogObj(), getInitData);
|
||||
});
|
||||
|
||||
it('properties', async ()=>{
|
||||
await getPropertiesView(catalogObj, getInitData);
|
||||
await getPropertiesView(createCatalogObj(), getInitData);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -13,23 +13,19 @@ import {genericBeforeEach, getCreateView, getPropertiesView} from '../genericFun
|
||||
|
||||
describe('CatalogObjectColumn', ()=>{
|
||||
|
||||
let schemaObj = new CatalogObjectColumn();
|
||||
let createSchemaObj = () => new CatalogObjectColumn();
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
beforeEach(()=>{
|
||||
genericBeforeEach();
|
||||
});
|
||||
|
||||
it('create', async ()=>{
|
||||
await getCreateView(schemaObj);
|
||||
await getCreateView(createSchemaObj());
|
||||
});
|
||||
|
||||
it('properties', async ()=>{
|
||||
await getPropertiesView(schemaObj, getInitData);
|
||||
await getPropertiesView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -35,7 +35,8 @@ function getFieldDepChange(schema, id) {
|
||||
|
||||
describe('CheckConstraintSchema', ()=>{
|
||||
|
||||
let schemaObj = new CheckConstraintSchema();
|
||||
let createSchemaObj = () => new CheckConstraintSchema();
|
||||
let schemaObj = createSchemaObj();
|
||||
let getInitData = ()=>Promise.resolve({});
|
||||
|
||||
|
||||
@@ -47,15 +48,15 @@ describe('CheckConstraintSchema', ()=>{
|
||||
});
|
||||
|
||||
it('create', async ()=>{
|
||||
await getCreateView(schemaObj);
|
||||
await getCreateView(createSchemaObj());
|
||||
});
|
||||
|
||||
it('edit', async ()=>{
|
||||
await getEditView(schemaObj, getInitData);
|
||||
await getEditView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('properties', async ()=>{
|
||||
await getPropertiesView(schemaObj, getInitData);
|
||||
await getPropertiesView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('create collection', async ()=>{
|
||||
|
@@ -12,8 +12,7 @@ import CollationSchema from '../../../pgadmin/browser/server_groups/servers/data
|
||||
import {genericBeforeEach, getCreateView, getEditView, getPropertiesView} from '../genericFunctions';
|
||||
|
||||
describe('CollationsSchema', () => {
|
||||
|
||||
let schemaObj = new CollationSchema(
|
||||
const createSchemaObj = () => new CollationSchema(
|
||||
{
|
||||
rolesList: () => [],
|
||||
schemaList: () => [],
|
||||
@@ -24,24 +23,23 @@ describe('CollationsSchema', () => {
|
||||
schema: ''
|
||||
}
|
||||
);
|
||||
let schemaObj = createSchemaObj();
|
||||
let getInitData = () => Promise.resolve({});
|
||||
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
genericBeforeEach();
|
||||
});
|
||||
|
||||
it('create', () => {
|
||||
getCreateView(schemaObj);
|
||||
getCreateView(createSchemaObj());
|
||||
});
|
||||
|
||||
it('edit', () => {
|
||||
getEditView(schemaObj, getInitData);
|
||||
getEditView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('properties', () => {
|
||||
getPropertiesView(schemaObj, getInitData);
|
||||
getPropertiesView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('validate', () => {
|
||||
|
@@ -46,13 +46,13 @@ function getFieldDepChange(schema, id) {
|
||||
}
|
||||
|
||||
describe('ColumnSchema', ()=>{
|
||||
|
||||
let schemaObj = new ColumnSchema(
|
||||
const createSchemaObj = () => new ColumnSchema(
|
||||
()=>new MockSchema(),
|
||||
{},
|
||||
()=>Promise.resolve([]),
|
||||
()=>Promise.resolve([]),
|
||||
);
|
||||
let schemaObj = createSchemaObj();
|
||||
let datatypes = [
|
||||
{value: 'numeric', length: true, precision: true, min_val: 1, max_val: 140391},
|
||||
{value: 'character varying', length: true, precision: false, min_val: 1, max_val: 140391},
|
||||
@@ -64,15 +64,15 @@ describe('ColumnSchema', ()=>{
|
||||
});
|
||||
|
||||
it('create', async ()=>{
|
||||
await getCreateView(schemaObj);
|
||||
await getCreateView(createSchemaObj());
|
||||
});
|
||||
|
||||
it('edit', async ()=>{
|
||||
await getEditView(schemaObj, getInitData);
|
||||
await getEditView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('properties', async ()=>{
|
||||
await getPropertiesView(schemaObj, getInitData);
|
||||
await getPropertiesView(createSchemaObj(), getInitData);
|
||||
});
|
||||
|
||||
it('create collection', async ()=>{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user