/** @module store */
import { observer, inject, Provider } from 'mobx-react';
import AppTree from '../AppTree';
import { rmlOpts, i18n, ajax, fetchTree as utilFetchTree, applyNodeDefaults, ICON_OBJ_FOLDER_CLOSED,
ICON_OBJ_FOLDER_OPEN, ICON_OBJ_FOLDER_ROOT, ICON_OBJ_FOLDER_COLLECTION, ICON_OBJ_FOLDER_GALLERY,
humanFileSize, secondsFormat } from '../util';
import { Icon } from 'react-aiot';
import { types, flow, resolvePath, onPatch } from "mobx-state-tree";
import TreeNode from './TreeNode';
import Upload from './Upload';
/**
* The main Mobx State Tree store for the RML application. It holds a static tree and
* the fetched tree from the server. The properties are read-only.
*
* @class Store
* @property {int} [rootId=rmlOpts.rootId] The root folder id
* @property {module:store/TreeNode~TreeNode[]} staticTree The static tree
* @property {module:store/TreeNode~TreeNode[]} [tree] The tree
* @property {object} [refs] Refs to all available tree nodes
* @property {string|int} [selectedId=0] The selected id
* @property {mixed[]} [foldersNeedsRefresh] Node ids which needs to be refreshed when they gets queried
* @property {module:store/Upload~Upload[]} [uploading] The upload queue
* @property {int} [uploadTotalLoaded=0] The upload total loaded
* @property {int} [uploadTotalSize=0] The upload total size
* @property {object} [sortables] Available sortables for the order menu
* @property {int} [uploadTotalBytesPerSec=0] The uploader bytes per second
* @property {module:store/TreeNode~TreeNode} [selected] The selected tree node
* @property {module:store/Upload~Upload} [currentUpload] The current upload file
* @property {string} [uploadTotalRemainTime] The current upload remaining time in human readable form
* @property {string} [readableUploadTotalLoaded] The uploader total loaded in human readable form
* @property {string} [readableUploadTotalSize] The uploader total size in human readable form
* @property {string} [readableUploadTotalBytesPerSec] The uploader bytes per second in human readable form
*/
const Store = types.model('RMLStore', {
rootId: +rmlOpts.rootId,
staticTree: types.array(TreeNode),
tree: types.optional(types.array(TreeNode), []),
refs: types.optional(types.map(types.reference(TreeNode)), {}),
selectedId: types.optional(types.union(types.string, types.number), 0), // Do not fill manually, it is filled in afterCreated through onPatch
foldersNeedsRefresh: types.optional(types.array(types.union(types.string, types.number)), []),
uploading: types.optional(types.array(Upload), []),
uploadTotalLoaded: types.optional(types.number, 0),
uploadTotalSize: types.optional(types.number, 0),
sortables: types.optional(types.frozen),
uploadTotalBytesPerSec: types.optional(types.number, 0)
}).views(self => ({
/**
* Get tree item by id.
*
* @param {string|int} id
* @param {boolean} [exlucdeStatic=true]
* @returns {module:store/TreeNode~TreeNode} Tree node
* @memberof module:store~Store
* @instance
*/
getTreeItemById(id, excludeStatic = true) {
const useId = parseInt(id, 10),
ref = self.refs.get(isNaN(useId) ? id : useId);
return excludeStatic && ref && self.staticTree.indexOf(ref) > -1 ? undefined : ref;
},
get selected() {
return self.getTreeItemById(self.selectedId, false);
},
get currentUpload() {
return self.uploading.length ? self.uploading[0] : undefined;
},
get uploadTotalRemainTime() {
if (self.uploadTotalBytesPerSec > 0) {
const remainTime = Math.floor((self.uploadTotalSize - self.uploadTotalLoaded) / self.uploadTotalBytesPerSec);
return secondsFormat(remainTime);
}else{
return '00:00:00';
}
},
get readableUploadTotalLoaded() {
return humanFileSize(self.uploadTotalLoaded);
},
get readableUploadTotalSize() {
return humanFileSize(self.uploadTotalSize);
},
get readableUploadTotalBytesPerSec() {
return humanFileSize(self.uploadTotalBytesPerSec);
}
})).actions(self => ({
/**
* The model is created so watch for specific properties. For example set
* the selected property.
*
* @memberof module:store~Store
* @private
* @instance
*/
afterCreate() {
onPatch(self, ({ op, path, value }) => {
// A new selected item is setted
if ((path.startsWith('/tree/') || path.startsWith('/staticTree/')) && path.endsWith('/selected') && value === true) {
const currentSelected = self.selected, obj = resolvePath(self, path.slice(0, path.length - 9));
currentSelected && currentSelected.id !== obj.id && currentSelected.setter(node => { node.selected = false; });
self._setSelectedIdFromPath(obj);
}
});
// Initially load sortables
!self.sortables && self.fetchSortables();
},
_setSelectedIdFromPath(obj) {
self.selectedId = obj.id;
},
/**
* A new tree node is created (static or normal tree).
*
* @memberof module:store~Store
* @instance
* @private
*/
_afterAttachTreeNode(instance) {
self.refs.set(instance.id, instance);
},
/**
* An instance is destroyed.
*
* @private
*/
_beforeDestroyTreeNode(instance) {
self.refs.delete(instance.id);
},
/**
* Update this node attributes.
*
* @param {function} callback The callback with one argument (node draft)
* @memberof module:store~Store
* @instance
*/
setter(callback) {
callback(self);
},
/**
* Set the tree.
*
* @param {object} tree The object representing a tree
* @param {boolean} [isStatic=false]
* @memberof module:store~Store
* @instance
*/
setTree(tree, isStatic = false) {
if (isStatic) {
self.staticTree = tree;
}else{
self.tree = tree;
}
},
/**
* Set upload total stats.
*
* @memberof module:store~Store
* @instance
*/
setUploadTotal({ loaded, size, bytesPerSec }) {
self.uploadTotalLoaded = loaded;
self.uploadTotalSize = size;
self.uploadTotalBytesPerSec = bytesPerSec;
},
/**
* Add an uploading file.
*
* @param {object} object The object to push
* @returns {object} The upload instance
* @memberof module:store~Store
* @instance
*/
addUploading(upload) {
self.uploading.push(upload);
return self.uploading[self.uploading.length - 1];
},
/**
* Register a folder that it needs refresh.
*
* @memberof module:store~Store
* @instance
*/
addFoldersNeedsRefresh(id) {
self.foldersNeedsRefresh.indexOf(id) === -1 && self.foldersNeedsRefresh.push(id);
},
/**
* Register a folder that it needs refresh.
*
* @memberof module:store~Store
* @instance
*/
removeFoldersNeedsRefresh(id) {
const idx = self.foldersNeedsRefresh.indexOf(id);
idx > -1 && self.foldersNeedsRefresh.splice(idx, 1);
},
/**
* Remove an uploading file from queue.
*
* @param {string} cid The cid
* @returns {object} A copy of the original object
* @memberof module:store~Store
* @instance
*/
removeUploading(cid) {
const found = self.uploading.filter(u => u.cid === cid),
result = !!found.length && found[0].toJSON();
found.length && self.uploading.splice(self.uploading.indexOf(found[0]), 1);
return result;
},
/**
* Handle sort mechanism.
*
* @returns {boolean}
* @throws Error
* @memberof module:store~Store
* @instance
*/
handleSort: flow(function*({ id, oldIndex, newIndex, parentFromId, parentToId, nextId, request = true }) {
const { tree, rootId } = self;
// Find parent trees with children
let treeItem;
if (parentFromId === rootId) {
treeItem = tree[oldIndex].toJSON();
tree.splice(oldIndex, 1);
}else{
self.getTreeItemById(parentFromId).setter(node => {
treeItem = node.childNodes[oldIndex].toJSON();
node.childNodes.splice(oldIndex, 1);
}, true);
}
// Find destination tree
if (parentToId === rootId) {
tree.splice(newIndex, 0, treeItem);
}else{
self.getTreeItemById(parentToId).setter(node => {
node.childNodes.splice(newIndex, 0, treeItem);
}, true);
}
if (!request) {
return true;
}
// Request
try {
yield ajax('hierarchy/' + id, {
method: 'PUT',
data: {
parent: parentToId,
nextId: nextId === 0 ? false : nextId
}
});
return true;
}catch (e) {
yield store.handleSort({
id: self.rootId,
oldIndex: newIndex,
newIndex: oldIndex,
parentFromId: parentToId,
parentToId: parentFromId,
request: false
});
throw e;
}
}),
/**
* Fetch the folder tree.
*
* @returns {object[]} Tree
* @memberof module:store~Store
* @instance
* @async
*/
fetchTree: flow(function*(setSelectedId) {
const result = { tree, cntRoot, cntAll, slugs } = yield utilFetchTree();
self.setTree(tree);
typeof setSelectedId !== 'undefined' && self.getTreeItemById(setSelectedId, false).setter(node => (node.selected = true));
self.getTreeItemById('all', false).setter(node => (node.count = cntAll));
self.getTreeItemById(self.rootId, false).setter(node => (node.count = cntRoot));
return result;
}),
/**
* Update the folder count. If you pass no argument the folder count is
* requested from server.
*
* @param {object} counts Key value map of folder and count
* @returns {object<string|int,int>} Count map
* @memberof module:store~Store
* @instance
* @async
*/
fetchCounts: flow(function*(counts) {
if (counts) {
Object.keys(counts).forEach(k => {
const ref = self.getTreeItemById(k, false);
ref && (ref.count = counts[k]);
});
return counts;
}
return yield self.fetchCounts(yield ajax('folders/content/counts'));
}),
/**
* Fetch sortables.
*
* @memberof module:store~Store
* @instance
* @async
*/
fetchSortables: flow(function*() {
self.sortables = yield ajax('folders/content/sortables');
}),
/**
* Create a new tree node.
*
* @param {string} name The name of the new folder
* @param {object} obj The object representing the folder
* @param {string|int} obj.parent
* @param {int} obj.typeInt
* @param {function} [beforeAttach] Callback executed before attaching the new object to the tree
* @returns {object} The tree node (no mobx model)
* @memberof module:store~Store
* @instance
* @async
*/
persist: flow(function*(name, { parent, typeInt }, beforeAttach) {
const newObj = applyNodeDefaults([ yield ajax('folders', {
method: 'POST',
data: {
name,
parent,
type: typeInt
}
}) ])[0];
// Add to tree
beforeAttach && beforeAttach(newObj);
if (parent === self.rootId) {
self.tree.push(newObj);
}else{
self.getTreeItemById(parent).setter(node => {
node.childNodes.push(newObj);
}, true);
}
return newObj;
})
}));
/**
* Main store instance.
*/
const store = Store.create({
staticTree: [{
id: 'all',
title: <span>{ i18n('allPosts') }</span>,
icon: <Icon type="copy" />,
count: rmlOpts.allPostCnt
}, {
id: +rmlOpts.rootId,
title: i18n('unorganized'),
icon: ICON_OBJ_FOLDER_ROOT,
count: 0,
properties: {
type: 4
}
}]
});
/**
* A single instance of store.
*/
export default store;
/**
* An AppTree implementation with store provided. This means you have no longer
* implement the Provider of mobx here.
*
* @returns {React.Element}
*/
export const StoredAppTree = ({ children, ...rest }) => (<Provider store={ store }>
<AppTree {...rest}>{ children }</AppTree>
</Provider>);
/**
* Import general store to ReactJS component.
*/
export function injectAndObserve(fn, store = 'store') {
return inject(store)(observer(fn));
}
export { TreeNode, Upload };