Browse Source

mol-plugin-ui: fixed crash caused by invalid state during updates

David Sehnal 5 years ago
parent
commit
949425d14d

+ 1 - 1
src/mol-plugin-state/manager/structure/hierarchy-state.ts

@@ -279,7 +279,7 @@ const Mapping: [TestCell, ApplyRef, LeaveRef][] = [
 ]
 
 function isValidCell(cell?: StateObjectCell): cell is StateObjectCell {
-    if (!cell || !cell.parent.cells.has(cell.transform.ref)) return false;
+    if (!cell || !cell?.parent || !cell.parent.cells.has(cell.transform.ref)) return false;
     const { obj } = cell;
     if (!obj || obj === StateObject.Null || (cell.status !== 'ok' && cell.status !== 'error')) return false;
     return true;

+ 20 - 20
src/mol-plugin-ui/state/tree.tsx

@@ -93,12 +93,12 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
     hasDecorator(children: _StateTree.ChildSet) {
         if (children.size !== 1) return false;
         const ref = children.values().next().value;
-        return !!this.props.cell.parent.tree.transforms.get(ref).transformer.definition.isDecorator;
+        return !!this.props.cell.parent?.tree.transforms.get(ref).transformer.definition.isDecorator;
     }
 
     render() {
         const cell = this.props.cell;
-        if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) {
+        if (!cell || !cell.parent || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) {
             return null;
         }
 
@@ -109,7 +109,7 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
         if (!showLabel) {
             if (children.size === 0) return null;
             return <>
-                {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={this.props.depth} />)}
+                {children.map(c => <StateTreeNode cell={cell.parent!.cells.get(c!)!} key={c} depth={this.props.depth} />)}
             </>;
         }
 
@@ -119,7 +119,7 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
             {children.size === 0
                 ? void 0
                 : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                    {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
+                    {children.map(c => <StateTreeNode cell={cell.parent!.cells.get(c!)!} key={c} depth={newDepth} />)}
                 </div>
             }
         </>;
@@ -151,13 +151,13 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         this.subscribe(this.plugin.state.behavior.currentObject, e => {
             if (!this.is(e)) {
                 if (this.state.isCurrent && e.state.transforms.has(this.ref)) {
-                    this._setCurrent(this.props.cell.parent.current === this.ref, this.state.isCollapsed);
+                    this._setCurrent(this.props.cell.parent!.current === this.ref, this.state.isCollapsed);
                 }
                 return;
             }
 
             if (e.state.transforms.has(this.ref)) {
-                this._setCurrent(this.props.cell.parent.current === this.ref, !!this.props.cell.state.isCollapsed);
+                this._setCurrent(this.props.cell.parent!.current === this.ref, !!this.props.cell.state.isCollapsed);
             }
         });
     }
@@ -171,14 +171,14 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
     }
 
     state: StateTreeNodeLabelState = {
-        isCurrent: this.props.cell.parent.current === this.ref,
+        isCurrent: this.props.cell.parent!.current === this.ref,
         isCollapsed: !!this.props.cell.state.isCollapsed,
         action: void 0,
         currentAction: void 0 as StateAction | undefined
     }
 
     static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
-        const isCurrent = props.cell.parent.current === props.cell.transform.ref;
+        const isCurrent = props.cell.parent!.current === props.cell.transform.ref;
         const isCollapsed = !!props.cell.state.isCollapsed;
 
         if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null;
@@ -188,35 +188,35 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
     setCurrent = (e?: React.MouseEvent<HTMLElement>) => {
         e?.preventDefault();
         e?.currentTarget.blur();
-        PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: this.ref });
+        PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
     }
 
     setCurrentRoot = (e?: React.MouseEvent<HTMLElement>) => {
         e?.preventDefault();
         e?.currentTarget.blur();
-        PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent, ref: StateTransform.RootRef });
+        PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent!, ref: StateTransform.RootRef });
     }
 
     remove = (e?: React.MouseEvent<HTMLElement>) => {
         e?.preventDefault();
-        PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true });
+        PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent!, ref: this.ref, removeParentGhosts: true });
     }
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.ref });
+        PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
         e.currentTarget.blur();
     }
 
     toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleExpanded(this.plugin, { state: this.props.cell.parent, ref: this.ref });
+        PluginCommands.State.ToggleExpanded(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
         e.currentTarget.blur();
     }
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.cell.parent, ref: this.ref });
+        PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
         e.currentTarget.blur();
     }
 
@@ -230,7 +230,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
 
     get actions() {
         const cell = this.props.cell;
-        const actions = [...cell.parent.actions.fromCell(cell, this.plugin)];
+        const actions = [...cell.parent!.actions.fromCell(cell, this.plugin)];
         if (actions.length === 0) return;
 
         actions.sort((a, b) => a.definition.display.name < b.definition.display.name ? -1 : a.definition.display.name === b.definition.display.name ? 0 : 1);
@@ -248,12 +248,12 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
 
     updates(margin: string) {
         const cell = this.props.cell;
-        const decoratorChain = StateTreeSpine.getDecoratorChain(cell.parent, cell.transform.ref);
+        const decoratorChain = StateTreeSpine.getDecoratorChain(cell.parent!, cell.transform.ref);
 
         const decorators = [];
         for (let i = decoratorChain.length - 1; i >= 0; i--) {
             const d = decoratorChain[i];
-            decorators!.push(<UpdateTransformControl key={`${d.transform.transformer.id}-${i}`} state={cell.parent} transform={d.transform} noMargin wrapInExpander expanderHeaderLeftMargin={margin} />);
+            decorators!.push(<UpdateTransformControl key={`${d.transform.transformer.id}-${i}`} state={cell.parent!} transform={d.transform} noMargin wrapInExpander expanderHeaderLeftMargin={margin} />);
         }
 
         return <div className='msp-tree-updates-wrapper'>{decorators}</div>;
@@ -264,7 +264,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         const n = cell.transform;
         if (!cell) return null;
 
-        const isCurrent = this.is(cell.parent.behaviors.currentObject.value);
+        const isCurrent = this.is(cell.parent!.behaviors.currentObject.value);
 
         const disabled = cell.status !== 'error' && cell.status !== 'ok';
 
@@ -282,7 +282,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             </Button>;
         }
 
-        const children = cell.parent.tree.children.get(this.ref);
+        const children = cell.parent!.tree.children.get(this.ref);
         const cellState = cell.state;
 
         const expand = <IconButton icon={cellState.isCollapsed ? 'expand' : 'collapse'} flex='20px' disabled={disabled} onClick={this.toggleExpanded} transparent className='msp-no-hover-outline' style={{ visibility: children.size > 0 ? 'visible' : 'hidden' }} />
@@ -306,7 +306,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             return <div style={{ marginBottom: '1px' }}>
                 {row}
                 <ControlGroup header={`Apply ${this.state.currentAction.definition.display.name}`} initialExpanded={true} hideExpander={true} hideOffset={false} onHeaderClick={this.hideApply} topRightIcon='off' headerLeftMargin={`${this.props.depth * 8 + 21}px`}>
-                    <ApplyActionControl onApply={this.hideApply} state={this.props.cell.parent} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
+                    <ApplyActionControl onApply={this.hideApply} state={this.props.cell.parent!} action={this.state.currentAction} nodeRef={this.props.cell.transform.ref} hideHeader noMargin />
                 </ControlGroup>
             </div>
         }

+ 4 - 3
src/mol-plugin-ui/structure/components.tsx

@@ -298,7 +298,8 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.group[0].cell.parent, ref: this.props.group.map(c => c.cell.transform.ref) });
+        if (this.props.group[0].cell.parent) return;
+        PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.group[0].cell.parent!, ref: this.props.group.map(c => c.cell.transform.ref) });
     }
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
@@ -382,9 +383,9 @@ class StructureRepresentationEntry extends PurePluginUIComponent<{ group: Struct
     render() {
         const repr = this.props.representation.cell;
         return <div className='msp-representation-entry'>
-            <ExpandGroup header={`${repr.obj?.label || ''} Representation`} noOffset>
+            {repr.parent && <ExpandGroup header={`${repr.obj?.label || ''} Representation`} noOffset>
                 <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' customUpdate={this.update} noMargin />
-            </ExpandGroup>
+            </ExpandGroup>}
             <IconButton onClick={this.remove} icon='remove' title='Remove' small className='msp-default-bg' style={{
                 position: 'absolute', top: 0, right: '32px', lineHeight: '24px', height: '24px', textAlign: 'right', width: '44px', paddingRight: '6px'
             }} />

+ 2 - 1
src/mol-plugin-ui/structure/generic.tsx

@@ -78,6 +78,7 @@ export class GenericEntry<T extends HierarchyRef> extends PurePluginUIComponent<
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
+        if (!this.pivot.cell.parent) return;
         PluginCommands.Interactivity.Object.Highlight(this.plugin, {
             state: this.pivot.cell.parent,
             ref: this.props.refs.map(c => c.cell.transform.ref)
@@ -142,7 +143,7 @@ export class GenericEntry<T extends HierarchyRef> extends PurePluginUIComponent<
                 <IconButton className='msp-form-control' onClick={this.toggleVisibility} icon='visual-visibility' toggleState={!pivot.cell.state.isHidden} title={`${pivot.cell.state.isHidden ? 'Show' : 'Hide'}`} small flex />
                 {refs.length === 1 && <IconButton className='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} flex />}
             </div>
-            {(refs.length === 1 && this.state.showOptions) && <>
+            {(refs.length === 1 && this.state.showOptions && pivot.cell.parent) && <>
                 <div className='msp-control-offset'>
                     <UpdateTransformControl state={pivot.cell.parent} transform={pivot.cell.transform} customHeader='none' autoHideApply />
                 </div>

+ 3 - 3
src/mol-plugin-ui/structure/measurements.tsx

@@ -227,12 +227,12 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
     }
 
     delete = () => {
-        PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent, removeParentGhosts: true });
+        PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent!, ref: this.props.cell.transform.parent, removeParentGhosts: true });
     };
 
     toggleVisibility = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.parent });
+        PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent!, ref: this.props.cell.transform.parent });
         e.currentTarget.blur();
     }
 
@@ -299,7 +299,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
                 <IconButton small className='msp-form-control' onClick={this.delete} icon='remove' flex title='Delete' />
                 <IconButton className='msp-form-control' onClick={this.toggleUpdate} icon='dot-3' flex title='Actions' toggleState={this.state.showUpdate} />
             </div>
-            {this.state.showUpdate && <>
+            {this.state.showUpdate && cell.parent && <>
                 <div className='msp-accent-offset'>
                     <ActionMenu items={this.actions} onSelect={this.selectAction} noOffset />
                     <ExpandGroup header='Options' noOffset>

+ 1 - 1
src/mol-plugin-ui/structure/source.tsx

@@ -241,7 +241,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
         if (selection.structures.length !== 1) return null;
         const s = selection.structures[0];
         const params = s.cell.params?.definition;
-        if (!params) return null;
+        if (!params || !s.cell.parent) return null;
 
         return <UpdateTransformControl state={s.cell.parent} transform={s.cell.transform} customHeader='none' customUpdate={this.updateStructure} noMargin autoHideApply />
     }

+ 6 - 2
src/mol-plugin-ui/structure/volume.tsx

@@ -62,8 +62,11 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea
 
     renderEnable() {
         const pivot = this.pivot;
-        const root = StateSelection.findTagInSubtree(this.pivot.cell.parent.tree, this.pivot.cell.transform.ref, VolumeStreaming.RootTag);
-        const rootCell = root && this.pivot.cell.parent.cells.get(root);
+
+        if (!pivot.cell.parent) return null;
+
+        const root = StateSelection.findTagInSubtree(pivot.cell.parent.tree, this.pivot.cell.transform.ref, VolumeStreaming.RootTag);
+        const rootCell = root && pivot.cell.parent.cells.get(root);
 
         const simpleApply = rootCell && rootCell.status === 'error'
             ? { header: 'Error enabling', icon: 'alert' as const, title: rootCell.errorText }
@@ -74,6 +77,7 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea
 
     renderParams() {
         const pivot = this.pivot;
+        if (!pivot.cell.parent) return null;
         const bindings = pivot.volumeStreaming?.cell.transform.params?.entry.params.view.name === 'selection-box' && this.plugin.state.behaviors.cells.get(FocusLoci.id)?.params?.values?.bindings;
         return <>
             <UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin autoHideApply />

+ 1 - 0
src/mol-plugin/behavior/dynamic/custom-props/rcsb/ui/assembly-symmetry.tsx

@@ -55,6 +55,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
 
     renderEnable() {
         const pivot = this.pivot;
+        if (!pivot.cell.parent) return null;
         return <ApplyActionControl state={pivot.cell.parent} action={InitAssemblySymmetry3D} initiallyCollapsed={true} nodeRef={pivot.cell.transform.ref} simpleApply={{ header: 'Enable', icon: 'check' }} />;
     }
 

+ 0 - 1
src/mol-plugin/context.ts

@@ -261,7 +261,6 @@ export class PluginContext {
                 if (timeout !== void 0) clearTimeout(timeout);
                 timeout = void 0;
                 if (isBusy.value) {
-                    // console.log('busy false')
                     isBusy.next(false);
                 }
             }

+ 1 - 1
src/mol-state/object.ts

@@ -65,7 +65,7 @@ namespace StateObject {
 }
 
 interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform = StateTransform> {
-    parent: State,
+    parent?: State,
 
     transform: F,
 

+ 4 - 1
src/mol-state/state.ts

@@ -455,7 +455,10 @@ async function update(ctx: UpdateContext) {
 
         for (const d of deletes) {
             const cell = ctx.cells.get(d);
-            if (cell) unlinkCell(cell);
+            if (cell) {
+                cell.parent = void 0;
+                unlinkCell(cell);
+            }
             const obj = cell && cell.obj;
             ctx.cells.delete(d);
             deletedObjects.push(obj);