import _ from 'lodash' import { LIST } from '@adalo/constants' import calculatePushRelations from 'utils/variableHeight/calculatePushRelations' import { makePush, calculateHeightFromChildren, } from 'utils/variableHeight/runtime' import { deepFilter, deepMap, traverse, traverseLeafsFirst, } from 'utils/treeTraversal' import { RangeWithY, RunnerPartialObject } from './types' import PushGraph from './types/PushGraph' import { getXRangeWithY } from './range' import { buildRunnerPartialObject } from './buildRunnerPartialObject' import { DataBindingOptions } from './types/RunnerPartialObject' export interface PushNode { range: RangeWithY objectId: string type: string originalY: number yOffset: number originalHeight: number pushedBy: PushNode[] pushes: PushNode[] children: PushNode[] parent?: PushNode dataNode?: DataNode masonry?: boolean columnCount?: number rowMargin?: number paddingBottom?: number options?: DataBindingOptions // Pre-baked push graphs from the editor pushGraph?: PushGraph } export interface DataNode { id: string name: string listItems: number[] height: number visible: boolean visibleOnDevice: boolean pushNode?: PushNode hiddenHeight?: number } export const createDataNode = ( pushId: string, object: RunnerPartialObject ): DataNode => ({ id: pushId, name: object.attributes.name, listItems: [], height: Math.round(object.attributes.height), visible: true, visibleOnDevice: true, hiddenHeight: 0, }) export const createPushNode = ({ id, type, pushGraph, dataBinding, attributes: { width, x, adjustedY, masonry, columnCount, rowMargin, height, paddingBottom, }, }: RunnerPartialObject): PushNode => ({ range: getXRangeWithY(x, width, adjustedY), objectId: id, type, originalY: Math.round(adjustedY), yOffset: 0, originalHeight: Math.round(height), pushGraph, pushedBy: [], pushes: [], children: [], masonry, options: dataBinding?.options, columnCount, rowMargin, paddingBottom, }) interface ListItemTemplate { listId: string listName: string children: RunnerPartialObject[] pushGraph?: PushGraph } const buildListItemTemplate = (obj: RunnerPartialObject): ListItemTemplate => { return { listId: obj.id, listName: obj.attributes.name, children: obj.children, pushGraph: obj.pushGraph, } } const mapListItemPushGraph = ( pushGraph: PushGraph, listItemId: string ): PushGraph => { const nodeIds = pushGraph.nodeIds.map(nodeId => getListItemChildId(listItemId, nodeId) ) const edges = pushGraph.edges.map(edge => ({ ...edge, startNodeId: getListItemChildId(listItemId, edge.startNodeId), endNodeId: getListItemChildId(listItemId, edge.endNodeId), })) return { nodeIds, edges } } const createListItem = ( template: ListItemTemplate, itemId: number ): RunnerPartialObject => { const { listId, listName, children: templateChildren, pushGraph: templatePushGraph = { nodeIds: [], edges: [] }, } = template const objectId = getListItemObjectId(listId, itemId) const children = templateChildren.map(templateChild => generateObjectFromTemplate(templateChild, objectId) ) const pushGraph = mapListItemPushGraph(templatePushGraph, objectId) return buildRunnerPartialObject({ id: objectId, type: 'LIST_ITEM', itemId, children, pushGraph, name: `${listName} - item ${itemId}`, height: 0, width: 0, x: 0, adjustedY: 0, }) } const getListItemChildId = ( listItemId: string, templateChildId: string ): string => `${listItemId} -- ${templateChildId}` export const generateObjectFromTemplate = ( templateObj: RunnerPartialObject, parentId: string ): RunnerPartialObject => { let { children } = templateObj if (templateObj.type !== LIST) { children = templateObj.children.map(child => generateObjectFromTemplate(child, parentId) ) } return { ...templateObj, id: getListItemChildId(parentId, templateObj.id), attributes: { ...templateObj.attributes, name: `${templateObj.attributes.name} - inside ${parentId}`, }, children, } } export const getListItemObjectId = ( listObjectId: string, itemId?: number ): string => { if (itemId !== undefined) { return `${listObjectId} - ${itemId}` } return listObjectId } export const addListItems = ( obj: RunnerPartialObject, dataMap: Record ): void => { const listItemIds = dataMap[obj.id]?.listItems if (obj.type !== LIST || !listItemIds?.length) { return } const itemTemplate = buildListItemTemplate(obj) const listItems = listItemIds.map(itemId => createListItem(itemTemplate, itemId) ) obj.children = listItems } export const saveMissingDataToMap = ( obj: RunnerPartialObject, dataMap: Record ) => { if (!dataMap[obj.id]) { dataMap[obj.id] = createDataNode(obj.id, obj) } } export const savePushNodeToDataMap = ( pushNode: PushNode, dataMap: Record ): void => { const dataNode = dataMap[pushNode.objectId] dataNode.pushNode = pushNode pushNode.dataNode = dataNode } export const addParent = (pushNode: PushNode): void => { pushNode.children.forEach(child => { child.parent = pushNode }) } export const isNodeVisible = ( pushNode: PushNode, dataMap: Record ): boolean => { if (!pushNode.dataNode) { console.error({ pushNode }) throw new Error( `isNodeVisible -> pushNode ${pushNode.objectId} has no dataNode` ) } return pushNode.dataNode.visibleOnDevice } // This reduces the height of nodes hidden by conditional visibility to 0 and pulls components beneath it upward to fill in the gap const applyConditionalVisibility = (pushNode: PushNode): void => { if (!pushNode.dataNode) { console.error({ pushNode }) throw new Error( `applyConditionalVisibility -> pushNode ${pushNode.objectId} has no dataNode` ) } const { visible, visibleOnDevice, hiddenHeight } = pushNode.dataNode if (!visible && visibleOnDevice) { let givenYOffset = 0 if (pushNode.pushedBy.length > 0) { givenYOffset = pushNode.pushedBy.reduce( (currentValue, node) => Math.max(currentValue, node.yOffset), Number.MIN_SAFE_INTEGER ) } pushNode.yOffset = givenYOffset - pushNode.originalHeight pushNode.dataNode.hiddenHeight = pushNode.dataNode.height || hiddenHeight pushNode.dataNode.height = 0 } if (hiddenHeight && visible) { pushNode.dataNode.height = hiddenHeight || pushNode.originalHeight pushNode.yOffset += pushNode.originalHeight pushNode.dataNode.hiddenHeight = undefined } } export const applyHeightChanges = ( pushNode: PushNode, dataMap: Record ): void => { const { dataNode } = pushNode if (!dataNode) { console.error({ pushNode }) throw new Error( ` applyHeightChanges -> pushNode ${pushNode.objectId} has no dataNode` ) } makePush(pushNode) if (pushNode.parent) { calculateHeightFromChildren(pushNode.parent) } } export type Device = 'mobile' | 'tablet' | 'desktop' export const getDeviceProperties = ( obj: RunnerPartialObject, device?: Device ): RunnerPartialObject => { if (device && obj[device]) { return { ...obj, ...obj[device], attributes: { ...obj.attributes, ...obj[device].attributes, }, } } return obj } export const buildPushTree = ( body: RunnerPartialObject[], dataMap: Record, device?: Device ): Record => { const deepClonedBody = _.cloneDeep(body) // Step0 - getProperDeviceProperties const clonedBody = deepMap(deepClonedBody, obj => getDeviceProperties(obj, device) ) // Step1 - generate list items traverse(clonedBody, obj => addListItems(obj, dataMap)) // Step2 - Save missing data to datamap const newDataMap = _.cloneDeep(dataMap) traverse(clonedBody, obj => saveMissingDataToMap(obj, newDataMap)) // Step3 - Generate pushNodes const pushNodes = deepMap(clonedBody, createPushNode) // Step4 - Save Push Nodes in datamap traverse(pushNodes, node => savePushNodeToDataMap(node, newDataMap)) // Step5 - remove non-visible nodes const filteredNodes = deepFilter(pushNodes, node => isNodeVisible(node, dataMap) ) // Step6 - Fill in gaps left by conditional visibility traverse(pushNodes, node => applyConditionalVisibility(node)) // Step7 - Save Push Nodes in datamap traverse(filteredNodes, node => savePushNodeToDataMap(node, newDataMap)) // Step8 - Add Parents to pushNodes traverse(filteredNodes, addParent) // Step9 - Generate Push Relations traverse(filteredNodes, calculatePushRelations) // Step10 - Apply pushes, recalculate parents traverseLeafsFirst(filteredNodes, node => applyHeightChanges(node, dataMap)) return newDataMap } export default buildPushTree