1
votes

I'm looking for a way to show document type alias or name of selected content in Multinode tree picker inside content tab.

This will tremendously help me to quickly identify type of content attached. Also hovering a content added in MNTP inside content shows numeric path which is not quite useful, is there a way to show path names instead of id's?

Attaching image for reference.

Kindly suggest.enter image description here

1

1 Answers

0
votes

The easiest way to do this is to simply modify the MNTP property editor and have it show the extra data. It is however not really recommended to modify these files since it will be reverted every time you upgrade your Umbraco site.

There is a bit of a hacky workaround to achieve what you want however;


Angular has something called interceptors, allowing you to intercept and modify requests being made. By registrering an interceptor you could intercept the requests to contentpicker.html and redirect it to a contentpicker.html file located somewhere else - this means you will be able to just copy this file somewhere else and not modify the one in the umbraco folder being overwritten by upgrades.

Unfortunately in this case it isn't enough to just override the view being sent to the browser, since the document type alias is not actually available to that view - so the view would never be able to show the alias even if we modified it.

To work around that, you could create a copy of the contentpicker.controller.js file and create a modified version of that to use in your custom contentpicker.html view. This modified ContentPickerController would make sure to include the contentTypeAlias in the model being used in rendering the view.

All of this can be wrapped up as a "package" in the App_Plugins folder and it will be nicely separated away from being overwritten when you upgrade Umbraco, while being automatically loaded via the package.manifest file. The only caveat is that in case something is updated in the content picker code, you would have to manually merge those updates over into your custom content picker files - fortunately the content picker is rarely updated.

Place the following files in App_Plugins/CustomContentPicker/ folder and you should have what you want without actually modifying the core code:

package.manifest

{
  "javascript": [
    "~/App_Plugins/CustomContentPicker/customcontentpicker.controller.js"
  ]
}

customcontentpicker.html

<div ng-controller="Umbraco.PropertyEditors.CustomContentPickerController" class="umb-editor umb-contentpicker">

    <ng-form name="contentPickerForm">

        <ul class="unstyled list-icons"
            ui-sortable
            ng-model="renderModel">
            <li ng-repeat="node in renderModel" ng-attr-title="{{model.config.showPathOnHover && 'Path: ' + node.path || undefined}}">
                <i class="icon icon-navigation handle"></i>
                <a href="#" prevent-default ng-click="remove($index)">
                    <i class="icon icon-delete red hover-show"></i>
                    <i class="{{node.icon}} hover-hide"></i>
                    {{node.name}}<br />
                    ({{node.contentTypeAlias}})
                </a>

                <div ng-if="!dialogEditor && ((model.config.showOpenButton && allowOpenButton) || (model.config.showEditButton && allowEditButton))">
                    <small ng-if="model.config.showOpenButton && allowOpenButton"><a href ng-click="showNode($index)"><localize key="open">Open</localize></a></small>
                    <small ng-if="model.config.showEditButton && allowEditButton"><a href umb-launch-mini-editor="node"><localize key="edit">Edit</localize></a></small>
                </div>
            </li>
        </ul>

        <ul class="unstyled list-icons" ng-show="model.config.multiPicker === true || renderModel.length === 0">
            <li>
                <i class="icon icon-add blue"></i>
                <a href="#" ng-click="openContentPicker()" prevent-default>
                    <localize key="general_add">Add</localize>
                </a>
            </li>
        </ul>

        <!--These are here because we need ng-form fields to validate against-->
        <input type="hidden" name="minCount" ng-model="renderModel" />
        <input type="hidden" name="maxCount" ng-model="renderModel" />

        <div class="help-inline" val-msg-for="minCount" val-toggle-msg="minCount">
            You need to add at least {{model.config.minNumber}} items
        </div>

        <div class="help-inline" val-msg-for="maxCount" val-toggle-msg="maxCount">
            You can only have {{model.config.maxNumber}} items selected
        </div>


    </ng-form>

    <umb-overlay
      ng-if="contentPickerOverlay.show"
      model="contentPickerOverlay"
      view="contentPickerOverlay.view"
      position="right">
    </umb-overlay>

</div>

customcontentpicker.controller.js

angular.module('umbraco.services').config([
   '$httpProvider',
   function ($httpProvider) {
       $httpProvider.interceptors.push(function ($q) {
           return {
               'request': function (request) {
                   var url = 'views/propertyeditors/contentpicker/contentpicker.html';
                   if (request.url.indexOf(url) !== -1) {
                       request.url = request.url.replace(url, '/App_Plugins/CustomContentPicker/customcontentpicker.html');
                   }
                   return request || $q.when(request);
               }
           };
       });
   }]);

// Below is contentpicker.controller.js modified

//this controller simply tells the dialogs service to open a mediaPicker window
//with a specified callback, this callback will receive an object with a selection on it

function customContentPickerController($scope, dialogService, entityResource, editorState, $log, iconHelper, $routeParams, fileManager, contentEditingHelper, angularHelper, navigationService, $location) {

    function trim(str, chr) {
        var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g');
        return str.replace(rgxtrim, '');
    }

    function startWatch() {
        //We need to watch our renderModel so that we can update the underlying $scope.model.value properly, this is required
        // because the ui-sortable doesn't dispatch an event after the digest of the sort operation. Any of the events for UI sortable
        // occur after the DOM has updated but BEFORE the digest has occured so the model has NOT changed yet - it even states so in the docs.
        // In their source code there is no event so we need to just subscribe to our model changes here.
        //This also makes it easier to manage models, we update one and the rest will just work.
        $scope.$watch(function () {
            //return the joined Ids as a string to watch
            return _.map($scope.renderModel, function (i) {
                return i.id;
            }).join();
        }, function (newVal) {
            var currIds = _.map($scope.renderModel, function (i) {
                return i.id;
            });
            $scope.model.value = trim(currIds.join(), ",");

            //Validate!
            if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) {
                $scope.contentPickerForm.minCount.$setValidity("minCount", false);
            }
            else {
                $scope.contentPickerForm.minCount.$setValidity("minCount", true);
            }

            if ($scope.model.config && $scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) < $scope.renderModel.length) {
                $scope.contentPickerForm.maxCount.$setValidity("maxCount", false);
            }
            else {
                $scope.contentPickerForm.maxCount.$setValidity("maxCount", true);
            }
        });
    }

    $scope.renderModel = [];

    $scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true;

    //the default pre-values
    var defaultConfig = {
        multiPicker: false,
        showOpenButton: false,
        showEditButton: false,
        showPathOnHover: false,
        startNode: {
            query: "",
            type: "content",
            id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker
        }
    };

    if ($scope.model.config) {
        //merge the server config on top of the default config, then set the server config to use the result
        $scope.model.config = angular.extend(defaultConfig, $scope.model.config);
    }

    //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that!
    $scope.model.config.multiPicker = ($scope.model.config.multiPicker === "1" ? true : false);
    $scope.model.config.showOpenButton = ($scope.model.config.showOpenButton === "1" ? true : false);
    $scope.model.config.showEditButton = ($scope.model.config.showEditButton === "1" ? true : false);
    $scope.model.config.showPathOnHover = ($scope.model.config.showPathOnHover === "1" ? true : false);

    var entityType = $scope.model.config.startNode.type === "member"
        ? "Member"
        : $scope.model.config.startNode.type === "media"
        ? "Media"
        : "Document";
    $scope.allowOpenButton = entityType === "Document" || entityType === "Media";
    $scope.allowEditButton = entityType === "Document";

    //the dialog options for the picker
    var dialogOptions = {
        multiPicker: $scope.model.config.multiPicker,
        entityType: entityType,
        filterCssClass: "not-allowed not-published",
        startNodeId: null,
        callback: function (data) {
            if (angular.isArray(data)) {
                _.each(data, function (item, i) {
                    $scope.add(item);
                });
            } else {
                $scope.clear();
                $scope.add(data);
            }
            angularHelper.getCurrentForm($scope).$setDirty();
        },
        treeAlias: $scope.model.config.startNode.type,
        section: $scope.model.config.startNode.type
    };

    //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the 
    // pre-value config on to the dialog options
    angular.extend(dialogOptions, $scope.model.config);

    //We need to manually handle the filter for members here since the tree displayed is different and only contains
    // searchable list views
    if (entityType === "Member") {
        //first change the not allowed filter css class
        dialogOptions.filterCssClass = "not-allowed";
        var currFilter = dialogOptions.filter;
        //now change the filter to be a method
        dialogOptions.filter = function (i) {
            //filter out the list view nodes
            if (i.metaData.isContainer) {
                return true;
            }
            if (!currFilter) {
                return false;
            }
            //now we need to filter based on what is stored in the pre-vals, this logic duplicates what is in the treepicker.controller, 
            // but not much we can do about that since members require special filtering.
            var filterItem = currFilter.toLowerCase().split(',');
            var found = filterItem.indexOf(i.metaData.contentType.toLowerCase()) >= 0;
            if (!currFilter.startsWith("!") && !found || currFilter.startsWith("!") && found) {
                return true;
            }

            return false;
        }
    }


    //if we have a query for the startnode, we will use that. 
    if ($scope.model.config.startNode.query) {
        var rootId = $routeParams.id;
        entityResource.getByQuery($scope.model.config.startNode.query, rootId, "Document").then(function (ent) {
            dialogOptions.startNodeId = ent.id;
        });
    } else {
        dialogOptions.startNodeId = $scope.model.config.startNode.id;
    }

    //dialog
    $scope.openContentPicker = function () {
        $scope.contentPickerOverlay = dialogOptions;
        $scope.contentPickerOverlay.view = "treepicker";
        $scope.contentPickerOverlay.show = true;

        $scope.contentPickerOverlay.submit = function (model) {

            if (angular.isArray(model.selection)) {
                _.each(model.selection, function (item, i) {
                    $scope.add(item);
                });
            }

            $scope.contentPickerOverlay.show = false;
            $scope.contentPickerOverlay = null;
        }

        $scope.contentPickerOverlay.close = function (oldModel) {
            $scope.contentPickerOverlay.show = false;
            $scope.contentPickerOverlay = null;
        }

    };

    $scope.remove = function (index) {
        $scope.renderModel.splice(index, 1);
        angularHelper.getCurrentForm($scope).$setDirty();
    };

    $scope.showNode = function (index) {
        var item = $scope.renderModel[index];
        var id = item.id;
        var section = $scope.model.config.startNode.type.toLowerCase();

        entityResource.getPath(id, entityType).then(function (path) {
            navigationService.changeSection(section);
            navigationService.showTree(section, {
                tree: section, path: path, forceReload: false, activate: true
            });
            var routePath = section + "/" + section + "/edit/" + id.toString();
            $location.path(routePath).search("");
        });
    }

    $scope.add = function (item) {
        var currIds = _.map($scope.renderModel, function (i) {
            return i.id;
        });

        if (currIds.indexOf(item.id) < 0) {
            item.icon = iconHelper.convertFromLegacyIcon(item.icon);
            $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, path: item.path, contentTypeAlias: item.metaData.ContentTypeAlias });
        }
    };

    $scope.clear = function () {
        $scope.renderModel = [];
    };

    var unsubscribe = $scope.$on("formSubmitting", function (ev, args) {
        var currIds = _.map($scope.renderModel, function (i) {
            return i.id;
        });
        $scope.model.value = trim(currIds.join(), ",");
    });

    //when the scope is destroyed we need to unsubscribe
    $scope.$on('$destroy', function () {
        unsubscribe();
    });

    //load current data
    var modelIds = $scope.model.value ? $scope.model.value.split(',') : [];
    entityResource.getByIds(modelIds, entityType).then(function (data) {

        //Ensure we populate the render model in the same order that the ids were stored!
        _.each(modelIds, function (id, i) {
            var entity = _.find(data, function (d) {
                return d.id == id;
            });

            if (entity) {
                entity.icon = iconHelper.convertFromLegacyIcon(entity.icon);
                $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon, path: entity.path, contentTypeAlias: entity.metaData.ContentTypeAlias });
            }


        });

        //everything is loaded, start the watch on the model
        startWatch();

    });
}

angular.module('umbraco').controller("Umbraco.PropertyEditors.CustomContentPickerController", customContentPickerController);