Introduce new TreeView UI component
Key highlights: - Written from scratch to cater specifically to privacy.sexy's needs and requirements. - The visual look mimics the previous component with minimal changes, but its internal code is completely rewritten. - Lays groundwork for future functionalities like the "expand all" button a flat view mode as discussed in #158. - Facilitates the transition to Vue 3 by omitting the Vue 2.0 dependent `liquour-tree` as part of #230. Improvements and features: - Caching for quicker node queries. - Gradual rendering of nodes that introduces a noticable boost in performance, particularly during search/filtering. - `TreeView` solely governs the check states of branch nodes. Changes: - Keyboard interactions now alter the background color to highlight the focused item. Previously, it was changing the color of the text. - Better state management with clear separation of concerns: - `TreeView` exclusively manages indeterminate states. - `TreeView` solely governs the check states of branch nodes. - Introduce transaction pattern to update state in batches to minimize amount of events handled. - Improve keyboard focus, style background instead of foreground. Use hover/touch color on keyboard focus. - `SelectableTree` has been removed. Instead, `TreeView` is now directly integrated with `ScriptsTree`. - `ScriptsTree` has been refactored to incorporate hooks for clearer code and separation of duties. - Adopt Vue-idiomatic bindings instead of keeping a reference of the tree component. - Simplify and change filter event management. - Abandon global styles in favor of class-scoped styles. - Use global mixins with descriptive names to clarify indended functionality.
This commit is contained in:
@@ -6,6 +6,7 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
||||
import { UserSelectionStub } from './UserSelectionStub';
|
||||
import { UserFilterStub } from './UserFilterStub';
|
||||
@@ -13,42 +14,53 @@ import { ApplicationCodeStub } from './ApplicationCodeStub';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
|
||||
export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
||||
private collectionStub = new CategoryCollectionStub();
|
||||
|
||||
public readonly code: IApplicationCode = new ApplicationCodeStub();
|
||||
|
||||
public filter: IUserFilter = new UserFilterStub();
|
||||
|
||||
public get os(): OperatingSystem {
|
||||
return this.collectionStub.os;
|
||||
return this.collection.os;
|
||||
}
|
||||
|
||||
public get collection(): ICategoryCollection {
|
||||
return this.collectionStub;
|
||||
}
|
||||
public collection: ICategoryCollection = new CategoryCollectionStub();
|
||||
|
||||
public readonly selection: UserSelectionStub;
|
||||
public selection: IUserSelection = new UserSelectionStub([]);
|
||||
|
||||
constructor(readonly allScripts: IScript[] = [new ScriptStub('script-id')]) {
|
||||
this.selection = new UserSelectionStub(allScripts);
|
||||
this.collectionStub = new CategoryCollectionStub()
|
||||
this.collection = new CategoryCollectionStub()
|
||||
.withOs(this.os)
|
||||
.withTotalScripts(this.allScripts.length)
|
||||
.withAction(new CategoryStub(0).withScripts(...allScripts));
|
||||
}
|
||||
|
||||
public withOs(os: OperatingSystem) {
|
||||
this.collectionStub = this.collectionStub.withOs(os);
|
||||
public withCollection(collection: ICategoryCollection): this {
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFilter(filter: IUserFilter) {
|
||||
public withOs(os: OperatingSystem): this {
|
||||
if (this.collection instanceof CategoryCollectionStub) {
|
||||
this.collection = this.collection.withOs(os);
|
||||
} else {
|
||||
this.collection = new CategoryCollectionStub().withOs(os);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFilter(filter: IUserFilter): this {
|
||||
this.filter = filter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withSelectedScripts(initialScripts: readonly SelectedScript[]) {
|
||||
this.selection.withSelectedScripts(initialScripts);
|
||||
public withSelectedScripts(initialScripts: readonly SelectedScript[]): this {
|
||||
return this.withSelection(
|
||||
new UserSelectionStub([]).withSelectedScripts(initialScripts),
|
||||
);
|
||||
}
|
||||
|
||||
public withSelection(selection: IUserSelection) {
|
||||
this.selection = selection;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
42
tests/unit/shared/Stubs/HierarchyAccessStub.ts
Normal file
42
tests/unit/shared/Stubs/HierarchyAccessStub.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { HierarchyAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess';
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
|
||||
export class HierarchyAccessStub implements HierarchyAccess {
|
||||
public parent: TreeNode = undefined;
|
||||
|
||||
public children: readonly TreeNode[] = [];
|
||||
|
||||
public depthInTree = 0;
|
||||
|
||||
public isLeafNode = true;
|
||||
|
||||
public isBranchNode = false;
|
||||
|
||||
public setParent(parent: TreeNode): void {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public setChildren(children: readonly TreeNode[]): void {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public withParent(parent: TreeNode): this {
|
||||
this.parent = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDepthInTree(depthInTree: number): this {
|
||||
this.depthInTree = depthInTree;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withChildren(children: readonly TreeNode[]): this {
|
||||
this.setChildren(children);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withIsBranchNode(value: boolean): this {
|
||||
this.isBranchNode = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
25
tests/unit/shared/Stubs/NodeMetadataStub.ts
Normal file
25
tests/unit/shared/Stubs/NodeMetadataStub.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
|
||||
export class NodeMetadataStub implements NodeMetadata {
|
||||
public id = 'stub-id';
|
||||
|
||||
public readonly text: string = 'stub-text';
|
||||
|
||||
public readonly isReversible: boolean = false;
|
||||
|
||||
public readonly docs: readonly string[] = [];
|
||||
|
||||
public children?: readonly NodeMetadata[] = [];
|
||||
|
||||
public readonly type: NodeType = NodeType.Category;
|
||||
|
||||
public withChildren(children: readonly NodeMetadata[]): this {
|
||||
this.children = children;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
33
tests/unit/shared/Stubs/NodeStateChangedEventStub.ts
Normal file
33
tests/unit/shared/Stubs/NodeStateChangedEventStub.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { NodeStateChangedEvent } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
|
||||
|
||||
export class NodeStateChangedEventStub implements NodeStateChangedEvent {
|
||||
public oldState: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
|
||||
|
||||
public newState: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
|
||||
|
||||
public withNewState(newState: TreeNodeStateDescriptor): this {
|
||||
this.newState = newState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withOldState(oldState: TreeNodeStateDescriptor): this {
|
||||
this.oldState = oldState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCheckStateChange(change: {
|
||||
readonly oldState: TreeNodeCheckState,
|
||||
readonly newState: TreeNodeCheckState,
|
||||
}) {
|
||||
return this
|
||||
.withOldState(
|
||||
new TreeNodeStateDescriptorStub().withCheckState(change.oldState),
|
||||
)
|
||||
.withNewState(
|
||||
new TreeNodeStateDescriptorStub().withCheckState(change.newState),
|
||||
);
|
||||
}
|
||||
}
|
||||
22
tests/unit/shared/Stubs/QueryableNodesStub.ts
Normal file
22
tests/unit/shared/Stubs/QueryableNodesStub.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
|
||||
|
||||
export class QueryableNodesStub implements QueryableNodes {
|
||||
public rootNodes: readonly TreeNode[];
|
||||
|
||||
public flattenedNodes: readonly TreeNode[];
|
||||
|
||||
public getNodeById(): TreeNode {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public withRootNodes(rootNodes: readonly TreeNode[]): this {
|
||||
this.rootNodes = rootNodes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFlattenedNodes(flattenedNodes: readonly TreeNode[]): this {
|
||||
this.flattenedNodes = flattenedNodes;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
9
tests/unit/shared/Stubs/SingleNodeFocusManagerStub.ts
Normal file
9
tests/unit/shared/Stubs/SingleNodeFocusManagerStub.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { SingleNodeFocusManager } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeFocusManager';
|
||||
import { TreeNodeStub } from './TreeNodeStub';
|
||||
|
||||
export class SingleNodeFocusManagerStub implements SingleNodeFocusManager {
|
||||
public currentSingleFocusedNode: TreeNode = new TreeNodeStub();
|
||||
|
||||
setSingleFocus(): void { /* NOOP */ }
|
||||
}
|
||||
26
tests/unit/shared/Stubs/TreeInputNodeDataStub.ts
Normal file
26
tests/unit/shared/Stubs/TreeInputNodeDataStub.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||
|
||||
export class TreeInputNodeDataStub implements TreeInputNodeData {
|
||||
public id = 'stub-id';
|
||||
|
||||
public children?: readonly TreeInputNodeData[];
|
||||
|
||||
public parent?: TreeInputNodeData;
|
||||
|
||||
public data?: object;
|
||||
|
||||
public withData(data: object): this {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withChildren(children: readonly TreeInputNodeData[]): this {
|
||||
this.children = children;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
23
tests/unit/shared/Stubs/TreeNodeCollectionStub.ts
Normal file
23
tests/unit/shared/Stubs/TreeNodeCollectionStub.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/Query/QueryableNodes';
|
||||
import { TreeNodeCollection } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection';
|
||||
import { EventSourceStub } from './EventSourceStub';
|
||||
|
||||
export class TreeNodeCollectionStub implements TreeNodeCollection {
|
||||
public nodes: QueryableNodes;
|
||||
|
||||
public nodesUpdated = new EventSourceStub<QueryableNodes>();
|
||||
|
||||
public updateRootNodes(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public withNodes(nodes: QueryableNodes): this {
|
||||
this.nodes = nodes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public triggerNodesUpdatedEvent(nodes: QueryableNodes): this {
|
||||
this.nodesUpdated.notify(nodes);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
29
tests/unit/shared/Stubs/TreeNodeParserStub.ts
Normal file
29
tests/unit/shared/Stubs/TreeNodeParserStub.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { parseTreeInput } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeInputParser';
|
||||
import { TreeNodeStub } from './TreeNodeStub';
|
||||
|
||||
interface StubScenario {
|
||||
readonly given: readonly TreeInputNodeData[],
|
||||
readonly result: TreeNode[],
|
||||
}
|
||||
|
||||
export function createTreeNodeParserStub() {
|
||||
const scenarios = new Array<StubScenario>();
|
||||
function registerScenario(scenario: StubScenario) {
|
||||
scenarios.push(scenario);
|
||||
}
|
||||
const parseTreeInputStub: typeof parseTreeInput = (
|
||||
data: readonly TreeInputNodeData[],
|
||||
): TreeNode[] => {
|
||||
const result = scenarios.find((scenario) => scenario.given === data);
|
||||
if (result !== undefined) {
|
||||
return result.result;
|
||||
}
|
||||
return data.map(() => new TreeNodeStub());
|
||||
};
|
||||
return {
|
||||
registerScenario,
|
||||
parseTreeInputStub,
|
||||
};
|
||||
}
|
||||
45
tests/unit/shared/Stubs/TreeNodeStateAccessStub.ts
Normal file
45
tests/unit/shared/Stubs/TreeNodeStateAccessStub.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NodeStateChangedEvent, TreeNodeStateAccess, TreeNodeStateTransaction } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
import { TreeNodeStateDescriptorStub } from './TreeNodeStateDescriptorStub';
|
||||
import { EventSourceStub } from './EventSourceStub';
|
||||
import { TreeNodeStateTransactionStub } from './TreeNodeStateTransactionStub';
|
||||
|
||||
export class TreeNodeStateAccessStub implements TreeNodeStateAccess {
|
||||
public current: TreeNodeStateDescriptor = new TreeNodeStateDescriptorStub();
|
||||
|
||||
public changed: EventSourceStub<NodeStateChangedEvent> = new EventSourceStub();
|
||||
|
||||
public toggleCheck(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public toggleExpand(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public beginTransaction(): TreeNodeStateTransaction {
|
||||
return new TreeNodeStateTransactionStub();
|
||||
}
|
||||
|
||||
public commitTransaction(transaction: TreeNodeStateTransaction): void {
|
||||
const oldState = this.current;
|
||||
const newState = {
|
||||
...oldState,
|
||||
...transaction.updatedState,
|
||||
};
|
||||
this.current = newState;
|
||||
this.changed.notify({
|
||||
oldState,
|
||||
newState,
|
||||
});
|
||||
}
|
||||
|
||||
public triggerStateChangedEvent(event: NodeStateChangedEvent) {
|
||||
this.changed.notify(event);
|
||||
}
|
||||
|
||||
public withCurrent(state: TreeNodeStateDescriptor): this {
|
||||
this.current = state;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
24
tests/unit/shared/Stubs/TreeNodeStateDescriptorStub.ts
Normal file
24
tests/unit/shared/Stubs/TreeNodeStateDescriptorStub.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
|
||||
export class TreeNodeStateDescriptorStub implements TreeNodeStateDescriptor {
|
||||
public checkState: TreeNodeCheckState = TreeNodeCheckState.Checked;
|
||||
|
||||
public isExpanded = false;
|
||||
|
||||
public isVisible = false;
|
||||
|
||||
public isMatched = false;
|
||||
|
||||
public isFocused = false;
|
||||
|
||||
public withFocusState(isFocused: boolean): this {
|
||||
this.isFocused = isFocused;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCheckState(checkState: TreeNodeCheckState): this {
|
||||
this.checkState = checkState;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
32
tests/unit/shared/Stubs/TreeNodeStateTransactionStub.ts
Normal file
32
tests/unit/shared/Stubs/TreeNodeStateTransactionStub.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
|
||||
import { TreeNodeStateTransaction } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { TreeNodeStateDescriptor } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateDescriptor';
|
||||
|
||||
export class TreeNodeStateTransactionStub implements TreeNodeStateTransaction {
|
||||
public withExpansionState(isExpanded: boolean): TreeNodeStateTransaction {
|
||||
this.updatedState = { ...this.updatedState, isExpanded };
|
||||
return this;
|
||||
}
|
||||
|
||||
public withMatchState(isMatched: boolean): TreeNodeStateTransaction {
|
||||
this.updatedState = { ...this.updatedState, isMatched };
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFocusState(isFocused: boolean): TreeNodeStateTransaction {
|
||||
this.updatedState = { ...this.updatedState, isFocused };
|
||||
return this;
|
||||
}
|
||||
|
||||
public withVisibilityState(isVisible: boolean): TreeNodeStateTransaction {
|
||||
this.updatedState = { ...this.updatedState, isVisible };
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCheckState(checkState: TreeNodeCheckState): TreeNodeStateTransaction {
|
||||
this.updatedState = { ...this.updatedState, checkState };
|
||||
return this;
|
||||
}
|
||||
|
||||
public updatedState: Partial<TreeNodeStateDescriptor>;
|
||||
}
|
||||
35
tests/unit/shared/Stubs/TreeNodeStub.ts
Normal file
35
tests/unit/shared/Stubs/TreeNodeStub.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { HierarchyAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess';
|
||||
import { TreeNodeStateAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { NodeMetadataStub } from './NodeMetadataStub';
|
||||
import { HierarchyAccessStub } from './HierarchyAccessStub';
|
||||
|
||||
export class TreeNodeStub implements TreeNode {
|
||||
public state: TreeNodeStateAccess;
|
||||
|
||||
public hierarchy: HierarchyAccess = new HierarchyAccessStub();
|
||||
|
||||
public id: string;
|
||||
|
||||
public metadata?: object = new NodeMetadataStub();
|
||||
|
||||
public withMetadata(metadata: object): this {
|
||||
this.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withHierarchy(hierarchy: HierarchyAccess): this {
|
||||
this.hierarchy = hierarchy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withState(state: TreeNodeStateAccess): this {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
16
tests/unit/shared/Stubs/TreeRootStub.ts
Normal file
16
tests/unit/shared/Stubs/TreeRootStub.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { SingleNodeFocusManager } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/Focus/SingleNodeFocusManager';
|
||||
import { TreeNodeCollection } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/NodeCollection/TreeNodeCollection';
|
||||
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
|
||||
import { TreeNodeCollectionStub } from './TreeNodeCollectionStub';
|
||||
import { SingleNodeFocusManagerStub } from './SingleNodeFocusManagerStub';
|
||||
|
||||
export class TreeRootStub implements TreeRoot {
|
||||
public collection: TreeNodeCollection = new TreeNodeCollectionStub();
|
||||
|
||||
public focus: SingleNodeFocusManager = new SingleNodeFocusManagerStub();
|
||||
|
||||
public withCollection(collection: TreeNodeCollection): this {
|
||||
this.collection = collection;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,14 @@ import { CategoryCollectionStateStub } from './CategoryCollectionStateStub';
|
||||
import { EventSubscriptionCollectionStub } from './EventSubscriptionCollectionStub';
|
||||
import { ApplicationContextStub } from './ApplicationContextStub';
|
||||
import { UserFilterStub } from './UserFilterStub';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class UseCollectionStateStub {
|
||||
export class UseCollectionStateStub
|
||||
extends StubWithObservableMethodCalls<ReturnType<typeof useCollectionState>> {
|
||||
private currentContext: IApplicationContext = new ApplicationContextStub();
|
||||
|
||||
private readonly currentState = ref<ICategoryCollectionState>(new CategoryCollectionStateStub());
|
||||
|
||||
private readonly onStateChangeHandlers = new Array<NewStateEventHandler>();
|
||||
|
||||
public withFilter(filter: IUserFilter) {
|
||||
const state = new CategoryCollectionStateStub()
|
||||
.withFilter(filter);
|
||||
@@ -49,10 +49,18 @@ export class UseCollectionStateStub {
|
||||
return this.currentState.value;
|
||||
}
|
||||
|
||||
public triggerOnStateChange(newState: ICategoryCollectionState): void {
|
||||
this.currentState.value = newState;
|
||||
this.onStateChangeHandlers.forEach(
|
||||
(handler) => handler(newState, undefined),
|
||||
public triggerOnStateChange(scenario: {
|
||||
readonly newState: ICategoryCollectionState,
|
||||
readonly immediateOnly: boolean,
|
||||
}): void {
|
||||
this.currentState.value = scenario.newState;
|
||||
let calls = this.callHistory.filter((call) => call.methodName === 'onStateChange');
|
||||
if (scenario.immediateOnly) {
|
||||
calls = calls.filter((call) => call.args[1].immediate === true);
|
||||
}
|
||||
const handlers = calls.map((call) => call.args[0] as NewStateEventHandler);
|
||||
handlers.forEach(
|
||||
(handler) => handler(scenario.newState, undefined),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,15 +71,26 @@ export class UseCollectionStateStub {
|
||||
if (settings?.immediate) {
|
||||
handler(this.currentState.value, undefined);
|
||||
}
|
||||
this.onStateChangeHandlers.push(handler);
|
||||
this.registerMethodCall({
|
||||
methodName: 'onStateChange',
|
||||
args: [handler, settings],
|
||||
});
|
||||
}
|
||||
|
||||
private modifyCurrentState(mutator: StateModifier) {
|
||||
mutator(this.currentState.value);
|
||||
this.registerMethodCall({
|
||||
methodName: 'modifyCurrentState',
|
||||
args: [mutator],
|
||||
});
|
||||
}
|
||||
|
||||
private modifyCurrentContext(mutator: ContextModifier) {
|
||||
mutator(this.currentContext);
|
||||
this.registerMethodCall({
|
||||
methodName: 'modifyCurrentContext',
|
||||
args: [mutator],
|
||||
});
|
||||
}
|
||||
|
||||
public get(): ReturnType<typeof useCollectionState> {
|
||||
|
||||
@@ -3,19 +3,23 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class UserSelectionStub implements IUserSelection {
|
||||
export class UserSelectionStub
|
||||
extends StubWithObservableMethodCalls<IUserSelection>
|
||||
implements IUserSelection {
|
||||
public readonly changed: IEventSource<readonly SelectedScript[]> = new EventSource<
|
||||
readonly SelectedScript[]>();
|
||||
|
||||
public selectedScripts: readonly SelectedScript[] = [];
|
||||
|
||||
constructor(private readonly allScripts: readonly IScript[]) {
|
||||
|
||||
super();
|
||||
}
|
||||
|
||||
public withSelectedScripts(selectedScripts: readonly SelectedScript[]) {
|
||||
public withSelectedScripts(selectedScripts: readonly SelectedScript[]): this {
|
||||
this.selectedScripts = selectedScripts;
|
||||
return this;
|
||||
}
|
||||
|
||||
public areAllSelected(): boolean {
|
||||
@@ -34,16 +38,22 @@ export class UserSelectionStub implements IUserSelection {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public addSelectedScript(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
this.registerMethodCall({
|
||||
methodName: 'addSelectedScript',
|
||||
args: [scriptId, revert],
|
||||
});
|
||||
}
|
||||
|
||||
public addOrUpdateSelectedScript(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public removeSelectedScript(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
public removeSelectedScript(scriptId: string): void {
|
||||
this.registerMethodCall({
|
||||
methodName: 'removeSelectedScript',
|
||||
args: [scriptId],
|
||||
});
|
||||
}
|
||||
|
||||
public selectOnly(scripts: ReadonlyArray<IScript>): void {
|
||||
|
||||
Reference in New Issue
Block a user