Source: public/src/AppTree.js

/** @module AppTree */
import React from 'react';
import ReactDOM from 'react-dom';
import { DashIcon } from './components';
import renderOrderMenu from './others/renderOrderMenu';
import AIOTree, { getTreeParentById } from 'react-aiot';
import { message, Icon, Popconfirm } from 'react-aiot';
import { hooks, rmlOpts, i18n, urlParam, addUrlParam, ICON_OBJ_FOLDER_CLOSED,
    ICON_OBJ_FOLDER_OPEN, ICON_OBJ_FOLDER_ROOT, ICON_OBJ_FOLDER_COLLECTION, ICON_OBJ_FOLDER_GALLERY } from './util';
import { draggable, droppable } from './util/dragdrop';
import produce from 'immer';
import $ from 'jquery';
import { FILTER_SELECTOR } from './others/filter';
import createLockedToolTipText from './hooks/permissions';
import { inject, observer, Observer } from 'mobx-react';
import { toggleSortable, orderUrl } from './hooks/sortable';
import MetaBox from './components/MetaBox';

/**
 * The latest queried folder.
 * 
 * @type object
 */
export let latestQueriedFolder = { node: null };

message.config({ top: 50 });

/**
 * The application tree handler for Real Media Library.
 * 
 * @param {string} id The HTML id (needed to localStorage support)
 * @param {object} [attachmentsBrowser] The attachments browser (for media grid view)
 * @param {boolean} [isModal=false] If true the given app tree is a modal dialog
 * @param {module:AppTree~AppTree~init} [init]
 * @see module:store.StoredAppTree
 * @see module:react-aiot~Tree
 * @extends React.Component
 */
@inject('store')
@observer
class AppTree extends React.Component {
    /**
     * Initialize properties and state for AIOTree component.
     * Also handles the responsiveness.
     */
    constructor(props) {
        super(props);
        
        // Add respnsive handler for non-modal views
        !props.isModal && $(window).resize(this.handleWindowResize);
        const isMobile = this._isMobile();
        
        // State refs (see https://github.com/reactjs/redux/issues/1793) and #resolveStateRefs
        this.stateRefs = {
            keysCreatable: 'icon,iconActive,toolTipTitle,toolTipText,onClick,label'.split(','),
            keysToolbar: 'content,toolTipTitle,toolTipText,onClick,onCancel,onSave,modifier,label,save,menu'.split(','),
            
            // Icons
            ICON_OBJ_FOLDER_CLOSED,
            ICON_OBJ_FOLDER_OPEN,
            ICON_OBJ_FOLDER_ROOT,
            ICON_OBJ_FOLDER_COLLECTION,
            ICON_OBJ_FOLDER_GALLERY,
            ICON_SETTINGS: <Icon type="setting" />,
            ICON_LOCKED: <Icon type="lock" />,
            ICON_ORDER: <DashIcon name="move" />,
            ICON_RELOAD: <Icon type="reload" />,
            ICON_RENAME: <Icon type="edit" />,
            ICON_TRASH: <Icon type="delete" />,
            ICON_SORT: <DashIcon name="sort" />,
            ICON_SAVE: <Icon type="save" />,
            ICON_ELLIPSIS: <Icon type="ellipsis" />,
            
            // Creatable
            handleCreatableClickBackButton: () => this.handleCreatableClick(),
            handleCreatableClickFolder: () => this.handleCreatableClick('folder', 0),
            handleCreatableClickCollection: () => this.handleCreatableClick('collection', 1),
            handleCreatableClickGallery: () => this.handleCreatableClick('gallery', 2),
            
            // Toolbar buttons
            renderOrderMenu: renderOrderMenu.bind(this),
            handleOrderClick: this.handleOrderClick,
            handleOrderCancel: this.handleOrderCancel,
            handleReload: this.handleReload,
            handleRenameClick: this.handleRenameClick,
            handleRenameCancel: this.handleRenameCancel,
            handleTrashModifier: body => {
                const node = this.getTreeItemById();
                return node ? <Popconfirm placement="bottom" onConfirm={ this.handleTrash }
                    title={i18n('deleteConfirm', { name: node.title }, 'maxWidth')}
                    okText={i18n('ok')} cancelText={i18n('cancel')}>{ body }</Popconfirm> : body;
            },
            handleSortClick: () => this._handleSortNode('sort'),
            handleSortCancel: () => this._handleSortNode(),
            handleDetailsClick: () => this._handleDetails('details'),
            handleDetailsCancel: () => this._handleDetails(),
            handleDetailsSave: () => this._handleDetails('save'),
            handleUserSettingsClick: () => this._handleDetails('usersettings'),
            handleUserSettingsCancel: () => this._handleDetails(),
            handleUserSettingsSave: () => this._handleDetails('save')
        };
        
        // Determine selected id and fetch tree
        let selectedId = urlParam('rml_folder');

        this.attachmentsBrowser = props.attachmentsBrowser;
        this.state = {
            // Custom
            currentFolderRestrictions: [],
            isModal: props.isModal,
            isMoveable: true,
            isWPAttachmentsSortMode: false, // See modal.js
            initialSelectedId: !selectedId || selectedId === 'all' ? 'all' : +selectedId,
            metaBoxId: false,
            metaBoxErrors: false,
            
            // Creatables
            availableCreatables: 'folder,collection,gallery'.split(','),
            selectedCreatableType: undefined, // The selected folder type
            creatable_folder: {
                icon: 'ICON_OBJ_FOLDER_CLOSED',
	            iconActive: 'ICON_OBJ_FOLDER_OPEN',
	            visibleInFolderType: [undefined, 0],
	            cssClasses: 'page-title-action add-new-h2',
	            toolTipTitle: 'i18n.creatable0ToolTipTitle',
	            toolTipText: 'i18n.creatable0ToolTipText',
	            label: '+',
	            onClick: 'handleCreatableClickFolder'
            },
            creatable_collection: {
	            icon: 'ICON_OBJ_FOLDER_COLLECTION',
	            visibleInFolderType: [undefined, 0, 1],
	            cssClasses: 'page-title-action add-new-h2',
	            toolTipTitle: 'i18n.creatable1ToolTipTitle',
	            toolTipText: 'i18n.creatable1ToolTipText',
	            label: '+',
	            onClick: 'handleCreatableClickCollection'
	        },
	        creatable_gallery: {
	            icon: 'ICON_OBJ_FOLDER_GALLERY',
	            visibleInFolderType: [1],
	            visible: false,
	            cssClasses: 'page-title-action add-new-h2',
	            toolTipTitle: 'i18n.creatable2ToolTipTitle',
	            toolTipText: 'i18n.creatable2ToolTipText',
	            label: '+',
	            onClick: 'handleCreatableClickGallery'
	        },
            creatableBackButton: {
	            cssClasses: 'page-title-action add-new-h2',
	            label: 'i18n.cancel',
	            onClick: 'handleCreatableClickBackButton'
            },
            
            // Toolbar buttons
            availableToolbarButtons: 'locked,usersettings,order,reload,rename,trash,sort,details'.split(','),
            toolbar_usersettings: {
                content: 'ICON_SETTINGS',
                visible: !!+rmlOpts.userSettings,
                toolTipTitle: 'i18n.userSettingsToolTipTitle',
                toolTipText: 'i18n.userSettingsToolTipText',
                onClick: 'handleUserSettingsClick',
                onCancel: 'handleUserSettingsCancel',
                onSave: 'handleUserSettingsSave'
            },
            toolbar_locked: {
                content: 'ICON_LOCKED',
                visible: false,
                toolTipTitle: 'i18n.lockedToolTipTitle',
                toolTipText: '' // Lazy
            },
            toolbar_order: {
                content: 'ICON_ORDER',
                toolTipTitle: 'i18n.orderToolTipTitle',
                toolTipText: 'i18n.orderToolTipText',
                onClick: 'handleOrderClick',
                onCancel: 'handleOrderCancel',
                menu: 'resolve.renderOrderMenu',
                toolTipPlacement: 'topLeft',
                dropdownPlacement: 'bottomLeft'
            },
            toolbar_reload: {
                content: 'ICON_RELOAD',
                toolTipTitle: 'i18n.refreshToolTipTitle',
                toolTipText: 'i18n.refreshToolTipText',
                onClick: 'handleReload'
            },
            toolbar_rename: {
                content: 'ICON_RENAME',
                toolTipTitle: 'i18n.renameToolTipTitle',
                toolTipText: 'i18n.renameToolTipText',
                onClick: 'handleRenameClick',
                onCancel: 'handleRenameCancel',
                disabled: true
            },
            toolbar_trash: {
                content: 'ICON_TRASH',
                toolTipTitle: 'i18n.trashToolTipTitle',
                toolTipText: 'i18n.trashToolTipText',
                modifier: 'handleTrashModifier',
                disabled: true
            },
            toolbar_sort: {
                content: 'ICON_SORT',
                toolTipTitle: 'i18n.sortToolTipTitle',
                toolTipText: 'i18n.sortToolTipText',
                onClick: 'handleSortClick',
                onCancel: 'handleSortCancel'
            },
            toolbar_details: {
                content: 'ICON_ELLIPSIS',
                disabled: true,
                toolTipTitle: 'i18n.detailsToolTipTitle',
                toolTipText: 'i18n.detailsToolTipText',
                onClick: 'handleDetailsClick',
                onCancel: 'handleDetailsCancel',
                onSave: 'handleDetailsSave'
            },
            toolbarBackButton: {
                label: 'i18n.cancel',
                save: 'i18n.save'
            },
            
            // AIO
            isResizable: !isMobile,
            isSticky: !isMobile,
            isStickyHeader: !isMobile,
            isFullWidth: isMobile,
            style: isMobile ? { marginLeft: 10 } : {},
            isSortable: true,
            isSortableDisabled: false,
            isTreeBusy: false,
            isBusyHeader: false,
            sortableDelay: 100,
            headerStickyAttr: {
                top: '#wpadminbar'
            },
            isCreatableLinkDisabled: false,
            toolbarActiveButton: undefined,
            isTreeLinkDisabled: false
        };
        
        // What happens if the attachments browser is available? We will add a reference to this React element
        this.attachmentsBrowser && (this.attachmentsBrowser.controller.$RmlAppTree = this);
        
        /**
         * Called on initialzation and allows you to modify the init state.
         * 
         * @callback module:AppTree~AppTree~init
         * @param {object} state The default state
         * @param {AppTree} tree The AppTree component instance
         * @returns {object} The new state
         */
        props.init && (this.state = props.init(this.state, this));
        
        /**
         * The React AppTree instance gets constructed and you can modify it here.
         * 
         * @event module:util/hooks#tree/init
         * @param {object} state
         * @param {object} props
         * @this module:AppTree~AppTree
         */
        hooks.call('tree/init', [this.state, props], this);
        this.initialSelectedId = this.state.initialSelectedId;
    }
    
    /**
     * Render AIO tree with tax switcher.
     */
    render() {
        const { staticTree, tree, selectedId } = this.props.store,
            { metaBoxId, metaBoxErrors, isBusyHeader } = this.state;
        return <AIOTree ref={ this.doRef } id={ this.props.id } rootId={ +rmlOpts.rootId }
            staticTree={ staticTree } selectedId={ selectedId } tree={ tree.length > 0 ? tree : [] }
            opposite={ document.getElementById('wpbody-content') } onSelect={ this.handleSelect }
            onRenameClose={ this.handleRenameClose } onAddClose={ this.handleAddClose }
            onNodeExpand={ () => setTimeout(() => droppable(this), 200) } renderItem={ this.onTreeNodeRender }
            onNodePressF2={ this.handleRenameClick } onSort={ this.handleSort } onResize={ this.handleResize }
            headline={ <span style={{ paddingRight: 5 }}>{ i18n('folders') }</span> }
            renameSaveText={ this.stateRefs.ICON_SAVE } renameAddText={ this.stateRefs.ICON_SAVE }
            noFoldersTitle={ i18n('noFoldersTitle') } noFoldersDescription={ i18n('noFoldersDescription') }
            noSearchResult={ i18n('noSearchResult') } innerClassName="wrap" theme="wordpress"
            creatable={ this.renderCreatables() } toolbar={ this.renderToolbarButtons() }
            { ...this.state }>
            { metaBoxId !== false && (<MetaBox patcher={ patcher => (this.metaboxPatcher = patcher) } busy={ isBusyHeader }
                errors={ metaBoxErrors } id={ metaBoxId } />) }
        </AIOTree>;
    }
    
    /**
     * @returns {object}
     */
    renderToolbarButtons = () => {
        const { availableToolbarButtons, toolbarBackButton } = this.state, toolbar = {
            buttons: { },
            backButton: this.resolveStateRefs(toolbarBackButton, 'keysToolbar')
        };
        for (let i = 0; i < availableToolbarButtons.length; i++) {
            toolbar.buttons[availableToolbarButtons[i]] = this.resolveStateRefs(this.state['toolbar_' + availableToolbarButtons[i]], 'keysToolbar');
        }
        return toolbar;
    }
    
    /**
     * @returns {object}
     */
    renderCreatables = () => {
        const { availableCreatables, creatableBackButton } = this.state, creatable = {
            buttons: { },
            backButton: this.resolveStateRefs(creatableBackButton, 'keysCreatable')
        };
        for (let i = 0; i < availableCreatables.length; i++) {
            creatable.buttons[availableCreatables[i]] = this.resolveStateRefs(this.state['creatable_' + availableCreatables[i]], 'keysCreatable');
        }
        return creatable;
    }
    
    /**
     * Iterates all available values in an object and resolve it with the available
     * this::stateRefs.
     * 
     * @returns {object}
     */
    resolveStateRefs(_obj, keys) {
        const obj = Object.assign({}, _obj);
        let value, newValue;
        for (let key in obj) {
            if (obj.hasOwnProperty(key) && (value = obj[key]) && this.stateRefs[keys].indexOf(key) > -1 &&
                typeof value === 'string' && (newValue = this.resolveStateRef(value))) {
                obj[key] = newValue;
            }
        }
        return obj;
    }
    
    /**
     * Resolve single state ref key.
     * 
     * @returns {object}
     */
    resolveStateRef(key) {
        if (typeof key !== 'string') {
            return;
        }
        if (key.indexOf('i18n.') === 0) {
            return i18n(key.substr(5));
        }else if (key.indexOf('resolve.') === 0) {
            return this.stateRefs[key.substr(8)]();
        }else if (this.stateRefs[key]) {
            return this.stateRefs[key];
        }
    }
    
    doRef = ref => this.ref = ref
    
    /**
     * Remove resize handler.
     */
    componentWillUnmount() {
        $(window).off('resize', this.handleWindowResize);
    }
    
    /**
     * Fetch initial tree.
     */
    componentWillMount() {
        console.log(this);
        this.fetchTree(this.initialSelectedId);
    }
    
    /**
     * Initiate draggable and droppable
     */
    componentDidMount() {
        draggable(this);
        droppable(this);
        this.handleResize();
        
        // If order should be enabled in list mode, then activate it now
        if (rmlOpts.listMode === 'list' && window.location.hash === '#order') {
            this.handleOrderClick();
            window.location.hash = '';
        }
    }
    
    /**
     * When the component updates the droppable zone is reinitialized.
     * Also the toolbar buttons gets disabled or enabled depending on selected node.
     */
    componentDidUpdate() {
        const { selectedCreatableType } = this.state,
            selected = this.getTreeItemById();
        if ((selected && selectedCreatableType !== selected.properties.type) || (!selected && selectedCreatableType !== undefined)) {
            this._updateCreatableButtons(selected ? selected.properties.type : undefined);
        }
        
        // Enable / Disable toolbar buttons
        this._updateToolbarButtons();
        
        // Enable locked toolbar item
        createLockedToolTipText(this);
        
        draggable(this);
        droppable(this);
    }
    
    /**
     * Return the backbone filter view for the given attachments browser.
     * 
     * @returns object
     */
    getBackboneFilter() {
        const { attachmentsBrowser } = this;
        return attachmentsBrowser && attachmentsBrowser.toolbar.get('rml_folder');
    }
    
    /**
     * Get the selected node id.
     * 
     * @returns {string|int}
     */
    getSelectedId() {
        return this.props.store.selectedId;
    }
    
    /**
     * Get tree item by id.
     * 
     * @param {string|int} [id=Current]
     * @param {boolean} [excludeStatic=true]
     * @returns {object} Tree node
     */
    getTreeItemById(id = this.getSelectedId(), excludeStatic = true) {
        return this.props.store.getTreeItemById(id, excludeStatic);
    }
    
    /**
     * Update a tree item by id.
     * 
     * @param {function|array} callback The callback with one argument (node draft) and should return the new node.
     * @param {string|int} [id=Current] The id which should be updated
     */
    updateTreeItemById(callback, id = this.getSelectedId()) {
        const node = this.props.store.getTreeItemById(id);
        node && node.setter(callback);
    }
    
    /**
     * Updates the create node. That's the node without id and the input field.
     * 
     * @param {function} callback The callback with one argument (node draft) and should return the new node.
     */
    async updateCreateNode(callback) {
        // Root update
        const createRoot = this.state.createRoot;
        createRoot && this.setState({
            createRoot: produce(createRoot, callback)
        });
        
        // Child node update
        const node = this.getTreeItemById();
        node && node.$create && this.updateTreeItemById(node => {
            node.$create = produce(node.$create, callback);
        });
    }
    
    /**
     * Handles the creatable click and creates a new node depending on the selected one.
     * 
     * @method
     */
    handleCreatableClick = (type, typeInt) => {
        let createRoot = undefined,
            $create = undefined;
        
        if (type) {
            // Activate create
            const creatable = this.state['creatable_' + type],
                newNode = {
                    $rename: true,
                    icon: this.resolveStateRef(creatable.icon),
                    iconActive: this.resolveStateRef(creatable.iconActive),
                    parent: +rmlOpts.rootId,
                    typeInt
                }, selectedId = this.getSelectedId();
            if (typeof selectedId !== 'number' || selectedId === +rmlOpts.rootId) {
                createRoot = newNode;
            }else{
                $create = newNode;
                newNode.parent = selectedId;
            }
        }
        
        this.setState({
            isTreeLinkDisabled: !!type,
            isCreatableLinkCancel: !!type,
            isToolbarActive: !type,
            createRoot
        });
        this.updateTreeItemById(node => { node.$create = $create });
    }
    
    /**
     * A node gets selected. Depending on the fast mode the page gets reloaded
     * or the wp list table gets reloaded.
     * 
     * @method
     */
    handleSelect = id => {
        // Do nothing when sort mode is active
        if (this.state.toolbarActiveButton === 'sort') {
            return;
        }
        
        const select = this.getTreeItemById(id, false),
            setter = (_id, $busy) => {
                latestQueriedFolder.node = select;
                latestQueriedFolder.node.setter(node => {
                    node.$busy = $busy;
                    node.selected = true;
                });
            };
        
        if (this.attachmentsBrowser) {
            !id && this.attachmentsBrowser.collection.props.set({ignore: (+ new Date())}); // Reload the view
            this._handleBackboneFilterSelection(select.id);
        }else{
            let href = window.location.href;
            urlParam('orderby') === 'rml' && (href = href.split('?')[0]);
            select.properties && select.contentCustomOrder === 1 &&
                (href = orderUrl(href));
            window.location.href = addUrlParam(href, 'rml_folder', select.id);
        }
        setter(select.id, !this.attachmentsBrowser);
    }
    
    /**
     * When resizing the container set ideal width for attachments.
     */
    handleResize = () => {
        const { attachmentsBrowser } = this;
        attachmentsBrowser && attachmentsBrowser.attachments.setColumns();
    }
    
    /**
     * Handle order click.
     * 
     * @method
     */
    handleOrderClick = () => {
        if (toggleSortable(this.getTreeItemById(), true, this.attachmentsBrowser)) {
            this.setState({
                isMoveable: false,
                toolbarActiveButton: 'order',
                toolbarBackButton: produce(this.state.toolbarBackButton, draft => {
                    draft.label = 'i18n.back';
                })
            });
        }
    }
    
    /**
     * Handle order cancel.
     * 
     * @method
     */
    handleOrderCancel = () => {
        toggleSortable(this.getTreeItemById(), false, this.attachmentsBrowser);
        this.setState({
            isMoveable: true,
            toolbarActiveButton: undefined,
            toolbarBackButton: produce(this.state.toolbarBackButton, draft => {
                draft.label = 'i18n.cancel';
            })
        });
    }
    
    /**
     * Handle rename click and enable the input field if necessery.
     * 
     * @method
     */
    handleRenameClick = () => this._handleRenameNode('rename', true, true, true)
    
    /**
     * Handle rename cancel.
     * 
     * @method
     */
    handleRenameCancel = () => this._handleRenameNode(undefined, false, false, undefined)
    
    /**
     * Handle rename close and depending on the save state create the new node.
     * 
     * @method
     */
    handleRenameClose = async (save, inputValue, { id }) => {
        if (save && inputValue.length) {
            const hide = message.loading(i18n('renameLoadingText', { name: inputValue }));
            try {
                const { name } = await this.props.store.getTreeItemById(id).setName(inputValue);
                message.success(i18n('renameSuccess', { name }));
                this.handleRenameCancel();
            }catch(e) {
                message.error(e.responseJSON.message);
            }finally{
                hide();
            }
        }else{
            this.handleRenameCancel();
        }
    }
    
    /**
     * Handle add close and remove the new node.
     * 
     * @method
     */
    handleAddClose = async (save, name, { parent, typeInt }) => {
        if (save) {
            this.updateCreateNode(node => { node.$busy = true; });
            const hide = message.loading(i18n('addLoadingText', { name }));
            
            try {
                const newObj = await this.props.store.persist(name, { parent, typeInt }, () => this.handleCreatableClick());
                message.success(i18n('addSuccess', { name }));
                
                // Modify all available attachments browsers filter
                let backboneFilter, lastSlugs;
                $(FILTER_SELECTOR).each(function() {
                    backboneFilter = $(this).data('backboneView');
                    if (backboneFilter) {
                        lastSlugs = backboneFilter.lastSlugs;
                        lastSlugs.names.push('(NEW) ' + name);
                        lastSlugs.slugs.push(newObj.id);
                        lastSlugs.types.push(typeInt);
                        backboneFilter.createFilters(lastSlugs);
                    }
                });
            }catch(e) {
                message.error(e.responseJSON.message);
                this.updateCreateNode(node => { node.$busy = false; });
            }finally{
                hide();
            }
        }else{
            this.handleCreatableClick();
        }
    }
    
    /**
     * Handle trashing of a category. If the category has subcategories the
     * trash is forbidden.
     * 
     * @method
     */
    handleTrash = async () => {
        const node = this.getTreeItemById();
        
        // Check if subdirectories
        if (node.childNodes.filter(node => node.$visible).length) {
            return message.error(i18n('deleteFailedSub', { name: node.title }));
        }
        
        const hide = message.loading(<span>Deleteing <strong>{ node.title }</strong>...</span>);
        try {
            await node.trash();
            message.success(i18n('deleteSuccess', { name: node.title }));
            
            // Select parent
            const parentId = getTreeParentById(node.id, this.props.store.tree);
            this.handleSelect(parentId === 0 ? +rmlOpts.rootId : parentId);
        }catch(e) {
            message.error(e.responseJSON.message);
        }finally{
            hide();
        }
    }
    
    /**
     * Handle categories sorting and update the tree so the changes are visible. If sorting
     * is cancelled the old tree gets restored.
     * 
     * @method
     */
    handleSort = async (props) => {
        this.setState({
            isSortableBusy: true,
            isToolbarBusy: true
        });
        
        const hide = message.loading(i18n('sortLoadingText')),
            { toolbarActiveButton } = this.state,
            { store } = this.props;
            
        try {
            await store.handleSort(props);
            message.success(i18n('sortedSuccess'));
        }catch (e) {
            message.error(e.responseJSON.message);
        }finally{
            hide();
            this._handleSortNode(toolbarActiveButton, false);
        }
    }
    
    /**
     * Handle responsiveness on window resize.
     * 
     * @method
     */
    handleWindowResize = () => {
        const isMobile = this._isMobile();
        this.setState({
            isSticky: !isMobile,
            isStickyHeader: !isMobile,
            isResizable: !isMobile,
            isFullWidth: isMobile,
            style: isMobile ? { marginLeft: 10 } : {}
        });
    }
    
    /**
     * Handle refesh of content.
     */
    handleReload = () => {
        this.handleSelect();
    }
    
    handleDestroy() {
        ReactDOM.unmountComponentAtNode(this.ref.container.parentNode);
    }
    
    /**
     * A node item should be an observer (mobx).
     */
    onTreeNodeRender = (createTreeNode, TreeNode, node) => {
        return <Observer key={ node.id }>{ () => createTreeNode(node) }</Observer>;
    }
    
    /**
     * Handle rename node states (helper).
     * 
     * @method
     */
    _handleRenameNode = (toolbarActiveButton, isCreatableLinkDisabled, isTreeLinkDisabled, nodeRename) => {
        this.setState({ // Make other nodes editable / not editable
            isCreatableLinkDisabled,
            isTreeLinkDisabled,
            toolbarActiveButton
        });
        this.updateTreeItemById(node => { // Make selected node editable / not editable
            node.$rename = nodeRename;
        });
    }
    
    /**
     * Checks if the current window size is mobile.
     * 
     * @returns {boolean}
     * @method
     */
    _isMobile = () => $(window).width() <= 700
    
    /**
     * Handle the sort node button.
     * 
     * @method
     */
    _handleSortNode = (toolbarActiveButton, isBusy) => {
        this.setState({
            isCreatableLinkDisabled: !!toolbarActiveButton,
            toolbarActiveButton,
            sortableDelay: toolbarActiveButton ? 0 : 100,
            toolbarBackButton: produce(this.state.toolbarBackButton, draft => {
                draft.label = 'i18n.' + (toolbarActiveButton ? 'back' : 'cancel');
            })
        });
        
        typeof isBusy === 'boolean' && this.setState({ isSortableBusy: isBusy });
        typeof isBusy === 'boolean' && this.setState({ isToolbarBusy: isBusy });
    }
    
    /**
     * Handle the details meta box.
     */
    _handleDetails = async action => {
        action !== 'save' && this.setState({
            toolbarActiveButton: action,
            metaBoxId: action ? (action === 'usersettings' ? action : this.props.store.selectedId) : false
        });
        
        if (action === 'save') {
            this.setState({ isBusyHeader: true, metaBoxErrors: [] });
            try {
                await this.metaboxPatcher();
                this._handleDetails();
            }catch ({ responseJSON: { message } }) {
                this.setState({ metaBoxErrors: message });
            }finally{
                this.setState({ isBusyHeader: false });
            }
        }
    }
    
    /**
     * Set the attachments browser location.
     * 
     * @param {int} [id=Current selected id] The id
     */
    _handleBackboneFilterSelection(id = this.getSelectedId()) {
        const attachmentsBrowser = this.attachmentsBrowser;
        if (attachmentsBrowser) {
            setTimeout(() => {
                const backboneFilter = this.getBackboneFilter();
                backboneFilter && backboneFilter.$el.val(id).change();
                
                // Reset bulk select in no-modal mode
                attachmentsBrowser.$el.parents('.media-modal').size() === 0 && attachmentsBrowser.controller.state().get('selection').reset();
                
                // Check if folder needs refresh
                const { store } = this.props;
                if (store.foldersNeedsRefresh.indexOf(id) > -1) {
                    store.removeFoldersNeedsRefresh(id);
                    this.handleReload();
                }
            }, 0);
        }
    }
    
    /**
     * Update the creatable buttons regarding the selected type.
     * 
     * @param {int} selectedCreatableType
     */
    _updateCreatableButtons(selectedCreatableType) {
        this.setState({ selectedCreatableType });
        
        this.state.availableCreatables.forEach(c => this.setState({
            ['creatable_' + c]: produce(this.state['creatable_' + c], v => {
                v.visible = v.visibleInFolderType.indexOf(selectedCreatableType) > -1;
            })
        }));
    }
    
    _updateToolbarButtons() {
        const { isWPAttachmentsSortMode, toolbar_order, toolbar_rename, toolbar_trash, toolbar_details } = this.state,
            selected = this.getTreeItemById(),
            disableIfStatic = !selected,
            restrictions = selected && selected.properties && selected.properties.restrictions || [];
            
        const disableOrder = disableIfStatic || isWPAttachmentsSortMode || (selected && selected.contentCustomOrder === 2);
        toolbar_order.disabled !== disableOrder && this.setState({
            toolbar_order: produce(toolbar_order, draft => {
                draft.disabled = disableOrder;
            })
        });
        
        const disableRename = disableIfStatic || restrictions.indexOf('ren') > -1;
        toolbar_rename.disabled !== disableRename && this.setState({
            toolbar_rename: produce(toolbar_rename, draft => {
                draft.disabled = disableRename;
            })
        });
        
        const disableTrash = disableIfStatic || restrictions.indexOf('del') > -1;
        toolbar_trash.disabled !== disableTrash && this.setState({
            toolbar_trash: produce(toolbar_trash, draft => {
                draft.disabled = disableTrash;
            })
        });
        
        toolbar_details.disabled !== disableIfStatic && this.setState({
            toolbar_details: produce(toolbar_details, draft => {
                draft.disabled = disableIfStatic;
            })
        });
    }
    
    /**
     * Fetch folder tree.
     */
    async fetchTree(setSelectedId) {
        this.setState({ isTreeBusy: true });
        const { slugs } = await this.props.store.fetchTree(setSelectedId);

        // Modify all available attachments browsers filter
        $(FILTER_SELECTOR).each(function() {
            const backboneFilter = $(this).data('backboneView');
            backboneFilter && backboneFilter.createFilters(slugs);
        });
        
        this._handleBackboneFilterSelection();
        
        // Modify this tree
        this.setState({ isTreeBusy: false });
        latestQueriedFolder.node = this.props.store.selected;
    }
    
    /**
     * 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
     */
    async fetchCounts(counts) {
        return await this.props.store.fetchCounts(counts);
    }
}

export default AppTree;