import {TreeNode} from "./TreeNode"; import {FullState, Grove, NestedTreeNodeData, NestedTreeNodeDataWithoutIds, StateObjects} from "./Grove"; import {TreeNodeFactory} from "./TreeNodeFactory"; import {Testables} from "./testables"; import {Offers} from "./Offers"; import {StateBranchNode, TieredExtraData} from "./StateBranchNode"; import {Flow} from "./Flow"; import {generateDefaultNestedSelectActionsBranchData, generateIdForType, removeIds} from "./NodeHelpers"; import {NestedTestableNodeData} from "./InMemoryTestableNode"; import {NestedBranchData} from "./MemoryBranchNode"; import {OfferData, TestableData, TestablePartial} from "./store"; import {cloneStrategies} from "./grove-actions"; import {cloneDeep} from "lodash"; import {TierType} from "./tiers"; import {pickByPaths} from "./actions/SelectTierNew"; import {getComponentMetaFromType} from "./ComponentsMeta"; /** * See this class could have a better name, but it's been a long time and I need to get an MVP soon so... */ export class GroveActions { private testables: Testables; private offers: Offers; private treeNodeFactory: TreeNodeFactory; private grove: Grove; private flow: Flow; private stateObjects: StateObjects; constructor( private state: FullState, ) { this.stateObjects = Grove.fromState(state, true); const {grove, testables, offers, treeNodeFactory, flow} = this.stateObjects this.testables = testables this.offers = offers this.treeNodeFactory = treeNodeFactory this.grove = grove this.flow = flow } addTreeNode(treeNode: NestedTreeNodeData | NestedTreeNodeDataWithoutIds | TreeNode, options?: { parentBranchId?: string, position?: number, newTieredBranchData?: { baseTestable: Omit, tierType: TierType }, extraData?: {suggestedScope?: string} }) { // where to add the tree node? if (options?.parentBranchId) { // check if the parent branch is a tiered branch const parentBranch = this.grove.get(options.parentBranchId) if (!parentBranch) { console.error('Cannot add tree node, parent branch not found') return } if (parentBranch.isSelectAction()) { // convert branch to tiered const newBranch = this.grove.convertBranchToTiered(parentBranch, { baseTestable: options.newTieredBranchData!.baseTestable, tierType: options.newTieredBranchData!.tierType, treeNodeData: treeNode as NestedTreeNodeData | NestedTreeNodeDataWithoutIds, }) this.reRenderFlowData(this.grove.get(newBranch?.getId?.())!) } else if (parentBranch.isTiered()) { this.addTier( treeNode as NestedTreeNodeData | NestedTreeNodeDataWithoutIds, options.parentBranchId, options.position ) } // convert it to a tiered branch if target is select actions } else { // add it to the root const treeNodeToAdd = treeNode instanceof TreeNode ? treeNode : this.treeNodeFactory.createFromNestedData(treeNode)! this.grove.addTreeNode(treeNodeToAdd) /** * IMPORTANT! * Get it from the state since the given tree node object is temporary and * may not have all the runtime dependencies needed */ const newTreeNode = this.grove.get(treeNodeToAdd.getId())! this.reRenderFlowData(newTreeNode) this.flow.centerRootNode(newTreeNode) } } cloneTier(targetTreeNodeId: string) { // remember tiers are represented as the tree nodes whose parent branch is tiered const targetTreeNodeToClone = this.grove.get(targetTreeNodeId) const targetBranch = targetTreeNodeToClone?.getParentBranch()! if (!targetTreeNodeToClone) { console.error('Cannot clone tier: tree node not found %s', targetTreeNodeId) return } if (!targetBranch) { console.error('Cannot clone tier: branch not found %s', targetTreeNodeId) return } const treeNodeOfTargetBranch = targetBranch.getTreeNode()! let testablePartialToClone = targetTreeNodeToClone.testableNode.getTestable(); const testableDataToAdd = removeIds(cloneDeep(testablePartialToClone.getNestedData())) as TestablePartial const branchDataToAdd = removeIds(cloneDeep(targetTreeNodeToClone.branchNode.exportData('clone-tier'))) as NestedBranchData const cloneStrategyGroups = cloneStrategies(this.stateObjects) for (const clonerGroupType in cloneStrategyGroups) { const clonerGroup = cloneStrategyGroups[clonerGroupType] clonerGroup.forEach(cloner => { if (cloner.canCloneTier(targetTreeNodeToClone)) { cloner.cloneTier( testableDataToAdd, // @ts-ignore branchDataToAdd as NestedBranchData>, targetTreeNodeToClone, ) } }) } const meta = getComponentMetaFromType(testablePartialToClone.getType())! testableDataToAdd.options = pickByPaths(testableDataToAdd.options, meta.tierFields || []) const newNodeData: Omit = ({ // don't pass an id so that it generates a new one //id: ?, testable: testableDataToAdd, branch: branchDataToAdd, })! const position = targetBranch.getChildIndex(targetTreeNodeId)! + 1 this.addTier(newNodeData, targetBranch.getId(), position + 1) } /** * To manually add a tier to a tiered branch, use addTreeNode() */ private addTier(treeNodeData: NestedTreeNodeData | NestedTreeNodeDataWithoutIds, tieredBranchId: string, position?: number) { // get the branch const targetTieredBranch = this.grove.get(tieredBranchId) if (!targetTieredBranch) { console.error('Cannot add tier to branch, branch not found') return } // let's set the parent testable id to the tree node data so that the testable partial cab be added to the state treeNodeData.parentBranchTestableId = targetTieredBranch.getExtraData().baseTestableId // first create the node if we were given nested data const treeNodeToAdd = this.treeNodeFactory.createFromNestedData(treeNodeData)! // run hooks treeNodeToAdd.beforeAddingToGrove() // then add the treenode to it targetTieredBranch.addChildren([treeNodeToAdd], position) // then save the grove state this.grove.setTreeNodeDataInGrove(targetTieredBranch.getTreeNode()) this.reRenderFlowData(targetTieredBranch) } removeTier(treeNodeId: string) { const treeNodeToRemove = this.grove.get(treeNodeId) if (!treeNodeToRemove) { console.error('Cannot remove tier, tree node not found') return } let parentBranch = treeNodeToRemove.getParentBranch() if (!parentBranch) { console.error('Cannot remove tier, parent branch not found') return } const parentTreeNode = parentBranch.getTreeNode() // remove it from the branch parentBranch = this.removeBranchChild(parentBranch, treeNodeToRemove); // update the grove state this.grove.setTreeNodeDataInGrove(parentBranch.getTreeNode()) // get the root tree node to update its state const rootTreeNode: TreeNode = parentBranch.getTreeNode().getRoot()!; // then regenerate the flow data, its has to be THE ROOT NODE! // if this was a root tree node, we need to remove it from the flow instead of updating it if (treeNodeToRemove.isRoot()) { this.flow.removeTreeNode(treeNodeToRemove.id) } else { this.flow.renderTreeNode(rootTreeNode) } } /** * Will remove all tiers and thus the branch itself will be removed and replaced with a default select action branch */ removeTieredBranch(branchId: string) { const targetBranch = this.grove.get(branchId) if (!targetBranch) { console.error('Cannot remove tiered branch, branch not found') return } else if (!targetBranch.isTiered()) { console.error('Cannot remove tiered branch, branch is not tiered') return } // remove all children from the branch, simple as that.- targetBranch.getChildrenIds().forEach(this.removeTier.bind(this)) } private removeBranchChild(branch: StateBranchNode, itemToRemove: string | TreeNode) { // @ts-ignore branch.removeChildren([itemToRemove]) // now if this is the last item, we need to replace the branch with a default select action branch if (branch.getChildrenIds().length === 0) { branch = this.replaceBranchWithDefault(branch.getId())! } return branch; } replaceBranchWithDefault(branchId: string): StateBranchNode | undefined { const branch = this.grove.get(branchId) if (!branch) { console.error('Cannot replace branch with default, branch not found') return } const newBranch = new StateBranchNode( this.offers, generateDefaultNestedSelectActionsBranchData(), this.testables ); branch.getTreeNode().setBranchNode( newBranch ) // update the grove state this.grove.setTreeNodeDataInGrove(branch.getTreeNode()) this.reRenderFlowData(branch) return newBranch } removeBranchOffer(offerId: string, branchId: string) { const branch = this.grove.get(branchId) if (!branch) { console.error('Cannot remove offer from branch, branch not found') return } this.removeBranchChild(branch, offerId) // state should have been updated by the removeBranchChild method via replaceBranchWithDefault() } addBranchOffer(offerData: Omit | OfferData, branchId: string) { let branch = this.grove.get(branchId) if (!branch) { console.error('Cannot add offer to branch, branch not found') return } if (branch.isSelectAction()) { // if the branch is a select action, we need to replace it with an offers branch const treeNode = branch.getTreeNode() const newOffersBranch = new StateBranchNode( this.stateObjects.offers, { id: generateIdForType('branch'), type: 'offers', children: [ // no children here because they have not been added to the state yet ] }, this.stateObjects.testables, ); treeNode.setBranchNode( newOffersBranch ) // let's update the grove with the new branch so that we can use it later to add the offer this.grove.setTreeNodeDataInGrove(treeNode) // now let's update the reference to the branch so that we can add the offer to it branch = this.grove.get(newOffersBranch.getId())! } // add the offer to the branch branch.addChildren([ // to prevent any issues with other libraries (jotai was causing issues) cloneDeep(offerData) ]) // update the grove state this.grove.setTreeNodeDataInGrove(branch.getTreeNode()) this.reRenderFlowData(branch) } private reRenderFlowData(node: TreeNode) private reRenderFlowData(node: StateBranchNode) private reRenderFlowData(node: undefined) private reRenderFlowData(node: TreeNode | StateBranchNode | undefined) { if (!node) { // this for when you want to pass a node from a function that may return undefined like grove.get() console.error('Cannot re-render flow data, node is undefined') return } const treeNode: TreeNode = node instanceof TreeNode ? node : node.getTreeNode() // get the root tree node to update its state const rootTreeNode: TreeNode = treeNode.getRoot()! // then regenerate the flow data, its has to be THE ROOT NODE! this.flow.renderTreeNode(rootTreeNode) } }