fix search (got broken in b789250) with tests and refactorings
This commit is contained in:
@@ -10,9 +10,9 @@ declare module 'liquor-tree' {
|
||||
clearFilter(): void;
|
||||
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
||||
// getNodeById(id: string): ILiquorTreeExistingNode;
|
||||
// recurseDown(fn: (node: ILiquorTreeExistingNode) => void): void;
|
||||
recurseDown(fn: (node: ILiquorTreeExistingNode) => void): void;
|
||||
}
|
||||
interface ICustomLiquorTreeData {
|
||||
export interface ICustomLiquorTreeData {
|
||||
type: number;
|
||||
documentationUrls: ReadonlyArray<string>;
|
||||
isReversible: boolean;
|
||||
@@ -35,7 +35,7 @@ declare module 'liquor-tree' {
|
||||
data: ILiquorTreeNodeData;
|
||||
states: ILiquorTreeNodeState | undefined;
|
||||
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
||||
expand(): void;
|
||||
// expand(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ declare module 'liquor-tree' {
|
||||
deletion(node: ILiquorTreeNode): boolean;
|
||||
}
|
||||
|
||||
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
||||
export interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { ILiquorTreeOptions, ILiquorTreeFilter, ILiquorTreeNode } from 'liquor-tree';
|
||||
import { ILiquorTreeOptions, ILiquorTreeFilter, ILiquorTreeNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
|
||||
export class LiquorTreeOptions implements ILiquorTreeOptions {
|
||||
public multiple = true;
|
||||
public checkbox = true;
|
||||
public checkOnSelect = true;
|
||||
public readonly multiple = true;
|
||||
public readonly checkbox = true;
|
||||
public readonly checkOnSelect = true;
|
||||
/* For checkbox mode only. Children will have the same checked state as their parent.
|
||||
This is false as it's handled manually to be able to batch select for performance + highlighting */
|
||||
public autoCheckChildren = false;
|
||||
public parentSelect = false;
|
||||
public keyboardNavigation = true;
|
||||
constructor(public filter: ILiquorTreeFilter) { }
|
||||
public readonly autoCheckChildren = false;
|
||||
public readonly parentSelect = false;
|
||||
public readonly keyboardNavigation = true;
|
||||
public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs
|
||||
emptyText: this.liquorTreeFilter.emptyText,
|
||||
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
||||
return this.liquorTreeFilter.matcher(query, node);
|
||||
},
|
||||
};
|
||||
constructor(private readonly liquorTreeFilter: ILiquorTreeFilter) { }
|
||||
public deletion(node: ILiquorTreeNode): boolean {
|
||||
return false; // no op
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { ILiquorTreeExistingNode, ILiquorTreeNewNode, ILiquorTreeNode } from 'liquor-tree';
|
||||
import { NodeType } from './../Node/INode';
|
||||
|
||||
export function updateNodesCheckedState(
|
||||
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
|
||||
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
|
||||
const result = new Array<ILiquorTreeNewNode>();
|
||||
for (const oldNode of oldNodes) {
|
||||
const newState = oldNode.states;
|
||||
newState.checked = getNewCheckedState(oldNode, selectedNodeIds);
|
||||
const newNode: ILiquorTreeNewNode = {
|
||||
id: oldNode.id,
|
||||
text: oldNode.data.text,
|
||||
data: {
|
||||
type: oldNode.data.type,
|
||||
documentationUrls: oldNode.data.documentationUrls,
|
||||
isReversible: oldNode.data.isReversible,
|
||||
},
|
||||
children: !oldNode.children ? [] : updateNodesCheckedState(oldNode.children, selectedNodeIds),
|
||||
state: newState,
|
||||
};
|
||||
result.push(newNode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getNewCheckedState(
|
||||
oldNode: ILiquorTreeNode,
|
||||
selectedNodeIds: ReadonlyArray<string>): boolean {
|
||||
switch (oldNode.data.type) {
|
||||
case NodeType.Script:
|
||||
return selectedNodeIds.some((id) => id === oldNode.id);
|
||||
case NodeType.Category:
|
||||
return parseAllSubScriptIds(oldNode).every((id) => selectedNodeIds.includes(id));
|
||||
default:
|
||||
throw new Error('Unknown node type');
|
||||
}
|
||||
}
|
||||
|
||||
function parseAllSubScriptIds(categoryNode: ILiquorTreeNode): ReadonlyArray<string> {
|
||||
if (categoryNode.data.type !== NodeType.Category) {
|
||||
throw new Error('Not a category node');
|
||||
}
|
||||
if (!categoryNode.children) {
|
||||
return [];
|
||||
}
|
||||
const ids = new Array<string>();
|
||||
for (const child of categoryNode.children) {
|
||||
addNodeIds(child, ids);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function addNodeIds(node: ILiquorTreeNode, ids: string[]) {
|
||||
switch (node.data.type) {
|
||||
case NodeType.Script:
|
||||
ids.push(node.id);
|
||||
break;
|
||||
case NodeType.Category:
|
||||
const subCategoryIds = parseAllSubScriptIds(node);
|
||||
ids.push(...subCategoryIds);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unknown node type');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ILiquorTreeFilter, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
import { convertExistingToNode } from './NodeTranslator';
|
||||
import { INode } from '../Node/INode';
|
||||
import { INode } from './../../Node/INode';
|
||||
|
||||
export type FilterPredicate = (node: INode) => boolean;
|
||||
|
||||
export class NodePredicateFilter implements ILiquorTreeFilter {
|
||||
public emptyText: string = '🕵️Hmm.. Can not see one 🧐';
|
||||
public emptyText = ''; // Does not matter as a custom mesage is shown
|
||||
constructor(private readonly filterPredicate: FilterPredicate) {
|
||||
if (!filterPredicate) {
|
||||
throw new Error('filterPredicate is undefined');
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ILiquorTreeNode } from 'liquor-tree';
|
||||
import { NodeType } from './../../Node/INode';
|
||||
|
||||
export function getNewCheckedState(
|
||||
oldNode: ILiquorTreeNode,
|
||||
selectedNodeIds: ReadonlyArray<string>): boolean {
|
||||
switch (oldNode.data.type) {
|
||||
case NodeType.Script:
|
||||
return selectedNodeIds.some((id) => id === oldNode.id);
|
||||
case NodeType.Category:
|
||||
return parseAllSubScriptIds(oldNode).every((id) => selectedNodeIds.includes(id));
|
||||
default:
|
||||
throw new Error('Unknown node type');
|
||||
}
|
||||
}
|
||||
|
||||
function parseAllSubScriptIds(categoryNode: ILiquorTreeNode): ReadonlyArray<string> {
|
||||
if (categoryNode.data.type !== NodeType.Category) {
|
||||
throw new Error('Not a category node');
|
||||
}
|
||||
if (!categoryNode.children) {
|
||||
return [];
|
||||
}
|
||||
return categoryNode
|
||||
.children
|
||||
.flatMap((child) => getNodeIds(child));
|
||||
}
|
||||
|
||||
function getNodeIds(node: ILiquorTreeNode): ReadonlyArray<string> {
|
||||
switch (node.data.type) {
|
||||
case NodeType.Script:
|
||||
return [ node.id ];
|
||||
case NodeType.Category:
|
||||
return parseAllSubScriptIds(node);
|
||||
default:
|
||||
throw new Error('Unknown node type');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
import { INode } from './../Node/INode';
|
||||
import { INode } from './../../Node/INode';
|
||||
|
||||
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree';
|
||||
import Node from './Node/Node.vue';
|
||||
import { INode } from './Node/INode';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeTranslator';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
|
||||
import { INodeSelectedEvent } from './/INodeSelectedEvent';
|
||||
import { updateNodesCheckedState, getNewCheckedState } from './LiquorTree/NodeStateUpdater';
|
||||
import { getNewCheckedState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
|
||||
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodePredicateFilter';
|
||||
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
|
||||
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component({
|
||||
@@ -84,19 +84,9 @@
|
||||
if (!selectedNodeIds) {
|
||||
throw new Error('Selected nodes are undefined');
|
||||
}
|
||||
const newNodes = updateNodesCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
|
||||
this.getLiquorTreeApi().setModel(newNodes);
|
||||
/* Alternative:
|
||||
this.getLiquorTreeApi().recurseDown((node) => {
|
||||
node.states.checked = selectedNodeIds.includes(node.id);
|
||||
});
|
||||
Problem: Does not check their parent if all children are checked, because it does not
|
||||
trigger update on parent as we work with scripts not categories. */
|
||||
/* Alternative:
|
||||
this.getLiquorTreeApi().recurseDown((node) => {
|
||||
if(selectedNodeIds.includes(node.id)) { node.select(); } else { node.unselect(); }
|
||||
});
|
||||
Problem: Emits nodeSelected() event again which will cause an infinite loop. */
|
||||
this.getLiquorTreeApi().recurseDown((node) => {
|
||||
node.states.checked = getNewCheckedState(node, selectedNodeIds);
|
||||
});
|
||||
}
|
||||
|
||||
private getLiquorTreeApi(): ILiquorTree {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { NodeType, INode } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { NodePredicateFilter } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter';
|
||||
import { ILiquorTreeExistingNode } from 'liquor-tree';
|
||||
|
||||
describe('NodePredicateFilter', () => {
|
||||
it('calls predicate with expected node', () => {
|
||||
// arrange
|
||||
const object: ILiquorTreeExistingNode = {
|
||||
id: 'script',
|
||||
data: {
|
||||
text: 'script-text',
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
states: undefined,
|
||||
children: [],
|
||||
};
|
||||
const expected: INode = {
|
||||
id: 'script',
|
||||
text: 'script-text',
|
||||
isReversible: false,
|
||||
documentationUrls: [],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
};
|
||||
let actual: INode;
|
||||
const predicate = (node: INode) => { actual = node; return true; };
|
||||
const sut = new NodePredicateFilter(predicate);
|
||||
// act
|
||||
sut.matcher('nop query', object);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
describe('returns result from the predicate', () => {
|
||||
for (const expected of [false, true]) {
|
||||
it(expected.toString(), () => {
|
||||
// arrange
|
||||
const sut = new NodePredicateFilter(() => expected);
|
||||
// act
|
||||
const actual = sut.matcher('nop query', getExistingNode());
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function getExistingNode(): ILiquorTreeExistingNode {
|
||||
return {
|
||||
id: 'script',
|
||||
data: {
|
||||
text: 'script-text',
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
states: undefined,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { ILiquorTreeNode } from 'liquor-tree';
|
||||
import { NodeType } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { getNewCheckedState } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
|
||||
describe('getNewCheckedState', () => {
|
||||
describe('script node', () => {
|
||||
it('state is true when selected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', node.id, 'c' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('state is false when unselected', () => {
|
||||
// arrange
|
||||
const node = getScriptNode();
|
||||
const selectedScriptNodeIds = [ 'a', 'b', 'c' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
describe('category node', () => {
|
||||
it('state is true when every child 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', 'b', 'c' ];
|
||||
// act
|
||||
const actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
it('state is 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 actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('state is 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 actual = getNewCheckedState(node, selectedScriptNodeIds);
|
||||
// assert
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getScriptNode(scriptNodeId: string = 'script'): ILiquorTreeNode {
|
||||
return {
|
||||
id: scriptNodeId,
|
||||
data: {
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
isReversible: false,
|
||||
},
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { NodeType, INode } from '@/presentation/Scripts/ScriptsTree/SelectableTree/Node/INode';
|
||||
import { ILiquorTreeExistingNode, ILiquorTreeNewNode, ILiquorTreeNodeData, ICustomLiquorTreeData } from 'liquor-tree';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from '@/presentation/Scripts/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator';
|
||||
|
||||
describe('NodeTranslator', () => {
|
||||
it('convertExistingToNode', () => {
|
||||
// arrange
|
||||
const existingNode = getExistingNode();
|
||||
const expected = getNode();
|
||||
// act
|
||||
const actual = convertExistingToNode(existingNode);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
it('toNewLiquorTreeNode', () => {
|
||||
// arrange
|
||||
const node = getNode();
|
||||
const expected = getNewNode();
|
||||
// act
|
||||
const actual = toNewLiquorTreeNode(node);
|
||||
// assert
|
||||
expect(actual).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
function getNode(): INode {
|
||||
return {
|
||||
id: '1',
|
||||
text: 'parentcategory',
|
||||
isReversible: true,
|
||||
type: NodeType.Category,
|
||||
documentationUrls: [ 'parentcategory-url1', 'parentcategory-url2 '],
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
text: 'subcategory',
|
||||
isReversible: true,
|
||||
documentationUrls: [ 'subcategory-url1', 'subcategory-url2 '],
|
||||
type: NodeType.Category,
|
||||
children: [
|
||||
{
|
||||
id: 'script1',
|
||||
text: 'cool script 1',
|
||||
isReversible: true,
|
||||
documentationUrls: [ 'script1url1', 'script1url2'],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
},
|
||||
{
|
||||
id: 'script2',
|
||||
text: 'cool script 2',
|
||||
isReversible: true,
|
||||
documentationUrls: [ 'script2url1', 'script2url2'],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function getExpectedExistingNodeData(node: INode): ILiquorTreeNodeData {
|
||||
return {
|
||||
text: node.text,
|
||||
type: node.type,
|
||||
documentationUrls: node.documentationUrls,
|
||||
isReversible: node.isReversible,
|
||||
};
|
||||
}
|
||||
function getExpectedNewNodeData(node: INode): ICustomLiquorTreeData {
|
||||
return {
|
||||
type: node.type,
|
||||
documentationUrls: node.documentationUrls,
|
||||
isReversible: node.isReversible,
|
||||
};
|
||||
}
|
||||
|
||||
function getExistingNode(): ILiquorTreeExistingNode {
|
||||
const base = getNode();
|
||||
return {
|
||||
id: base.id,
|
||||
data: getExpectedExistingNodeData(base),
|
||||
states: undefined,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].id,
|
||||
data: getExpectedExistingNodeData(base.children[0]),
|
||||
states: undefined,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].children[0].id,
|
||||
data: getExpectedExistingNodeData(base.children[0].children[0]),
|
||||
states: undefined,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: base.children[0].children[1].id,
|
||||
data: getExpectedExistingNodeData(base.children[0].children[1]),
|
||||
states: undefined,
|
||||
children: [],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function getNewNode(): ILiquorTreeNewNode {
|
||||
const base = getNode();
|
||||
const commonState = {
|
||||
checked: false,
|
||||
};
|
||||
return {
|
||||
id: base.id,
|
||||
text: base.text,
|
||||
data: getExpectedNewNodeData(base),
|
||||
state: commonState,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].id,
|
||||
text: base.children[0].text,
|
||||
data: getExpectedNewNodeData(base.children[0]),
|
||||
state: commonState,
|
||||
children: [
|
||||
{
|
||||
id: base.children[0].children[0].id,
|
||||
text: base.children[0].children[0].text,
|
||||
data: getExpectedNewNodeData(base.children[0].children[0]),
|
||||
state: commonState,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: base.children[0].children[1].id,
|
||||
text: base.children[0].children[1].text,
|
||||
data: getExpectedNewNodeData(base.children[0].children[1]),
|
||||
state: commonState,
|
||||
children: [],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user