Fixes the Variable Schema UI issues and InlineView bug reported in #7884

* Show the icon for the 'Reset' button. (Reference #7884)

* Reload the server list after connecting to a server in the 'New
connection' dialog (QueryTool). (Reference: #7884)

* Pass the grid path during the bulk update (click on a radio action)

* Don't assign the cell value to the 'rowValue' variable.

* Don't rely on the 'optionsLoaded' for setting the variable types as it
is loaded asynchronously, and variable types data may not be available
while rendering the 'value' cell. (Fixes #7884)

* Fixed a type while checking for the 'inline-group'. fixes (#7884)

* 'vnameOptions' can be a Promise function too, hence - taken care accrodingly.

* Introduced a parameter 'reloadOnDepChanges' in the BaseSchemaUI field
to force reload the control on value change for one of the
dependencies.

* Reload on the components in case of dependent value changes.

* Introduced 'useSchemaStateSubscriber', which generates a state
subscriber mananager instance. It helps multiple subscribers in a
single control as we could have multiple subscribe within a control.
(For example - value, options, errors, etc).

* Fixed all the issues reported (#7884)
This commit is contained in:
Ashesh Vashi 2024-09-16 00:04:37 +05:30 committed by GitHub
parent 98d703645c
commit 5e96f0fd61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 262 additions and 121 deletions

View File

@ -96,7 +96,8 @@ class ForeignKeyHeaderSchema extends BaseUISchema {
return state._disable_references;
}
},{
id: 'referenced', label: gettext('Referencing'), editable: false, deps: ['references'],
id: 'referenced', label: gettext('Referencing'), editable: false,
deps: ['references'],
type: (state)=>{
return {
type: 'select',

View File

@ -22,7 +22,7 @@ export function getNodeVariableSchema(nodeObj, treeNodeInfo, itemNodeData, hasDa
keys.push('role');
}
return new VariableSchema(
()=>getNodeAjaxOptions('vopts', nodeObj, treeNodeInfo, itemNodeData, null, (vars)=>{
() => getNodeAjaxOptions('vopts', nodeObj, treeNodeInfo, itemNodeData, null, (vars)=>{
let res = [];
_.each(vars, function(v) {
res.push({
@ -38,8 +38,8 @@ export function getNodeVariableSchema(nodeObj, treeNodeInfo, itemNodeData, hasDa
return res;
}),
()=>getNodeListByName('database', treeNodeInfo, itemNodeData),
()=>getNodeListByName('role', treeNodeInfo, itemNodeData),
() => getNodeListByName('database', treeNodeInfo, itemNodeData),
() => getNodeListByName('role', treeNodeInfo, itemNodeData),
keys
);
}
@ -59,6 +59,8 @@ export default class VariableSchema extends BaseUISchema {
this.varTypes = {};
this.keys = keys;
this.allReadOnly = false;
setTimeout(() => this.setVarTypes(vnameOptions), 0);
}
setAllReadOnly(isReadOnly) {
@ -66,10 +68,18 @@ export default class VariableSchema extends BaseUISchema {
}
setVarTypes(options) {
options.forEach((option)=>{
this.varTypes[option.value] = {
...option,
};
let optPromise = options;
if (typeof options === 'function') {
optPromise = options();
}
Promise.resolve(optPromise).then((res) => {
res.forEach((option) => {
this.varTypes[option.value] = {
...option,
};
});
});
}
@ -145,13 +155,10 @@ export default class VariableSchema extends BaseUISchema {
},
{
id: 'name', label: gettext('Name'), type:'text',
editable: function(state) {
return obj.isNew(state) || !obj.allReadOnly;
},
editable: (state) => (obj.isNew(state) || !obj.allReadOnly),
cell: () => ({
cell: 'select',
options: this.vnameOptions,
optionsLoaded: (options)=>{obj.setVarTypes(options);},
options: obj.vnameOptions,
controlProps: { allowClear: false },
}),
},
@ -165,9 +172,9 @@ export default class VariableSchema extends BaseUISchema {
{
id: 'value', label: gettext('Value'), type: 'text',
deps: ['name'], editable: !obj.allReadOnly,
depChange: (state, source)=>{
depChange: (state, source) => {
if(source[source.length-1] == 'name') {
let variable = this.varTypes[state.name];
let variable = obj.varTypes[state.name];
if(variable.vartype === 'bool'){
return {
value: false,
@ -178,19 +185,20 @@ export default class VariableSchema extends BaseUISchema {
};
}
},
cell: (row)=>{
let variable = this.varTypes[row.name];
return this.getValueFieldProps(variable);
cell: (row) => {
let variable = obj.varTypes[row.name];
return obj.getValueFieldProps(variable);
}
},
{id: 'database', label: gettext('Database'), type: 'text',
cell: ()=>({cell: 'select', options: this.databaseOptions }),
{
id: 'database', label: gettext('Database'), type: 'text',
cell: ()=>({cell: 'select', options: obj.databaseOptions }),
},
{id: 'role', label: gettext('Role'), type: 'text',
cell: ()=>({cell: 'select', options: this.roleOptions,
controlProps: {
allowClear: false,
}
{
id: 'role', label: gettext('Role'), type: 'text',
cell: () => ({
cell: 'select', options: obj.roleOptions,
controlProps: { allowClear: false },
}),
},
];

View File

@ -65,10 +65,16 @@ class ChangePasswordSchema extends BaseUISchema {
export default function ChangePasswordContent({getInitData=() => { /*This is intentional (SonarQube)*/ },
onSave, onClose, hasCsrfToken=false, showUser=true}) {
const schema=React.useRef(null);
if (!schema.current)
schema.current = new ChangePasswordSchema(
'', false, hasCsrfToken, showUser
);
return <SchemaView
formType={'dialog'}
getInitData={getInitData}
schema={new ChangePasswordSchema('', false, hasCsrfToken, showUser)}
schema={schema.current}
viewHelperProps={{
mode: 'create',
}}

View File

@ -33,7 +33,9 @@ import CustomPropTypes from 'sources/custom_prop_types';
import { StyleDataGridBox } from '../StyledComponents';
import { SchemaStateContext } from '../SchemaState';
import { useFieldOptions, useFieldValue } from '../hooks';
import {
useFieldOptions, useFieldValue, useSchemaStateSubscriber,
} from '../hooks';
import { registerView } from '../registry';
import { listenDepChanges } from '../utils';
@ -49,20 +51,16 @@ export default function DataGridView({
}) {
const pgAdmin = usePgAdmin();
const [refreshKey, setRefreshKey] = useState(0);
const subscriberManager = useSchemaStateSubscriber(setRefreshKey);
const schemaState = useContext(SchemaStateContext);
const options = useFieldOptions(
accessPath, schemaState, refreshKey, setRefreshKey
);
const options = useFieldOptions(accessPath, schemaState, subscriberManager);
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);
}
[...accessPath, 'length'], schemaState, subscriberManager
);
useEffect(() => {

View File

@ -16,7 +16,9 @@ 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 {
useFieldOptions, useFieldValue, useSchemaStateSubscriber
} from '../hooks';
import { listenDepChanges } from '../utils';
import { DataGridContext, DataGridRowContext } from './context';
@ -25,14 +27,17 @@ import { DataGridContext, DataGridRowContext } from './context';
export function getMappedCell({field}) {
const Cell = ({reRenderRow, getValue}) => {
const [key, setKey] = useState(0);
const [, setKey] = useState(0);
const subscriberManager = useSchemaStateSubscriber(setKey);
const schemaState = useContext(SchemaStateContext);
const { dataDispatch, accessPath } = 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 colOptions = useFieldOptions(
colAccessPath, schemaState, subscriberManager
);
let value = useFieldValue(colAccessPath, schemaState, subscriberManager);
let rowValue = useFieldValue(rowAccessPath, schemaState);
listenDepChanges(colAccessPath, field, true, schemaState);

View File

@ -15,7 +15,9 @@ import CustomPropTypes from 'sources/custom_prop_types';
import { FieldControl } from './FieldControl';
import { SchemaStateContext } from './SchemaState';
import { useFieldSchema, useFieldValue } from './hooks';
import {
useFieldSchema, useFieldValue, useSchemaStateSubscriber,
} from './hooks';
import { registerView } from './registry';
import { createFieldControls, listenDepChanges } from './utils';
@ -23,13 +25,15 @@ import { createFieldControls, listenDepChanges } from './utils';
export default function FieldSetView({
field, accessPath, dataDispatch, viewHelperProps, controlClassName,
}) {
const [key, setRefreshKey] = useState(0);
const [, setKey] = useState(0);
const subscriberManager = useSchemaStateSubscriber(setKey);
const schema = field.schema;
const schemaState = useContext(SchemaStateContext);
const value = useFieldValue(accessPath, schemaState);
const options = useFieldSchema(
field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey
field, accessPath, value, viewHelperProps, schemaState, subscriberManager
);
const label = field.label;
listenDepChanges(accessPath, field, options.visible, schemaState);

View File

@ -27,7 +27,9 @@ import { FieldControl } from './FieldControl';
import { SQLTab } from './SQLTab';
import { FormContentBox } from './StyledComponents';
import { SchemaStateContext } from './SchemaState';
import { useFieldSchema, useFieldValue } from './hooks';
import {
useFieldSchema, useFieldValue, useSchemaStateSubscriber,
} from './hooks';
import { registerView, View } from './registry';
import { createFieldControls, listenDepChanges } from './utils';
@ -62,10 +64,11 @@ export default function FormView({
showError=false, resetKey, focusOnFirstInput=false
}) {
const [key, setKey] = useState(0);
const subscriberManager = useSchemaStateSubscriber(setKey);
const schemaState = useContext(SchemaStateContext);
const value = useFieldValue(accessPath, schemaState);
const { visible } = useFieldSchema(
field, accessPath, value, viewHelperProps, schemaState, key, setKey
field, accessPath, value, viewHelperProps, schemaState, subscriberManager
);
const [tabValue, setTabValue] = useState(0);
@ -106,13 +109,12 @@ export default function FormView({
useEffect(() => {
// Refresh on message changes.
return schemaState.subscribe(
['errors', 'message'],
return subscriberManager.current?.add(
schemaState, ['errors', 'message'], 'states',
(newState, prevState) => {
if (_.isUndefined(newState) || _.isUndefined(prevState));
setKey(Date.now());
},
'states'
if (_.isUndefined(newState) || _.isUndefined(prevState))
subscriberManager.current?.signal();
}
);
}, [key]);

View File

@ -28,7 +28,7 @@ import { evalFunc } from 'sources/utils';
import { SchemaStateContext } from './SchemaState';
import { isValueEqual } from './common';
import {
useFieldOptions, useFieldValue, useFieldError
useFieldOptions, useFieldValue, useFieldError, useSchemaStateSubscriber,
} from './hooks';
import { listenDepChanges } from './utils';
@ -339,22 +339,15 @@ export const MappedFormControl = ({
}) => {
const checkIsMounted = useIsMounted();
const [key, setKey] = useState(0);
const subscriberManager = useSchemaStateSubscriber(setKey);
const schemaState = useContext(SchemaStateContext);
const state = schemaState.data;
const avoidRenderingWhenNotMounted = (newKey) => {
if (checkIsMounted()) {
setKey(newKey);
}
const value = useFieldValue(accessPath, schemaState, subscriberManager);
const options = useFieldOptions(accessPath, schemaState, subscriberManager);
const {hasError} = useFieldError(accessPath, schemaState, subscriberManager);
const avoidRenderingWhenNotMounted = (...args) => {
if (checkIsMounted()) subscriberManager.current?.signal(...args);
};
const value = useFieldValue(
accessPath, schemaState, key, avoidRenderingWhenNotMounted
);
const options = useFieldOptions(
accessPath, schemaState, key, avoidRenderingWhenNotMounted
);
const { hasError } = useFieldError(
accessPath, schemaState, key, avoidRenderingWhenNotMounted
);
const origOnChange = onChange;
@ -369,7 +362,10 @@ export const MappedFormControl = ({
if (!isValueEqual(changedValue, currValue)) origOnChange(changedValue);
};
listenDepChanges(accessPath, field, options.visible, schemaState);
const depVals = listenDepChanges(
accessPath, field, options.visible, schemaState, state,
avoidRenderingWhenNotMounted
);
let newProps = {
...props,
@ -394,14 +390,17 @@ export const MappedFormControl = ({
newProps.onClick = ()=>{
origOnClick?.();
};
// 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));
memDeps.push(depVals);
// Filter out garbage props if any using ALLOWED_PROPS_FIELD.
return useMemo(

View File

@ -12,6 +12,7 @@ import { useFieldOptions } from './useFieldOptions';
import { useFieldValue } from './useFieldValue';
import { useSchemaState } from './useSchemaState';
import { useFieldSchema } from './useFieldSchema';
import { useSchemaStateSubscriber } from './useSchemaStateSubscriber';
export {
@ -20,4 +21,5 @@ export {
useFieldValue,
useFieldSchema,
useSchemaState,
useSchemaStateSubscriber,
};

View File

@ -9,26 +9,36 @@
import { useEffect } from 'react';
const isPathEqual = (path1, path2) => (
JSON.stringify(path1) === JSON.stringify(path2)
);
export const useFieldError = (path, schemaState, subscriberManager) => {
export const useFieldError = (
path, schemaState, key, setRefreshKey
) => {
useEffect(() => {
if (!schemaState || !setRefreshKey) return;
if (!schemaState || !subscriberManager?.current) 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;
if ((
!isPathEqual(prevState.name, path) &&
!isPathEqual(newState.name, path)
) || (
isPathEqual(prevState.name, newState.name) &&
prevState.message == newState.message
)) return;
setRefreshKey({id: Date.now()});
subscriberManager.current?.signal();
};
return schemaState.subscribe(['errors'], checkPathError, 'states');
}, [key, schemaState?._id]);
return subscriberManager.current?.add(
schemaState, ['errors'], 'states', checkPathError
);
});
const errors = schemaState?.errors || {};
const error = errors.name === path ? errors.message : null;
const error = isPathEqual(errors.name, path) ? errors.message : null;
return {hasError: !_.isNull(error), error};
};

View File

@ -10,16 +10,13 @@
import { useEffect } from 'react';
export const useFieldOptions = (
path, schemaState, key, setRefreshKey
) => {
useEffect(() => {
if (!schemaState) return;
export const useFieldOptions = (path, schemaState, subscriberManager) => {
return schemaState.subscribe(
path, () => setRefreshKey?.({id: Date.now()}), 'options'
);
}, [key, schemaState?._id]);
useEffect(() => {
if (!schemaState || !subscriberManager?.current) return;
return subscriberManager.current?.add(schemaState, path, 'options');
});
return schemaState?.options(path) || {visible: true};
};

View File

@ -14,32 +14,32 @@ import { booleanEvaluator } from '../options';
export const useFieldSchema = (
field, accessPath, value, viewHelperProps, schemaState, key, setRefreshKey
field, accessPath, value, viewHelperProps, schemaState, subscriberManager
) => {
useEffect(() => {
if (!schemaState || !field) return;
if (!schemaState || !field || !subscriberManager?.current) return;
// It already has 'id', 'options' is already evaluated.
if (field.id)
return schemaState.subscribe(
accessPath, () => setRefreshKey?.({id: Date.now()}), 'options'
);
return subscriberManager.current?.add(schemaState, accessPath, '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'
subscriberManager.current?.add(
schemaState, accessPath.concat(dep), 'value'
)
));
return () => {
unsubscribers.forEach(unsubscribe => unsubscribe());
unsubscribers.forEach(
unsubscribe => subscriberManager.current?.remove(unsubscribe)
);
};
}, [key, schemaState?._id]);
});
if (!field) return { visible: true };
if (field.id) return schemaState?.options(accessPath);

View File

@ -10,16 +10,13 @@
import { useEffect } from 'react';
export const useFieldValue = (
path, schemaState, key, setRefreshKey
) => {
useEffect(() => {
if (!schemaState || !setRefreshKey) return;
export const useFieldValue = (path, schemaState, subscriberManager) => {
return schemaState.subscribe(
path, () => setRefreshKey({id: Date.now()}), 'value'
);
}, [key, schemaState?._id]);
useEffect(() => {
if (!schemaState || !subscriberManager?.current) return;
return subscriberManager.current?.add(schemaState, path, 'value');
});
return schemaState?.value(path);
};

View File

@ -0,0 +1,92 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
/////////
//
// A class to handle the ScheamState subscription for a control to avoid
// rendering multiple times.
//
class SubscriberManager {
constructor(refreshKeyCallback) {
this.mounted = true;
this.callback = refreshKeyCallback;
this.unsubscribers = new Set();
this._id = Date.now();
}
add(schemaState, accessPath, kind, callback) {
if (!schemaState) return;
callback = callback || (() => this.signal());
return this._add(schemaState.subscribe(accessPath, callback, kind));
}
_add(unsubscriber) {
if (!unsubscriber) return;
// Avoid reinsertion of same unsubscriber.
if (this.unsubscribers.has(unsubscriber)) return;
this.unsubscribers.add(unsubscriber);
return () => this.remove(unsubscriber);
}
remove(unsubscriber) {
if (!unsubscriber) return;
if (!this.unsubscribers.has(unsubscriber)) return;
this.unsubscribers.delete(unsubscriber);
unsubscriber();
}
signal() {
// Do nothing - if already work is in progress.
if (!this.mounted) return;
this.mounted = false;
this.release();
this.callback(Date.now());
}
release () {
const unsubscribers = this.unsubscribers;
this.unsubscribers = new Set();
this.mounted = true;
setTimeout(() => {
Set.prototype.forEach.call(
unsubscribers, (unsubscriber) => unsubscriber()
);
}, 0);
}
mount() {
this.mounted = true;
}
}
export function useSchemaStateSubscriber(refreshKeyCallback) {
const subscriberManager = React.useRef(null);
React.useEffect(() => {
if (!subscriberManager.current) return;
return () => {
subscriberManager.current?.release();
};
}, []);
if (!subscriberManager.current)
subscriberManager.current = new SubscriberManager(refreshKeyCallback);
else
subscriberManager.current.mount();
return subscriberManager;
}

View File

@ -17,7 +17,7 @@ import { View, hasView } from '../registry';
import { StaticMappedFormControl, MappedFormControl } from '../MappedControl';
const DEFAULT_TAB = 'general';
const DEFAULT_TAB = gettext('General');
export const createFieldControls = ({
schema, schemaState, accessPath, viewHelperProps, dataDispatch
@ -50,13 +50,13 @@ export const createFieldControls = ({
};
// Create default group - 'General'.
createGroup(DEFAULT_TAB, gettext('General'), true);
createGroup(DEFAULT_TAB, DEFAULT_TAB, true);
schema?.fields?.forEach((field) => {
if (!isModeSupportedByField(field, viewHelperProps)) return;
let inlineGroup = null;
const inlineGroupId = field[inlineGroup];
const inlineGroupId = field['inlineGroup'];
if(field.type === 'group') {

View File

@ -13,7 +13,16 @@ import _ from 'lodash';
import { evalFunc } from 'sources/utils';
export const listenDepChanges = (accessPath, field, visible, schemaState) => {
export const listenDepChanges = (
accessPath, field, visible, schemaState, data, setRefreshKey
) => {
const deps = field?.deps ? (evalFunc(null, field.deps) || []) : null;
const parentPath = accessPath ? [...accessPath] : [];
// Remove the last element.
if (field?.id && field.id === parentPath[parentPath.length - 1]) {
parentPath.pop();
}
useEffect(() => {
if (!visible || !schemaState || !field) return;
@ -26,25 +35,22 @@ export const listenDepChanges = (accessPath, field, visible, schemaState) => {
}
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) => {
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) {
if (field.depChange || field.deferredDepChange) {
schemaState.addDepListener(
source, accessPath, field.depChange, field.deferredDepChange
);
}
if (setRefreshKey)
schemaState.subscribe(
source, () => setRefreshKey(Date.now()), 'value'
);
});
}
@ -54,4 +60,7 @@ export const listenDepChanges = (accessPath, field, visible, schemaState) => {
};
}, []);
return deps?.map((dep) => schemaState.value(
_.isArray(dep) ? dep : parentPath.concat(dep)
));
};

View File

@ -955,7 +955,6 @@ export const InputSelect = forwardRef(({
return () => umounted = true;
}, [optionsReloadBasis]);
/* Apply filter if any */
const filteredOptions = (controlProps.filter?.(finalOptions)) || finalOptions;
const flatFiltered = flattenSelectOptions(filteredOptions);

View File

@ -192,6 +192,11 @@ export default function MacrosDialog({onClose, onSave}) {
return <></>;
}
const schema = React.useRef(null);
if (!schema.current)
schema.current = new MacrosSchema(keyOptions);
return (
<StyledBox>
<SchemaView
@ -202,7 +207,7 @@ export default function MacrosDialog({onClose, onSave}) {
}
return Promise.resolve({macro: userMacrosData.filter((m)=>Boolean(m.name))});
}}
schema={new MacrosSchema(keyOptions)}
schema={schema.current}
viewHelperProps={{
mode: 'edit',
}}

View File

@ -172,9 +172,10 @@ class NewConnectionSchema extends BaseUISchema {
}
}, {
id: 'user', label: gettext('User'), deps: ['sid', 'connected'],
noEmpty: true, controlProps: { allowClear: false },
noEmpty: true,
type: (state) => ({
type: 'select',
controlProps: { allowClear: false },
options: () => this.getOtherOptions(
state.sid, 'get_new_connection_user'
),
@ -182,8 +183,9 @@ class NewConnectionSchema extends BaseUISchema {
}),
}, {
id: 'role', label: gettext('Role'), deps: ['sid', 'connected'],
type: (state)=>({
type: (state) => ({
type: 'select',
controlProps: { allowClear: false },
options: () => this.getOtherOptions(
state.sid, 'get_new_connection_role'
),

View File

@ -399,6 +399,11 @@ function UserManagementDialog({onClose}) {
window.open(url_for('help.static', { 'filename': 'user_management.html' }), 'pgadmin_help');
};
const schema = React.useRef(null);
if (!schema.current)
schema.current = new UserManagementSchema(authSourcesOptions, roleOptions);
return <StyledBox><SchemaView
formType={'dialog'}
getInitData={()=>{ return new Promise((resolve, reject)=>{
@ -410,7 +415,7 @@ function UserManagementDialog({onClose}) {
reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
});
}); }}
schema={new UserManagementSchema(authSourcesOptions, roleOptions)}
schema={schema.current}
viewHelperProps={{
mode: 'edit',
}}