Files
privacy.sexy/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/SelectableTree.vue
undergroundwires 44d79e2c9a Add more and unify tests for absent object cases
- 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.
2022-01-21 22:34:11 +01:00

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>