diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 0525b84ae7..64cba5e793 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -5,10 +5,177 @@ * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper($location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) { +function contentEditingHelper($q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState, keyboardService) { return { + /** Used by the content editor and mini content editor to perform saving operations */ + contentEditorPerformSave: function (args) { + if (!angular.isObject(args)) { + throw "args must be an object"; + } + if (!args.fileManager) { + throw "args.fileManager is not defined"; + } + if (!args.scope) { + throw "args.scope is not defined"; + } + if (!args.content) { + throw "args.content is not defined"; + } + if (!args.statusMessage) { + throw "args.statusMessage is not defined"; + } + if (!args.saveMethod) { + throw "args.saveMethod is not defined"; + } + + var self = this; + + var deferred = $q.defer(); + + if (formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage })) { + args.saveMethod(args.content, $routeParams.create, args.fileManager.getFiles()) + .then(function (data) { + + formHelper.resetForm({ scope: args.scope, notifications: data.notifications }); + + self.handleSuccessfulSave({ + scope: args.scope, + savedContent: data, + rebindCallback: self.reBindChangedProperties(args.content, data) + }); + deferred.resolve(data); + + }, function (err) { + self.handleSaveError({ + redirectOnFailure: true, + err: err, + rebindCallback: self.reBindChangedProperties(args.content, err.data) + }); + deferred.reject(err); + }); + } + else { + deferred.reject(); + } + + return deferred.promise; + }, + + /** Returns the action button definitions based on what permissions the user has. + The content.allowedActions parameter contains a list of chars, each represents a button by permission so + here we'll build the buttons according to the chars of the user. */ + configureContentEditorButtons: function (args) { + + if (!angular.isObject(args)) { + throw "args must be an object"; + } + if (!args.content) { + throw "args.content is not defined"; + } + if (!args.methods) { + throw "args.methods is not defined"; + } + if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) { + throw "args.methods does not contain all required defined methods"; + } + + var buttons = { + defaultButton: null, + subButtons: [] + }; + + function createButtonDefinition(ch) { + switch (ch) { + case "U": + //publish action + keyboardService.bind("ctrl+p", args.methods.saveAndPublish); + + return { + letter: ch, + labelKey: "buttons_saveAndPublish", + handler: args.methods.saveAndPublish, + hotKey: "ctrl+p" + }; + case "H": + //send to publish + keyboardService.bind("ctrl+p", args.methods.sendToPublish); + + return { + letter: ch, + labelKey: "buttons_saveToPublish", + handler: args.methods.sendToPublish, + hotKey: "ctrl+p" + }; + case "A": + //save + keyboardService.bind("ctrl+s", args.methods.save); + return { + letter: ch, + labelKey: "buttons_save", + handler: args.methods.save, + hotKey: "ctrl+s" + }; + case "Z": + //unpublish + keyboardService.bind("ctrl+u", args.methods.unPublish); + + return { + letter: ch, + labelKey: "content_unPublish", + handler: args.methods.unPublish + }; + default: + return null; + } + } + + //reset + buttons.subButtons = []; + + //This is the ideal button order but depends on circumstance, we'll use this array to create the button list + // Publish, SendToPublish, Save + var buttonOrder = ["U", "H", "A"]; + + //Create the first button (primary button) + //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. + if (!args.create || _.contains(args.content.allowedActions, "C")) { + for (var b in buttonOrder) { + if (_.contains(args.content.allowedActions, buttonOrder[b])) { + buttons.defaultButton = createButtonDefinition(buttonOrder[b]); + break; + } + } + } + + //Now we need to make the drop down button list, this is also slightly tricky because: + //We cannot have any buttons if there's no default button above. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. + if (buttons.defaultButton) { + + //get the last index of the button order + var lastIndex = _.indexOf(buttonOrder, buttons.defaultButton.letter); + //add the remaining + for (var i = lastIndex + 1; i < buttonOrder.length; i++) { + if (_.contains(args.content.allowedActions, buttonOrder[i])) { + buttons.subButtons.push(createButtonDefinition(buttonOrder[i])); + } + } + + + //if we are not creating, then we should add unpublish too, + // so long as it's already published and if the user has access to publish + if (!args.create) { + if (args.content.publishDate && _.contains(args.content.allowedActions, "U")) { + buttons.subButtons.push(createButtonDefinition("Z")); + } + } + } + + return buttons; + }, /** * @ngdoc method diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 4057623675..a4ee1c31fb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -1,21 +1,18 @@ -/** - * @ngdoc service - * @name umbraco.services.fileManager - * @function - * - * @description - * Used by editors to manage any files that require uploading with the posted data, normally called by property editors - * that need to attach files. - * When a route changes successfully, we ensure that the collection is cleared. - */ -function fileManager() { + +//This is the file manager class, for the normal angular service 'fileManager' an instance of this class is returned, +// however in some cases we also want to create sub-instances (non-singleton) of this class. An example for this is the mini +// content editor that is launched from inside of a content editor. If the global singleton instance is used for both editors, then +// they will be sharing the same file collection state which will cause unwanted side affects. So in the mini editor, we create +// a standalone instance of the file manager and dipose of it when the scope disposes. +function FileManager() { var fileCollection = []; return { + /** * @ngdoc function - * @name umbraco.services.fileManager#addFiles + * @name umbraco.services.fileManager#createStandaloneInstance * @methodOf umbraco.services.fileManager * @function * @@ -23,7 +20,23 @@ function fileManager() { * Attaches files to the current manager for the current editor for a particular property, if an empty array is set * for the files collection that effectively clears the files for the specified editor. */ - setFiles: function(propertyAlias, files) { + createStandaloneInstance: function() { + return new FileManager(); + }, + + /** + * @ngdoc function + * @name umbraco.services.fileManager#addFiles + * @methodOf umbraco.services.fileManager + * @function + * + * @description + * in some cases we want to create stand alone instances (non-singleton) of the fileManager. An example for this is the mini + * content editor that is launched from inside of a content editor. If the default global singleton instance is used for both editors, then + * they will be sharing the same file collection state which will cause unwanted side affects. So in the mini editor, we create + * a standalone instance of the file manager and dipose of it when the scope disposes. + */ + setFiles: function (propertyAlias, files) { //this will clear the files for the current property and then add the new ones for the current property fileCollection = _.reject(fileCollection, function (item) { return item.alias === propertyAlias; @@ -33,7 +46,7 @@ function fileManager() { fileCollection.push({ alias: propertyAlias, file: files[i] }); } }, - + /** * @ngdoc function * @name umbraco.services.fileManager#getFiles @@ -43,10 +56,10 @@ function fileManager() { * @description * Returns all of the files attached to the file manager */ - getFiles: function() { + getFiles: function () { return fileCollection; }, - + /** * @ngdoc function * @name umbraco.services.fileManager#clearFiles @@ -59,7 +72,23 @@ function fileManager() { clearFiles: function () { fileCollection = []; } -}; + }; } -angular.module('umbraco.services').factory('fileManager', fileManager); \ No newline at end of file + +angular.module('umbraco.services').factory('fileManager', + + /** + * @ngdoc service + * @name umbraco.services.fileManager + * @function + * + * @description + * Used by editors to manage any files that require uploading with the posted data, normally called by property editors + * that need to attach files. + * When a route changes successfully, we ensure that the collection is cleared. + */ + function () { + //return a new instance of a file Manager - this will be a singleton per app + return new FileManager(); + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index c6f54a75e8..1aea07bfbd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -40,6 +40,10 @@ padding: 10px; } +.umb-contentpicker small a { + color: @gray; +} + /* CODEMIRROR DATATYPE */ div.umb-codeeditor { border: 1px solid #e8e8e8; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.controller.js index 586bf7333a..d89cd1e8ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.controller.js @@ -1,50 +1,82 @@ function ContentEditDialogController($scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) { - //setup scope vars - $scope.model = {}; - $scope.model.defaultButton = null; - $scope.model.subButtons = []; - $scope.model.nodeId = 0; - + + $scope.defaultButton = null; + $scope.subButtons = []; var dialogOptions = $scope.$parent.dialogOptions; + var fileManagerInstance = fileManager.createStandaloneInstance(); - if(angular.isObject(dialogOptions.entity)){ - $scope.model.entity = $scope.filterTabs(dialogOptions.entity, dialogOptions.tabFilter); - $scope.loaded = true; - }else{ + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + contentEditingHelper.contentEditorPerformSave({ + fileManager: fileManagerInstance, + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content + }).then(function (content) { + //success + $scope.busy = false; + + if (dialogOptions.closeOnSave) { + $scope.submit(content); + } - if (dialogOptions.create) { - //we are creating so get an empty content item - contentResource.getScaffold(dialogOptions.id, dialogOptions.contentType) - .then(function(data) { - $scope.loaded = true; - $scope.model.entity = $scope.filterTabs(data, dialogOptions.tabFilter); - }); - } - else { - //we are editing so get the content item from the server - contentResource.getById(dialogOptions.id) - .then(function(data) { - $scope.loaded = true; - $scope.model.entity = $scope.filterTabs(data, dialogOptions.tabFilter); - - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - }); - } + }, function(err) { + //error + $scope.busy = false; + }); } - function performSave(args) { - var deferred = $q.defer(); + function filterTabs(entity, blackList) { + if (blackList) { + _.each(entity.tabs, function (tab) { + tab.hide = _.contains(blackList, tab.alias); + }); + } - $scope.busy = true; + return entity; + }; + + function init(content) { + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $routeParams.create, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish + } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + } - if (formHelper.submitForm({ scope: $scope, statusMessage: args.statusMessage })) { + //check if the entity is being passed in, otherwise load it from the server + if (angular.isObject(dialogOptions.entity)) { + $scope.loaded = true; + $scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter); + init($scope.content); + } + else { + contentResource.getById(dialogOptions.id) + .then(function(data) { + $scope.loaded = true; + $scope.content = filterTabs(data, dialogOptions.tabFilter); + init($scope.content); + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + }); + } - args.saveMethod($scope.model.entity, $routeParams.create, fileManager.getFiles()) + $scope.unPublish = function () { + + if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { + + contentResource.unPublish($scope.content.id) .then(function (data) { formHelper.resetForm({ scope: $scope, notifications: data.notifications }); @@ -52,63 +84,43 @@ function ContentEditDialogController($scope, $routeParams, $q, $timeout, $window contentEditingHelper.handleSuccessfulSave({ scope: $scope, savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.model.entity, data) + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); - $scope.busy = false; - deferred.resolve(data); - - }, function (err) { - - contentEditingHelper.handleSaveError({ - redirectOnFailure: true, - err: err, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.model.entity, err.data) - }); - - $scope.busy = false; - deferred.reject(err); + init($scope.content); }); } - else { - $scope.busy = false; - deferred.reject(); - } - return deferred.promise; - } - - $scope.filterTabs = function(entity, blackList){ - if(blackList){ - _.each(entity.tabs, function(tab){ - tab.hide = _.contains(blackList, tab.alias); - }); - } - - return entity; }; - $scope.saveAndPublish = function() { - $scope.submit($scope.model.entity); + $scope.sendToPublish = function () { + performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending..." }); }; - $scope.saveAndPublish = function() { - performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." }) - .then(function(content){ - if(dialogOptions.closeOnSave){ - $scope.submit(content); - } - }); + $scope.saveAndPublish = function () { + performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." }); }; $scope.save = function () { - performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." }) - .then(function(content){ - if(dialogOptions.closeOnSave){ - $scope.submit(content); - } - }); + performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." }); }; + + // this method is called for all action buttons and then we proxy based on the btn definition + $scope.performAction = function (btn) { + + if (!btn || !angular.isFunction(btn.handler)) { + throw "btn.handler must be a function reference"; + } + + if (!$scope.busy) { + btn.handler.apply(this); + } + }; + + //dispose the file manager instance + $scope.$on('$destroy', function () { + fileManagerInstance.clearFiles(); + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html index 4f3e3ca470..ad0a8e3c5e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html @@ -8,20 +8,19 @@
+ ng-model="content.name"/>
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index bcfb1a1722..a1f0f513d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -14,53 +14,23 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ $scope.currentSection = appState.getSectionState("currentSection"); $scope.currentNode = null; //the editors affiliated node $scope.isNew = $routeParams.create; + + function init(content) { - //This sets up the action buttons based on what permissions the user has. - //The allowedActions parameter contains a list of chars, each represents a button by permission so - //here we'll build the buttons according to the chars of the user. - function configureButtons(content) { - //reset - $scope.subButtons = []; - - //This is the ideal button order but depends on circumstance, we'll use this array to create the button list - // Publish, SendToPublish, Save - var buttonOrder = ["U", "H", "A"]; - - //Create the first button (primary button) - //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. - if (!$routeParams.create || _.contains(content.allowedActions, "C")) { - for (var b in buttonOrder) { - if (_.contains(content.allowedActions, buttonOrder[b])) { - $scope.defaultButton = createButtonDefinition(buttonOrder[b]); - break; - } + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $routeParams.create, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish } - } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; - //Now we need to make the drop down button list, this is also slightly tricky because: - //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. - if ($scope.defaultButton) { - - //get the last index of the button order - var lastIndex = _.indexOf(buttonOrder, $scope.defaultButton.letter); - //add the remaining - for (var i = lastIndex + 1; i < buttonOrder.length; i++) { - if (_.contains(content.allowedActions, buttonOrder[i])) { - $scope.subButtons.push(createButtonDefinition(buttonOrder[i])); - } - } - - - //if we are not creating, then we should add unpublish too, - // so long as it's already published and if the user has access to publish - if (!$routeParams.create) { - if (content.publishDate && _.contains(content.allowedActions, "U")) { - $scope.subButtons.push(createButtonDefinition("Z")); - } - } - } + editorState.set($scope.content); //We fetch all ancestors of the node to generate the footer breadcrump navigation if (!$routeParams.create) { @@ -72,51 +42,6 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ } } - function createButtonDefinition(ch) { - switch (ch) { - case "U": - //publish action - keyboardService.bind("ctrl+p", $scope.saveAndPublish); - - return { - letter: ch, - labelKey: "buttons_saveAndPublish", - handler: $scope.saveAndPublish, - hotKey: "ctrl+p" - }; - case "H": - //send to publish - keyboardService.bind("ctrl+p", $scope.sendToPublish); - - return { - letter: ch, - labelKey: "buttons_saveToPublish", - handler: $scope.sendToPublish, - hotKey: "ctrl+p" - }; - case "A": - //save - keyboardService.bind("ctrl+s", $scope.save); - return { - letter: ch, - labelKey: "buttons_save", - handler: $scope.save, - hotKey: "ctrl+s" - }; - case "Z": - //unpublish - keyboardService.bind("ctrl+u", $scope.unPublish); - - return { - letter: ch, - labelKey: "content_unPublish", - handler: $scope.unPublish - }; - default: - return null; - } - } - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { @@ -140,54 +65,26 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ } } - /** This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish */ + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { - var deferred = $q.defer(); - - $scope.busy = true; - - if (formHelper.submitForm({ scope: $scope, statusMessage: args.statusMessage })) { - - args.saveMethod($scope.content, $routeParams.create, fileManager.getFiles()) - .then(function (data) { - - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - editorState.set($scope.content); - $scope.busy = false; - - configureButtons(data); - - syncTreeNode($scope.content, data.path); - - deferred.resolve(data); - - }, function (err) { - - contentEditingHelper.handleSaveError({ - redirectOnFailure: true, - err: err, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) - }); - - editorState.set($scope.content); - $scope.busy = false; - - deferred.reject(err); - }); - } - else { + contentEditingHelper.contentEditorPerformSave({ + fileManager: fileManager, + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content + }).then(function (data) { + //success $scope.busy = false; - deferred.reject(); - } - - return deferred.promise; + init($scope.content); + syncTreeNode($scope.content, data.path); + }, function (err) { + //error + $scope.busy = false; + if (err) { + editorState.set($scope.content); + } + }); } function resetLastListPageNumber(content) { @@ -206,9 +103,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ $scope.loaded = true; $scope.content = data; - editorState.set($scope.content); - - configureButtons($scope.content); + init($scope.content); resetLastListPageNumber($scope.content); }); @@ -226,9 +121,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ : "/content/content/edit/" + data.parentId; } - editorState.set($scope.content); - - configureButtons($scope.content); + init($scope.content); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display @@ -258,9 +151,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); - editorState.set($scope.content); - - configureButtons(data); + init($scope.content); syncTreeNode($scope.content, data.path); @@ -301,7 +192,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ }; - /** this method is called for all action buttons and then we proxy based on the btn definition */ + // this method is called for all action buttons and then we proxy based on the btn definition $scope.performAction = function (btn) { if (!btn || !angular.isFunction(btn.handler)) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 3220b059d5..3f2652179a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -68,8 +68,7 @@ angular.module('umbraco') }; $scope.edit = function (node) { - dialogService.open( - { + dialogService.open({ template: "views/common/dialogs/content/edit.html", id: node.id, closeOnSave:true, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index 69662f7aa6..931b95dc9d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -15,7 +15,9 @@ {{node.name}} -
Edit +
+ Edit +