@onehat/data
Version:
JS data modeling package with adapters for many storage mediums.
451 lines (371 loc) • 10 kB
JavaScript
/** @module Repository */
import Repository from './Repository.js'; // so we can use static methods
import OneBuildRepository from './OneBuild.js';
import _ from 'lodash';
/**
* This class is used for OneBuild Trees
* that contain multiple node types.
*
* @extends TreeRepository
*/
class TreeRepository extends OneBuildRepository {
constructor(config = {}) {
super(...arguments);
const defaults = {
isTree: true,
rootNodeType: this.getModel(), // e.g. 'Fleets'
api: {
getNodes: 'getNodes',
moveNode: 'moveNode',
searchNodes: 'searchNodes',
},
editableNodeTypes: [
this.getModel(),
],
};
_.merge(this, defaults, config);
}
async initialize() {
this.registerEvents([
'loadRootNodes',
]);
await super.initialize();
}
getModelFromTreeNode(treeNode) {
return treeNode.nodeType || this.rootNodeType;
}
/**
* Loads the root nodes of this tree.
*/
loadRootNodes(depth) {
this.ensureTree();
if (this.isDestroyed) {
this.throwError('this.setRootNode is no longer valid. Repository has been destroyed.');
return;
}
if (!this.isOnline) {
this.throwError('Offline');
return;
}
this.emit('beforeLoad'); // TODO: canceling beforeLoad will cancel the load operation
this.markLoading();
const
data = _.merge({ depth }, this._baseParams, this._params),
url = this.rootNodeType + '/' + this.api.getNodes;
if (this.debugMode) {
console.log('loadRootNodes', data);
}
return this._send('POST', url, data)
.then((result) => {
if (this.debugMode) {
console.log('Response for loadRootNodes', result);
}
if (this.isDestroyed) {
// If this repository gets destroyed before it has a chance
// to process the Ajax request, just ignore the response.
return;
}
const {
root,
success,
total,
message
} = this._processServerResponse(result);
if (!success) {
this.throwError(message);
return;
}
this._destroyEntities();
// Set the current entities
const oThis = this;
this.entities = _.map(root, (data) => {
const entity = Repository._createEntity(oThis.schema, data, this, true);
oThis._relayEntityEvents(entity);
return entity;
});
this.assembleTreeNodes();
// Set the total records that pass filter
this.total = total;
this._setPaginationVars();
this.areRootNodesLoaded = true;
// Don't emit events for root nodes...
this.rehash();
this.emit('loadRootNodes', this);
// this.emit('changeData', this.entities);
return this.getBy((entity) => {
return entity.isRoot;
});
})
.finally(() => {
this.markLoading(false);
});
}
/**
* Loads (or reloads) the supplied treeNode
*/
loadNode(treeNode, depth = 1) {
this.ensureTree();
if (this.isDestroyed) {
this.throwError('this.loadNode is no longer valid. Repository has been destroyed.');
return;
}
if (!this.isOnline) {
this.throwError('Offline');
return;
}
// If children already exist, remove them from the repository
// This way, we can reload just a portion of the tree
if (!_.isEmpty(treeNode.children)) {
_.each(treeNode.children, (child) => {
treeNode.repository.removeTreeNode(child);
});
treeNode.children = [];
}
this.markLoading();
const
data = _.merge({ depth, nodeId: treeNode.id, }, this._baseParams, this._params),
url = treeNode.getModel() + '/' + this.api.getNodes;
if (this.debugMode) {
console.log('loadNode', data);
}
return this._send('POST', url, data)
.then((result) => {
if (this.debugMode) {
console.log('Response for loadNode', result);
}
if (this.isDestroyed) {
// If this repository gets destroyed before it has a chance
// to process the Ajax request, just ignore the response.
return;
}
const {
root,
success,
total,
message
} = this._processServerResponse(result);
if (!success) {
this.throwError(message);
return;
}
// Set the current entities
const children = [];
_.each(root, (data) => {
if (data.id === treeNode.id) {
// This is the node we're loading, so update it directly
treeNode.loadOriginalData(data);
return null;
}
const entity = Repository._createEntity(this.schema, data, this, true);
this._relayEntityEvents(entity);
children.push(entity);
});
if (children.length) {
this.entities = this.entities.concat(children);
this.assembleTreeNodes();
this._setPaginationVars();
}
this.rehash();
// this.emit('changeData', this.entities);
this.emit('load', this);
return treeNode;
})
.finally(() => {
this.markLoading(false);
});
}
/**
* alias for backward compatibility
*/
loadChildNodes(treeNode, depth = 1) {
return this.loadNode(treeNode, depth);
}
/**
* Override the AjaxRepository to we can reload a treeNode if needed
*/
reloadEntity(entity, callback = null) {
if (!entity.isTree) {
return super.reloadEntity(entity, callback);
}
return this.loadNode(entity, 1);
}
/**
* Searches all nodes for the supplied text.
* This basically takes the search query and returns whatever the server sends
*/
searchNodes(q) {
this.ensureTree();
if (this.isDestroyed) {
this.throwError('this.searchNodes is no longer valid. Repository has been destroyed.');
return;
}
if (!this.isOnline) {
this.throwError('Offline');
return;
}
const
data = _.merge({ q, }, this._baseParams, this._params),
url = this.rootNodeType + '/' + this.api.searchNodes;
if (this.debugMode) {
console.log('searchNodes', data);
}
return this._send('POST', url, data)
.then((result) => {
if (this.debugMode) {
console.log('Response for searchNodes', result);
}
if (this.isDestroyed) {
// If this repository gets destroyed before it has a chance
// to process the Ajax request, just ignore the response.
return;
}
const {
root,
success,
total,
message
} = this._processServerResponse(result);
if (!success) {
this.throwError(message);
return;
}
return root;
})
.finally(() => {
this.markLoading(false);
});
}
/**
* Moves the supplied treeNode to a new position on the tree
* @returns id of common ancestor node
*/
moveTreeNode(treeNode, newParentId) {
this.ensureTree();
if (this.isDestroyed) {
this.throwError('this.moveTreeNode is no longer valid. Repository has been destroyed.');
return;
}
if (!this.isOnline) {
this.throwError('Offline');
return;
}
const
oldParentId = treeNode.parent?.id,
data = _.merge({ nodeId: treeNode.id, parentId: newParentId, }, this._baseParams, this._params),
url = this.rootNodeType + '/' + this.api.moveNode;
// NOTE: The rootNodeType controller needs to know about all the possible nodeTypes,
// so any particular node can move all around the tree.
if (this.debugMode) {
console.log('moveTreeNode', data);
}
return this._send('POST', url, data)
.then((result) => {
if (this.debugMode) {
console.log('Response for searchNodes', result);
}
if (this.isDestroyed) {
// If this repository gets destroyed before it has a chance
// to process the Ajax request, just ignore the response.
return;
}
const {
root: {
commonAncestorId,
oldParent,
newParent,
node,
},
success,
total,
message
} = this._processServerResponse(result);
if (!success) {
this.throwError(message);
return;
}
// move it from oldParent.children to newParent.children
const
oldParentRecord = this.getById(oldParentId),
newParentRecord = this.getById(newParentId);
oldParentRecord?.loadOriginalData(oldParent);
newParentRecord.loadOriginalData(newParent);
treeNode.loadOriginalData(node);
this.assembleTreeNodes();
return commonAncestorId;
})
.finally(() => {
this.markLoading(false);
});
}
/**
* Gets the root TreeNodes
*/
getRootNodes() {
this.ensureTree();
if (this.isDestroyed) {
this.throwError('this.loadRootNodes is no longer valid. Repository has been destroyed.');
return;
}
// Look through all entities and pull out the root nodes.
// Subclasses of Repository will override this method to get root nodes from server
const entities = _.filter(this.getEntities(), (entity) => {
return entity.isRoot;
})
return entities;
}
/**
* Populates the TreeNodes with .parent and .children references
*/
assembleTreeNodes() {
this.ensureTree();
if (this.isDestroyed) {
this.throwError('this.assembleTreeNodes is no longer valid. Repository has been destroyed.');
return;
}
const treeNodes = this.getEntities();
// Reset all parent/child relationships
_.each(treeNodes, (treeNode) => {
treeNode.parent = null;
treeNode.children = [];
});
// Rebuild all parent/child relationships
const oThis = this;
_.each(treeNodes, (treeNode) => {
const parent = oThis.getById(treeNode.parentId);
if (parent) {
treeNode.parent = parent;
parent.children.push(treeNode);
}
});
}
/**
* Removes the treeNode and all of its children from repository
* without deleting anything on the server
*/
removeTreeNode(treeNode) {
if (!_.isEmpty(treeNode.children)) {
const children = treeNode.children;
treeNode.parent = null;
treeNode.children = [];
const oThis = this;
_.each(children, (child) => {
oThis.removeTreeNode(child);
});
}
this.removeEntity(treeNode);
}
/**
* Helper to make sure this Repository is a tree
* @private
*/
async ensureTree() {
if (!this.isTree) {
this.throwError('This Repository is not a tree!');
return false;
}
return true;
}
}
TreeRepository.className = 'Tree';
TreeRepository.type = 'tree';
export default TreeRepository;