Why can't I use the build in data type: Umbraco.MultiNodeTreePicker as a parameter for a Macro? I've configurated a custom data type that uses the Umbraco.MultiNodeTreePicker and I can use is in a document type, but not in a macro. Why is this and how to solve this / what's the way to go?
You can add multiple versions into the package manifest with different names and pre values for start node and content type and use them as macro parameters!
This is what I gather from different posts about implementing MultiNode Tree Picker as a parameter for a Macro, bits and pieces here and there and finally got it to work.
Create a custom folder in your App_Plugins, in this case I name it CustomPicker. In that folder create another 3 files, package.manifest, customPicker.controller.js and customPicker.html.
Copy content from Umbraco source /umbraco/Views/propertyeditors/contentpicker/contentpicker.html and paste it to customPicker.html.
Then copy just the contentPickerController function in Umbraco source /umbraco/Js/umbraco.controllers.js and paste it to customPickerController.js.
Make sure to use the same name for your controller and your view. In this case I use "CustomPicker.CustomPickerController";
Refresh your page, and now you can find MyCustomPickerEditor in your Macro Parameters Type drop down list.
<div ng-controller="CustomPicker.CustomPickerController" 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}}
</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>
customPickerController.js
//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 contentPickerController($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 });
}
};
$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 });
}
});
//everything is loaded, start the watch on the model
startWatch();
});
}angular.module('umbraco').controller("CustomPicker.CustomPickerController", contentPickerController);
Good work overall but unfortunately, I cannot get this to work.
I add it as a new datatype and try to define the root node but all it does is take me to the homepage.
I then tried adding it directly as a macro parameter and it will not let me select anything at all.
I still can't believe that something like this is not in Umbraco as standard.
I realise this is a bump... Until I updated to 7.6.5 .... I had this working (a multinode treepicker as a parameter in my macro)... When I ran this previously
Model.MacroParamters["mymultinodetreepicker"]... gave me a CSV list of node IDs... then I used Umbraco.TypedContent(id) to get the IPublishedContent Item....
@inherits Umbraco.Web.Macros.PartialViewMacroPage
@{
var items = Model.MacroParameters["SelectedHighlightSections"].ToString().Trim();
string[] _items = new string[0] { };
_items = (items.Length > 0) ? items.Split(',') : _items;
}
@if (_items.Length > 0)
{
<section id="highlight-section" class="sections highlight-section">
<div class="container-fluid">
<div class="row text-center">
@for (var i = 0; i < _items.Length; i++)
{
var item = Umbraco.TypedContent(_items[i]);
..... more stuff
Now my Model.MacroParameters call returns an object string... not a List
This is a documented breaking change with 7.6.(2-3)
MultiNodeTreePicker
Hi all,
Why can't I use the build in data type: Umbraco.MultiNodeTreePicker as a parameter for a Macro? I've configurated a custom data type that uses the Umbraco.MultiNodeTreePicker and I can use is in a document type, but not in a macro. Why is this and how to solve this / what's the way to go?
Its because there is no way of adding pre values to a macro parameter.
You need to make your own version with some hard coded pre values.
See below for a version we use for a single content picker....
JS Controller
Angular view
Package manefest
}
You can add multiple versions into the package manifest with different names and pre values for start node and content type and use them as macro parameters!
Thanks
Carl
Ok, thank you for the clarification. I was searching the forum and the internet using Google but could not find a satisfying answer.
Thank you for your answer!
This is what I gather from different posts about implementing MultiNode Tree Picker as a parameter for a Macro, bits and pieces here and there and finally got it to work.
Create a custom folder in your App_Plugins, in this case I name it CustomPicker. In that folder create another 3 files, package.manifest, customPicker.controller.js and customPicker.html.
Copy content from Umbraco source /umbraco/Views/propertyeditors/contentpicker/contentpicker.html and paste it to customPicker.html.
Then copy just the contentPickerController function in Umbraco source /umbraco/Js/umbraco.controllers.js and paste it to customPickerController.js.
Make sure to use the same name for your controller and your view. In this case I use "CustomPicker.CustomPickerController";
Refresh your page, and now you can find MyCustomPickerEditor in your Macro Parameters Type drop down list.
~ Enjoy ~
package.manifest
customPicker.html
customPickerController.js
Good work overall but unfortunately, I cannot get this to work. I add it as a new datatype and try to define the root node but all it does is take me to the homepage.
I then tried adding it directly as a macro parameter and it will not let me select anything at all.
I still can't believe that something like this is not in Umbraco as standard.
I realise this is a bump... Until I updated to 7.6.5 .... I had this working (a multinode treepicker as a parameter in my macro)... When I ran this previously
Model.MacroParamters["mymultinodetreepicker"]... gave me a CSV list of node IDs... then I used Umbraco.TypedContent(id) to get the IPublishedContent Item....
Now my Model.MacroParameters call returns an object string... not a List
This is a documented breaking change with 7.6.(2-3)
is working on a reply...