fix indeterminate state being lost
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user