diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index a770aade4011dfe3204304459b3465f2c8340b85..12cb6d018b773121657e539bc0b86f02dcf47635 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/structure-quality-report.ts @@ -15,6 +15,7 @@ import { CustomPropSymbol } from 'mol-script/language/symbol'; import Type from 'mol-script/language/type'; import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler'; import { PropertyWrapper } from '../common/wrapper'; +import { Task } from 'mol-task'; export namespace StructureQualityReport { export type IssueMap = IndexedCustomProperty.Residue<string[]> @@ -82,6 +83,33 @@ export namespace StructureQualityReport { } } + export function createAttachTask(mapUrl: (model: Model) => string, fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) { + return (model: Model) => Task.create('PDBe Structure Quality Report', async ctx => { + if (get(model)) return true; + + let issueMap: IssueMap | undefined; + let info; + // TODO: return from CIF support once the data is recomputed + // = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model); + // if (info) { + // const data = getCifData(model); + // issueMap = createIssueMapFromCif(model, data.residues, data.groups); + // } else + { + const url = mapUrl(model); + const dataStr = await fetch(url, 'string').runInContext(ctx) as string; + const data = JSON.parse(dataStr)[model.label.toLowerCase()]; + if (!data) return false; + info = PropertyWrapper.createInfo(); + issueMap = createIssueMapFromJson(model, data); + } + + model.customProperties.add(Descriptor); + set(model, { info, data: issueMap }); + return false; + }); + } + export async function attachFromCifOrApi(model: Model, params: { // optional JSON source PDBe_apiSourceJson?: (model: Model) => Promise<any> diff --git a/src/mol-model-props/pdbe/themes/structure-quality-report.ts b/src/mol-model-props/pdbe/themes/structure-quality-report.ts new file mode 100644 index 0000000000000000000000000000000000000000..a527e58f7797addf3ad4b2a3de9109c5179f330b --- /dev/null +++ b/src/mol-model-props/pdbe/themes/structure-quality-report.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; +import { Location } from 'mol-model/location'; +import { StructureElement } from 'mol-model/structure'; +import { ColorTheme, LocationColor } from 'mol-theme/color'; +import { ThemeDataContext } from 'mol-theme/theme'; +import { Color } from 'mol-util/color'; +import { TableLegend } from 'mol-util/color/tables'; + +const ValidationColors = [ + Color.fromRgb(170, 170, 170), // not applicable + Color.fromRgb(0, 255, 0), // 0 issues + Color.fromRgb(255, 255, 0), // 1 + Color.fromRgb(255, 128, 0), // 2 + Color.fromRgb(255, 0, 0), // 3 or more +] + +const ValidationColorTable: [string, Color][] = [ + ['No Issues', ValidationColors[1]], + ['One Issue', ValidationColors[2]], + ['Two Issues', ValidationColors[3]], + ['Three Or More Issues', ValidationColors[4]], + ['Not Applicable', ValidationColors[9]] +] + +export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> { + let color: LocationColor + + if (ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) { + const getIssues = StructureQualityReport.getIssues; + color = (location: Location) => { + if (StructureElement.isLocation(location)) { + return ValidationColors[Math.min(3, getIssues(location).length) + 1]; + } + return ValidationColors[0]; + } + } else { + color = () => ValidationColors[0]; + } + + return { + factory: StructureQualityReportColorTheme, + granularity: 'group', + color: color, + props: props, + description: 'Assigns residue colors according to the number of issues in the PDBe Validation Report.', + legend: TableLegend(ValidationColorTable) + } +} \ No newline at end of file diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts index 98010eb2a0f55405bcabeb769dc9fd2b40e467bb..976e78982558424c4fa084fac2596efc672389f1 100644 --- a/src/mol-plugin/behavior.ts +++ b/src/mol-plugin/behavior.ts @@ -13,6 +13,7 @@ import * as StaticMisc from './behavior/static/misc' import * as DynamicRepresentation from './behavior/dynamic/representation' import * as DynamicCamera from './behavior/dynamic/camera' +import * as DynamicCustomProps from './behavior/dynamic/custom-props' export const BuiltInPluginBehaviors = { State: StaticState, @@ -24,4 +25,5 @@ export const BuiltInPluginBehaviors = { export const PluginBehaviors = { Representation: DynamicRepresentation, Camera: DynamicCamera, + CustomProps: DynamicCustomProps } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/custom-props.ts b/src/mol-plugin/behavior/dynamic/custom-props.ts new file mode 100644 index 0000000000000000000000000000000000000000..7dc6eefd80de94e5f390d467ee542e56aeb5e85c --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/custom-props.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { OrderedSet } from 'mol-data/int'; +import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; +import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report'; +import { Loci } from 'mol-model/loci'; +import { StructureElement } from 'mol-model/structure'; +import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginBehavior } from '../behavior'; + +export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({ + name: 'pdbe-structure-quality-report-prop', + display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { + private attach = StructureQualityReport.createAttachTask( + m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`, + this.ctx.fetch + ); + + private provider: CustomPropertyRegistry.Provider = { + option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'], + descriptor: StructureQualityReport.Descriptor, + defaultSelected: false, + attachableTo: () => true, + attach: this.attach + } + + register(): void { + this.ctx.customModelProperties.register(this.provider); + this.ctx.lociLabels.addProvider(labelPDBeValidation); + + // TODO: support filtering of themes based on the input structure + // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor) + // TODO: add remove functionality + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', { + label: 'PDBe Structure Quality Report', + factory: StructureQualityReportColorTheme, + getParams: () => ({}) + }) + } + + update(p: { autoAttach: boolean }) { + let updated = this.params.autoAttach !== p.autoAttach + this.params.autoAttach = p.autoAttach; + this.provider.defaultSelected = p.autoAttach; + return updated; + } + + unregister() { + this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name); + this.ctx.lociLabels.removeProvider(labelPDBeValidation); + + // TODO: add remove functionality to registry + // this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report') + } + }, + params: () => ({ + autoAttach: PD.Boolean(false) + }) +}); + +function labelPDBeValidation(loci: Loci): string | undefined { + switch (loci.kind) { + case 'element-loci': + const e = loci.elements[0]; + const u = e.unit; + if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0; + + const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]); + const issues = StructureQualityReport.getIssues(se); + if (issues.length === 0) return 'PDBe Validation: No Issues'; + return `PDBe Validation: ${issues.join(', ')}`; + + default: return void 0; + } +} \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 87a1eb6cbbdce154c187ed316c60d7f3db154952..0d43b9b3985c80982c91b430d8e57c5c4170e050 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -24,6 +24,7 @@ import { TaskManager } from './util/task-manager'; import { Color } from 'mol-util/color'; import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager'; import { ajaxGet } from 'mol-util/data-source'; +import { CustomPropertyRegistry } from './util/custom-prop-registry'; export class PluginContext { private disposed = false; @@ -58,13 +59,6 @@ export class PluginContext { } }; - readonly lociLabels: LociLabelManager; - - readonly structureRepresentation = { - registry: new StructureRepresentationRegistry(), - themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext - } - readonly behaviors = { canvas: { highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }), @@ -75,6 +69,14 @@ export class PluginContext { readonly canvas3d: Canvas3D; + readonly lociLabels: LociLabelManager; + + readonly structureRepresentation = { + registry: new StructureRepresentationRegistry(), + themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext + } + + readonly customModelProperties = new CustomPropertyRegistry(); initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) { try { diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 648029b6df32d7a65f4cb91d4aa2789f677f9a18..2201de2baf6d9c9a59034c3e437ff6bedcc5769c 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -35,7 +35,8 @@ const DefaultSpec: PluginSpec = { PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci), PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), - PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }) + PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }), + PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: false }) ] } diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index dae5e96b5161567c5b088aca58983999a021d9c7..b50889e6b87914b1300cd26bd7def51795c7e3ab 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/basic.ts @@ -27,10 +27,23 @@ const DownloadStructure = StateAction.build({ display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' }, params: { source: PD.MappedStatic('bcif-static', { - 'pdbe-updated': PD.Text('1cbs', { label: 'Id' }), - 'rcsb': PD.Text('1tqn', { label: 'Id' }), - 'bcif-static': PD.Text('1tqn', { label: 'Id' }), - 'url': PD.Group({ url: PD.Text(''), isBinary: PD.Boolean(false) }, { isExpanded: true }) + 'pdbe-updated': PD.Group({ + id: PD.Text('1cbs', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'rcsb': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'bcif-static': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'url': PD.Group({ + url: PD.Text(''), + isBinary: PD.Boolean(false), + supportProps: PD.Boolean(false) + }, { isFlat: true }) }, { options: [ ['pdbe-updated', 'PDBe Updated'], @@ -50,19 +63,19 @@ const DownloadStructure = StateAction.build({ url = src.params; break; case 'pdbe-updated': - url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params}` }; + url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` }; break; case 'rcsb': - url = { url: `https://files.rcsb.org/download/${src.params.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params}` }; + url = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` }; break; case 'bcif-static': - url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params}` }; + url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` }; break; default: throw new Error(`${(src as any).name} not supported.`); } const data = b.toRoot().apply(StateTransforms.Data.Download, url); - return state.update(createStructureTree(data)); + return state.update(createStructureTree(data, params.source.params.supportProps)); }); export const OpenStructure = StateAction.build({ @@ -72,15 +85,20 @@ export const OpenStructure = StateAction.build({ })(({ params, state }) => { const b = state.build(); const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); - return state.update(createStructureTree(data)); + return state.update(createStructureTree(data, false)); }); -function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>): StateTree { - const root = b +function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, supportProps: boolean): StateTree { + let root = b .apply(StateTransforms.Data.ParseCif) .apply(StateTransforms.Model.TrajectoryFromMmCif, {}) - .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) - .apply(StateTransforms.Model.StructureAssemblyFromModel); + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); + + if (supportProps) { + // TODO: implement automatic default property assigment in State.update + root = root.apply(StateTransforms.Model.CustomModelProperties, { properties: [] }); + } + root = root.apply(StateTransforms.Model.StructureAssemblyFromModel); complexRepresentation(root); diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 35fe8c775ed2849dd5c191099b8cc9ec1807d0be..440d3eb78d6501808bf4a355a0c75a24cd4133b1 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -6,7 +6,7 @@ import { PluginStateTransform } from '../objects'; import { PluginStateObject as SO } from '../objects'; -import { Task } from 'mol-task'; +import { Task, RuntimeContext } from 'mol-task'; import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import Expression from 'mol-script/language/expression'; @@ -168,3 +168,25 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({ } }); +export { CustomModelProperties } +type CustomModelProperties = typeof CustomModelProperties +const CustomModelProperties = PluginStateTransform.BuiltIn({ + name: 'custom-model-properties', + display: { name: 'Custom Model Properties' }, + from: SO.Molecule.Model, + to: SO.Molecule.Model, + params: (a, ctx: PluginContext) => ({ properties: ctx.customModelProperties.getSelect(a.data) }) +})({ + apply({ a, params }, ctx: PluginContext) { + return Task.create('Custom Props', async taskCtx => { + await attachProps(a.data, ctx, taskCtx, params.properties); + return new SO.Molecule.Model(a.data, { label: 'Props', description: `${params.properties.length} Selected` }); + }); + } +}); +async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) { + for (const name of names) { + const p = ctx.customModelProperties.get(name); + await p.attach(model).runInContext(taskCtx); + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index ac3330b70dfe374eb44afa13e6455cc75639265c..9d5a802b96009435dadc2175cb8a9236034c8b9a 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -45,7 +45,7 @@ export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArr } render() { - return <div> + return <div style={{ textAlign: 'right' }}> {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)} </div> } diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index bfaff131f548ccdb73cd6add74e849067d3efc85..b1a8775ddc594562b5af5b5ce17c02734f81b485 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -284,6 +284,12 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, const params = this.props.param.params; const label = this.props.param.label || camelCaseToWords(this.props.name); + const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />; + + if (this.props.param.isFlat) { + return controls; + } + return <div className='msp-control-group-wrapper'> <div className='msp-control-group-header'> <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}> @@ -292,7 +298,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, </button> </div> {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}> - <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} /> + {controls} </div> } </div> diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 4ef23dd07f35c1377e00b4694221faf0749abede..f804c554890b23404552d6d1f3052309b2ee9e92 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -96,6 +96,7 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { private wrapper = React.createRef<HTMLDivElement>(); componentDidMount() { + // TODO: only show last 100 entries. this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) })); } @@ -111,10 +112,12 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { } render() { - return <div ref={this.wrapper} style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}> - <ul style={{ listStyle: 'none' }} className='msp-log-list'> + return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}> + <ul className='msp-list-unstyled'> {this.state.entries.map((e, i) => <li key={i}> - <b>[{formatTime(e!.timestamp)} | {e!.type}]</b> {e!.message} + <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} /> + <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div> + <div className='msp-log-entry'>{e!.message}</div> </li>)} </ul> </div>; diff --git a/src/mol-plugin/util/custom-prop-registry.ts b/src/mol-plugin/util/custom-prop-registry.ts new file mode 100644 index 0000000000000000000000000000000000000000..b537d0df3752caf9a4679d8deae0b0909c568bb6 --- /dev/null +++ b/src/mol-plugin/util/custom-prop-registry.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { ModelPropertyDescriptor, Model } from 'mol-model/structure'; +import { OrderedMap } from 'immutable'; +import { ParamDefinition } from 'mol-util/param-definition'; +import { Task } from 'mol-task'; + +export { CustomPropertyRegistry } + +class CustomPropertyRegistry { + private providers = OrderedMap<string, CustomPropertyRegistry.Provider>().asMutable(); + + getSelect(model: Model) { + const values = this.providers.values(); + const options: [string, string][] = [], selected: string[] = []; + while (true) { + const v = values.next(); + if (v.done) break; + if (!v.value.attachableTo(model)) continue; + options.push(v.value.option); + if (v.value.defaultSelected) selected.push(v.value.option[0]); + } + return ParamDefinition.MultiSelect(selected, options); + } + + getDefault(model: Model) { + const values = this.providers.values(); + const selected: string[] = []; + while (true) { + const v = values.next(); + if (v.done) break; + if (!v.value.attachableTo(model)) continue; + if (v.value.defaultSelected) selected.push(v.value.option[0]); + } + return selected; + } + + get(name: string) { + const prop = this.providers.get(name); + if (!prop) throw new Error(`Custom prop '${name}' is not registered.`); + return this.providers.get(name); + } + + register(provider: CustomPropertyRegistry.Provider) { + this.providers.set(provider.descriptor.name, provider); + } + + unregister(name: string) { + this.providers.delete(name); + } +} + +namespace CustomPropertyRegistry { + export interface Provider { + option: [string, string], + defaultSelected: boolean, + descriptor: ModelPropertyDescriptor<any, any>, + attachableTo: (model: Model) => boolean, + attach: (model: Model) => Task<boolean> + } +} \ No newline at end of file diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index 403fa613690465eb60d988359a5e94bcb13206a8..b618052cfe18649afff4fca1d3624f9195b2654d 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -141,11 +141,13 @@ export namespace ParamDefinition { export interface Group<T> extends Base<T> { type: 'group', params: Params, - isExpanded?: boolean + isExpanded?: boolean, + isFlat?: boolean } - export function Group<P extends Params>(params: P, info?: Info & { isExpanded?: boolean }): Group<Values<P>> { + export function Group<P extends Params>(params: P, info?: Info & { isExpanded?: boolean, isFlat?: boolean }): Group<Values<P>> { const ret = setInfo<Group<Values<P>>>({ type: 'group', defaultValue: getDefaultValues(params) as any, params }, info); if (info && info.isExpanded) ret.isExpanded = info.isExpanded; + if (info && info.isFlat) ret.isFlat = info.isFlat; return ret; }