- Unify test data for nonexistence of an object/string and collection. - Introduce more test through adding missing test data to existing tests. - Improve logic for checking absence of values to match tests. - Add missing tests for absent value validation. - Update documentation to include shared test functionality.
164 lines
4.9 KiB
Vue
164 lines
4.9 KiB
Vue
<template>
|
|
<span>
|
|
<span v-if="initialLiquorTreeNodes != null && initialLiquorTreeNodes.length > 0">
|
|
<tree :options="liquorTreeOptions"
|
|
:data="initialLiquorTreeNodes"
|
|
v-on:node:checked="nodeSelected($event)"
|
|
v-on:node:unchecked="nodeSelected($event)"
|
|
ref="treeElement"
|
|
>
|
|
<span class="tree-text" slot-scope="{ node }" >
|
|
<Node :data="convertExistingToNode(node)" />
|
|
</span>
|
|
</tree>
|
|
</span>
|
|
<span v-else>Nooo 😢</span>
|
|
</span>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
Component, Prop, Vue, Watch,
|
|
} from 'vue-property-decorator';
|
|
import LiquorTree, {
|
|
ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeNode, ILiquorTreeNodeState,
|
|
} from 'liquor-tree';
|
|
import { sleep } from '@/infrastructure/Threading/AsyncSleep';
|
|
import Node from './Node/Node.vue';
|
|
import { INode } from './Node/INode';
|
|
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
|
|
import { INodeSelectedEvent } from './INodeSelectedEvent';
|
|
import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
|
|
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
|
|
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
|
|
|
|
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
|
@Component({
|
|
components: {
|
|
LiquorTree,
|
|
Node,
|
|
},
|
|
})
|
|
export default class SelectableTree extends Vue { // Stateless to make it easier to switch out
|
|
@Prop() public filterPredicate?: FilterPredicate;
|
|
|
|
@Prop() public filterText?: string;
|
|
|
|
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
|
|
|
|
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
|
|
|
public initialLiquorTreeNodes?: ILiquorTreeNewNode[] = null;
|
|
|
|
public liquorTreeOptions = new LiquorTreeOptions(
|
|
new NodePredicateFilter((node) => this.filterPredicate(node)),
|
|
);
|
|
|
|
public convertExistingToNode = convertExistingToNode;
|
|
|
|
public nodeSelected(node: ILiquorTreeExistingNode) {
|
|
const event: INodeSelectedEvent = {
|
|
node: convertExistingToNode(node),
|
|
isSelected: node.states.checked,
|
|
};
|
|
this.$emit('nodeSelected', event);
|
|
}
|
|
|
|
@Watch('initialNodes', { immediate: true })
|
|
public async updateNodes(nodes: readonly INode[]) {
|
|
if (!nodes) {
|
|
throw new Error('missing initial nodes');
|
|
}
|
|
const initialNodes = nodes.map((node) => toNewLiquorTreeNode(node));
|
|
if (this.selectedNodeIds) {
|
|
recurseDown(
|
|
initialNodes,
|
|
(node) => {
|
|
node.state = updateState(node.state, node, this.selectedNodeIds);
|
|
},
|
|
);
|
|
}
|
|
this.initialLiquorTreeNodes = initialNodes;
|
|
const api = await this.getLiquorTreeApi();
|
|
// We need to set the model manually on each update because liquor tree is not reactive to data
|
|
// changes after its initialization.
|
|
api.setModel(this.initialLiquorTreeNodes);
|
|
}
|
|
|
|
@Watch('filterText', { immediate: true })
|
|
public async updateFilterText(filterText: |string) {
|
|
const api = await this.getLiquorTreeApi();
|
|
if (!filterText) {
|
|
api.clearFilter();
|
|
} else {
|
|
api.filter('filtered'); // text does not matter, it'll trigger the filterPredicate
|
|
}
|
|
}
|
|
|
|
@Watch('selectedNodeIds')
|
|
public async setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
|
|
if (!selectedNodeIds) {
|
|
throw new Error('Selected recurseDown nodes are undefined');
|
|
}
|
|
const tree = await this.getLiquorTreeApi();
|
|
tree.recurseDown(
|
|
(node) => {
|
|
node.states = updateState(node.states, node, selectedNodeIds);
|
|
},
|
|
);
|
|
}
|
|
|
|
private async getLiquorTreeApi(): Promise<ILiquorTree> {
|
|
const accessor = (): ILiquorTree => {
|
|
const uiElement = this.$refs.treeElement;
|
|
type TreeElement = typeof uiElement & {tree: ILiquorTree};
|
|
return uiElement ? (uiElement as TreeElement).tree : undefined;
|
|
};
|
|
const treeElement = await tryUntilDefined(accessor, 5, 20); // Wait for it to render
|
|
if (!treeElement) {
|
|
throw Error('Referenced tree element cannot be found. Perhaps it\'s not yet rendered?');
|
|
}
|
|
return treeElement;
|
|
}
|
|
}
|
|
|
|
function updateState(
|
|
old: ILiquorTreeNodeState,
|
|
node: ILiquorTreeNode,
|
|
selectedNodeIds: ReadonlyArray<string>,
|
|
): ILiquorTreeNodeState {
|
|
return { ...old, ...getNewState(node, selectedNodeIds) };
|
|
}
|
|
|
|
function recurseDown(
|
|
nodes: ReadonlyArray<ILiquorTreeNewNode>,
|
|
handler: (node: ILiquorTreeNewNode) => void,
|
|
) {
|
|
for (const node of nodes) {
|
|
handler(node);
|
|
if (node.children) {
|
|
recurseDown(node.children, handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function tryUntilDefined<T>(
|
|
accessor: () => T | undefined,
|
|
delayInMs: number,
|
|
maxTries: number,
|
|
): Promise<T | undefined> {
|
|
let triesLeft = maxTries;
|
|
let value: T;
|
|
while (triesLeft !== 0) {
|
|
value = accessor();
|
|
if (value) {
|
|
return value;
|
|
}
|
|
triesLeft--;
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await sleep(delayInMs);
|
|
}
|
|
return value;
|
|
}
|
|
</script>
|