fix indeterminate state being lost

This commit is contained in:
undergroundwires
2020-09-06 15:26:19 +01:00
parent c7b2a70312
commit 1f266c3353
7 changed files with 227 additions and 100 deletions

View File

@@ -20,6 +20,7 @@ declare module 'liquor-tree' {
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js // https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
export interface ILiquorTreeNodeState { export interface ILiquorTreeNodeState {
checked: boolean; checked: boolean;
indeterminate: boolean;
} }
export interface ILiquorTreeNode { export interface ILiquorTreeNode {

View File

@@ -5,9 +5,10 @@ export class LiquorTreeOptions implements ILiquorTreeOptions {
public readonly checkbox = true; public readonly checkbox = true;
public readonly checkOnSelect = true; public readonly checkOnSelect = true;
/* For checkbox mode only. Children will have the same checked state as their parent. /* For checkbox mode only. Children will have the same checked state as their parent.
⚠️ Setting this false, does not update indeterminate state of nodes.
This is false as it's handled manually to be able to batch select for performance + highlighting */ This is false as it's handled manually to be able to batch select for performance + highlighting */
public readonly autoCheckChildren = false; public readonly autoCheckChildren = false;
public readonly parentSelect = false; public readonly parentSelect = true;
public readonly keyboardNavigation = true; public readonly keyboardNavigation = true;
public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs
emptyText: this.liquorTreeFilter.emptyText, emptyText: this.liquorTreeFilter.emptyText,

View File

@@ -1,14 +1,37 @@
import { ILiquorTreeNode } from 'liquor-tree'; import { ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
import { NodeType } from './../../Node/INode'; import { NodeType } from './../../Node/INode';
export function getNewCheckedState( export function getNewState(
oldNode: ILiquorTreeNode, node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
const checked = getNewCheckedState(node, selectedNodeIds);
const indeterminate = !checked && getNewIndeterminateState(node, selectedNodeIds);
return {
checked, indeterminate,
};
}
function getNewIndeterminateState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean { selectedNodeIds: ReadonlyArray<string>): boolean {
switch (oldNode.data.type) { switch (node.data.type) {
case NodeType.Script: case NodeType.Script:
return selectedNodeIds.some((id) => id === oldNode.id); return false;
case NodeType.Category: case NodeType.Category:
return parseAllSubScriptIds(oldNode).every((id) => selectedNodeIds.includes(id)); return parseAllSubScriptIds(node).some((id) => selectedNodeIds.includes(id));
default:
throw new Error('Unknown node type');
}
}
function getNewCheckedState(
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean {
switch (node.data.type) {
case NodeType.Script:
return selectedNodeIds.some((id) => id === node.id);
case NodeType.Category:
return parseAllSubScriptIds(node).every((id) => selectedNodeIds.includes(id));
default: default:
throw new Error('Unknown node type'); throw new Error('Unknown node type');
} }

View File

@@ -23,6 +23,7 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
text: node.text, text: node.text,
state: { state: {
checked: false, checked: false,
indeterminate: false,
}, },
children: convertChildren(node.children, toNewLiquorTreeNode), children: convertChildren(node.children, toNewLiquorTreeNode),
data: { data: {

View File

@@ -18,14 +18,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree'; import LiquorTree from 'liquor-tree';
import Node from './Node/Node.vue'; import Node from './Node/Node.vue';
import { INode } from './Node/INode'; import { INode } from './Node/INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator'; import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
import { INodeSelectedEvent } from './/INodeSelectedEvent'; import { INodeSelectedEvent } from './/INodeSelectedEvent';
import { getNewCheckedState } from './LiquorTree/NodeWrapper/NodeStateUpdater'; import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions'; import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter'; import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
import { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */ /** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
@Component({ @Component({
@@ -49,7 +50,7 @@
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node)); const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
if (this.selectedNodeIds) { if (this.selectedNodeIds) {
recurseDown(initialNodes, recurseDown(initialNodes,
(node) => node.state.checked = getNewCheckedState(node, this.selectedNodeIds)); (node) => node.state = updateState(node.state, node, this.selectedNodeIds));
} }
this.initialLiquourTreeNodes = initialNodes; this.initialLiquourTreeNodes = initialNodes;
} else { } else {
@@ -82,11 +83,11 @@
@Watch('selectedNodeIds') @Watch('selectedNodeIds')
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) { public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
if (!selectedNodeIds) { if (!selectedNodeIds) {
throw new Error('Selected nodes are undefined'); throw new Error('SelectedrecurseDown nodes are undefined');
} }
this.getLiquorTreeApi().recurseDown((node) => { this.getLiquorTreeApi().recurseDown(
node.states.checked = getNewCheckedState(node, selectedNodeIds); (node) => node.states = updateState(node.states, node, selectedNodeIds),
}); );
} }
private getLiquorTreeApi(): ILiquorTree { private getLiquorTreeApi(): ILiquorTree {
@@ -97,6 +98,13 @@
} }
} }
function updateState(
old: ILiquorTreeNodeState,
node: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
return {...old, ...getNewState(node, selectedNodeIds)};
}
function recurseDown( function recurseDown(
nodes: ReadonlyArray<ILiquorTreeNewNode>, nodes: ReadonlyArray<ILiquorTreeNewNode>,
handler: (node: ILiquorTreeNewNode) => void) { handler: (node: ILiquorTreeNewNode) => void) {

View File

@@ -2,97 +2,189 @@ import 'mocha';
import { expect } from 'chai'; import { expect } from 'chai';
import { ILiquorTreeNode } from 'liquor-tree'; import { ILiquorTreeNode } from 'liquor-tree';
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode'; import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
import { getNewCheckedState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater'; import { getNewState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
describe('getNewCheckedState', () => { describe('getNewState', () => {
describe('script node', () => { describe('checked', () => {
it('state is true when selected', () => { describe('script node', () => {
// arrange it('true when selected', () => {
const node = getScriptNode(); // arrange
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ]; const node = getScriptNode();
// act const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
const actual = getNewCheckedState(node, selectedScriptNodeIds); // act
// assert const state = getNewState(node, selectedScriptNodeIds);
expect(actual).to.equal(true); // assert
expect(state.checked).to.equal(true);
});
it('false when unselected', () => {
// arrange
const node = getScriptNode();
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
}); });
it('state is false when unselected', () => { describe('category node', () => {
// arrange it('true when every child selected', () => {
const node = getScriptNode(); // arrange
const selectedScriptNodeIds = [ 'a', 'b', 'c' ]; const node = {
// act id: '1',
const actual = getNewCheckedState(node, selectedScriptNodeIds); data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
// assert children: [
expect(actual).to.equal(false); { id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(true);
});
it('false when none of the children is selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
it('false when some of the children is selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{
id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{
id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.checked).to.equal(false);
});
}); });
}); });
describe('category node', () => { describe('indeterminate', () => {
it('state is true when every child selected', () => { describe('script node', () => {
// arrange it('false when selected', () => {
const node = { // arrange
id: '1', const node = getScriptNode();
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
children: [ // act
{ id: '2', const state = getNewState(node, selectedScriptNodeIds);
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, // assert
children: [ getScriptNode('a'), getScriptNode('b') ], expect(state.indeterminate).to.equal(false);
}, });
{ id: '3', it('false when not selected', () => {
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, // arrange
children: [ getScriptNode('c') ], const node = getScriptNode();
}, const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
], // act
}; const state = getNewState(node, selectedScriptNodeIds);
const selectedScriptNodeIds = [ 'a', 'b', 'c' ]; // assert
// act expect(state.indeterminate).to.equal(false);
const actual = getNewCheckedState(node, selectedScriptNodeIds); });
// assert
expect(actual).to.equal(true);
}); });
it('state is false when none of the children is selected', () => { describe('category node', () => {
// arrange it('false when all children are selected', () => {
const node = { // arrange
id: '1', const node = {
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, id: '1',
children: [ data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
{ id: '2', children: [
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, { id: '2',
children: [ getScriptNode('a'), getScriptNode('b') ], data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
}, children: [ getScriptNode('a'), getScriptNode('b') ],
{ id: '3', },
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, { id: '3',
children: [ getScriptNode('c') ], data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
}, children: [ getScriptNode('c') ],
], },
}; ],
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ]; };
// act const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
const actual = getNewCheckedState(node, selectedScriptNodeIds); // act
// assert const state = getNewState(node, selectedScriptNodeIds);
expect(actual).to.equal(false); // assert
}); expect(state.indeterminate).to.equal(false);
it('state is false when some of the children is selected', () => { });
// arrange it('true when all some are selected', () => {
const node = { // arrange
id: '1', const node = {
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, id: '1',
children: [ data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
{ children: [
id: '2', { id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ], children: [ getScriptNode('a'), getScriptNode('b') ],
}, },
{ { id: '3',
id: '3', data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
data: { type: NodeType.Category, documentationUrls: [], isReversible: false }, children: [ getScriptNode('c') ],
children: [ getScriptNode('c') ], },
}, ],
], };
}; const selectedScriptNodeIds = [ 'a' ];
const selectedScriptNodeIds = [ 'a', 'c', 'unrelated' ]; // act
// act const state = getNewState(node, selectedScriptNodeIds);
const actual = getNewCheckedState(node, selectedScriptNodeIds); // assert
// assert expect(state.indeterminate).to.equal(true);
expect(actual).to.equal(false); });
it('false when no children are selected', () => {
// arrange
const node = {
id: '1',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [
{ id: '2',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('a'), getScriptNode('b') ],
},
{ id: '3',
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
children: [ getScriptNode('c') ],
},
],
};
const selectedScriptNodeIds = [ 'none', 'of', 'them', 'are', 'selected' ];
// act
const state = getNewState(node, selectedScriptNodeIds);
// assert
expect(state.indeterminate).to.equal(false);
});
}); });
}); });
}); });

View File

@@ -108,6 +108,7 @@ function getNewNode(): ILiquorTreeNewNode {
const base = getNode(); const base = getNode();
const commonState = { const commonState = {
checked: false, checked: false,
indeterminate: false,
}; };
return { return {
id: base.id, id: base.id,