diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 0f3eea3291..4cef3058dd 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -28,6 +28,7 @@ "tinymce": "~4.1.10", "codemirror": "~5.3.0", "angular-local-storage": "~0.2.3", - "moment": "~2.10.3" + "moment": "~2.10.3", + "ace-builds": "^1.2.3" } } diff --git a/src/Umbraco.Web.UI.Client/gruntFile.js b/src/Umbraco.Web.UI.Client/gruntFile.js index 0a28d9efaf..9ce6c14731 100644 --- a/src/Umbraco.Web.UI.Client/gruntFile.js +++ b/src/Umbraco.Web.UI.Client/gruntFile.js @@ -394,7 +394,7 @@ module.exports = function (grunt) { tutorials: { src: [], title: '' - } + } }, eslint:{ @@ -524,7 +524,10 @@ module.exports = function (grunt) { 'addon/selection/*', 'addon/dialog/*' ] - } + }, + 'ace-builds': { + files: ['src-min-noconflict/**'] + } } } }, diff --git a/src/Umbraco.Web.UI.Client/lib/ace-razor-mode/theme/razor_chrome.css b/src/Umbraco.Web.UI.Client/lib/ace-razor-mode/theme/razor_chrome.css new file mode 100644 index 0000000000..7f401520d5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/ace-razor-mode/theme/razor_chrome.css @@ -0,0 +1,161 @@ +.ace-chrome .ace_gutter { + background: white !important; + color: #ccc !important; + overflow : hidden; +} + +.ace-chrome .ace_print-margin { + +} + +.ace-chrome { + background-color: #FFFFFF; + color: black; +} + +.ace-chrome .ace_cursor { + color: black; +} + +.ace-chrome .ace_invisible { + color: rgb(191, 191, 191); +} + +.ace-chrome .ace_constant.ace_buildin { + color: rgb(88, 72, 246); +} + +.ace-chrome .ace_constant.ace_language { + color: rgb(88, 92, 246); +} + +.ace-chrome .ace_constant.ace_library { + color: rgb(6, 150, 14); +} + +.ace-chrome .ace_invalid { + background-color: rgb(153, 0, 0); + color: white; +} + +.ace_punctuation.ace_short.ace_razor{ + background: yellow; +} + +.ace-chrome .ace_fold { +} + +.ace-chrome .ace_support.ace_function { + color: rgb(60, 76, 114); +} + +.ace-chrome .ace_support.ace_constant { + color: rgb(6, 150, 14); +} + +.ace-chrome .ace_support.ace_type, +.ace-chrome .ace_support.ace_class +.ace-chrome .ace_support.ace_other { + color: rgb(109, 121, 222); +} + +.ace-chrome .ace_variable.ace_parameter { + font-style:italic; + color:#FD971F; +} +.ace-chrome .ace_keyword.ace_operator { + color: rgb(104, 118, 135); +} + +.ace-chrome .ace_comment { + color: #236e24; +} + +.ace-chrome .ace_comment.ace_doc { + color: #236e24; +} + +.ace-chrome .ace_comment.ace_doc.ace_tag { + color: #236e24; +} + +.ace-chrome .ace_constant.ace_numeric { + color: rgb(0, 0, 205); +} + +.ace-chrome .ace_variable { + color: rgb(49, 132, 149); +} + +.ace-chrome .ace_xml-pe { + color: rgb(104, 104, 91); +} + +.ace-chrome .ace_entity.ace_name.ace_function { + color: #0000A2; +} + + +.ace-chrome .ace_heading { + color: rgb(12, 7, 255); +} + +.ace-chrome .ace_list { + color:rgb(185, 6, 144); +} + +.ace-chrome .ace_marker-layer .ace_selection { + background: rgb(181, 213, 255); +} + +.ace-chrome .ace_marker-layer .ace_step { + background: rgb(252, 255, 0); +} + +.ace-chrome .ace_marker-layer .ace_stack { + background: rgb(164, 229, 101); +} + +.ace-chrome .ace_marker-layer .ace_bracket { + margin: -1px 0 0 -1px; + border: 1px solid rgb(192, 192, 192); +} + +.ace-chrome .ace_marker-layer .ace_active-line { + background: rgba(0, 0, 0, 0.07) !important; +} + +.ace-chrome .ace_gutter-active-line { + background: rgba(0, 0, 0, 0.07) !important; +} + +.ace-chrome .ace_marker-layer .ace_selected-word { + background: rgb(250, 250, 255); + border: 1px solid rgb(200, 200, 250); +} + +.ace-chrome .ace_storage, +.ace-chrome .ace_keyword, +.ace-chrome .ace_meta.ace_tag { + color: rgb(147, 15, 128); +} + +.ace-chrome .ace_string.ace_regex { + color: rgb(255, 0, 0) +} + +.ace-chrome .ace_string { + color: #1A1AA6; +} + +.ace-chrome .ace_entity.ace_other.ace_attribute-name { + color: #994409; +} + +.ace-chrome .ace_indent-guide { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y; +} + +.ace-chrome .ace_razor { + background: yellow; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index cc57eb6e74..4d9222fa45 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -254,6 +254,7 @@ Use this directive to construct a header inside the main editor window. hideAlias: "@", description: "=", hideDescription: "@", + descriptionLocked: "@", navigation: "=" }, link: link diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js new file mode 100644 index 0000000000..c97851bbb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js @@ -0,0 +1,342 @@ +(function() { + 'use strict'; + + function AceEditorDirective(umbAceEditorConfig, assetsService, angularHelper) { + + /** + * Sets editor options such as the wrapping mode or the syntax checker. + * + * The supported options are: + * + * + * + * @param acee + * @param session ACE editor session + * @param {object} opts Options to be set + */ + var setOptions = function(acee, session, opts) { + + // sets the ace worker path, if running from concatenated + // or minified source + if (angular.isDefined(opts.workerPath)) { + var config = window.ace.require('ace/config'); + config.set('workerPath', opts.workerPath); + } + + // ace requires loading + if (angular.isDefined(opts.require)) { + opts.require.forEach(function(n) { + window.ace.require(n); + }); + } + + // Boolean options + if (angular.isDefined(opts.showGutter)) { + acee.renderer.setShowGutter(opts.showGutter); + } + if (angular.isDefined(opts.useWrapMode)) { + session.setUseWrapMode(opts.useWrapMode); + } + if (angular.isDefined(opts.showInvisibles)) { + acee.renderer.setShowInvisibles(opts.showInvisibles); + } + if (angular.isDefined(opts.showIndentGuides)) { + acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); + } + if (angular.isDefined(opts.useSoftTabs)) { + session.setUseSoftTabs(opts.useSoftTabs); + } + if (angular.isDefined(opts.showPrintMargin)) { + acee.setShowPrintMargin(opts.showPrintMargin); + } + + // commands + if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { + acee.commands.addCommands([{ + name: 'unfind', + bindKey: { + win: 'Ctrl-F', + mac: 'Command-F' + }, + exec: function() { + return false; + }, + readOnly: true + }]); + } + + // Basic options + if (angular.isString(opts.theme)) { + acee.setTheme('ace/theme/' + opts.theme); + } + if (angular.isString(opts.mode)) { + session.setMode('ace/mode/' + opts.mode); + } + // Advanced options + if (angular.isDefined(opts.firstLineNumber)) { + if (angular.isNumber(opts.firstLineNumber)) { + session.setOption('firstLineNumber', opts.firstLineNumber); + } else if (angular.isFunction(opts.firstLineNumber)) { + session.setOption('firstLineNumber', opts.firstLineNumber()); + } + } + + // advanced options + var key, obj; + if (angular.isDefined(opts.advanced)) { + for (key in opts.advanced) { + // create a javascript object with the key and value + obj = { + name: key, + value: opts.advanced[key] + }; + // try to assign the option to the ace editor + acee.setOption(obj.name, obj.value); + } + } + + // advanced options for the renderer + if (angular.isDefined(opts.rendererOptions)) { + for (key in opts.rendererOptions) { + // create a javascript object with the key and value + obj = { + name: key, + value: opts.rendererOptions[key] + }; + // try to assign the option to the ace editor + acee.renderer.setOption(obj.name, obj.value); + } + } + + // onLoad callbacks + angular.forEach(opts.callbacks, function(cb) { + if (angular.isFunction(cb)) { + cb(acee); + } + }); + }; + + function link(scope, el, attr, ngModel) { + + // Load in ace library + assetsService.loadJs('lib/ace-builds/src-min-noconflict/ace.js').then(function () { + if (angular.isUndefined(window.ace)) { + throw new Error('ui-ace need ace to work... (o rly?)'); + } else { + // init editor + init(); + } + }); + + function init() { + + /** + * Corresponds the umbAceEditorConfig ACE configuration. + * @type object + */ + var options = umbAceEditorConfig.ace || {}; + + /** + * umbAceEditorConfig merged with user options via json in attribute or data binding + * @type object + */ + var opts = angular.extend({}, options, scope.umbAceEditor); + + + //load ace libraries here... + + /** + * ACE editor + * @type object + */ + var acee = window.ace.edit(el[0]); + acee.$blockScrolling = Infinity; + + /** + * ACE editor session. + * @type object + * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session} + */ + var session = acee.getSession(); + + /** + * Reference to a change listener created by the listener factory. + * @function + * @see listenerFactory.onChange + */ + var onChangeListener; + + /** + * Reference to a blur listener created by the listener factory. + * @function + * @see listenerFactory.onBlur + */ + var onBlurListener; + + /** + * Calls a callback by checking its existing. The argument list + * is variable and thus this function is relying on the arguments + * object. + * @throws {Error} If the callback isn't a function + */ + var executeUserCallback = function() { + + /** + * The callback function grabbed from the array-like arguments + * object. The first argument should always be the callback. + * + * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} + * @type {*} + */ + var callback = arguments[0]; + + /** + * Arguments to be passed to the callback. These are taken + * from the array-like arguments object. The first argument + * is stripped because that should be the callback function. + * + * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments} + * @type {Array} + */ + var args = Array.prototype.slice.call(arguments, 1); + + if (angular.isDefined(callback)) { + scope.$evalAsync(function() { + if (angular.isFunction(callback)) { + callback(args); + } else { + throw new Error('ui-ace use a function as callback.'); + } + }); + } + }; + + + + /** + * Listener factory. Until now only change listeners can be created. + * @type object + */ + var listenerFactory = { + /** + * Creates a change listener which propagates the change event + * and the editor session to the callback from the user option + * onChange. It might be exchanged during runtime, if this + * happens the old listener will be unbound. + * + * @param callback callback function defined in the user options + * @see onChangeListener + */ + onChange: function(callback) { + return function(e) { + var newValue = session.getValue(); + angularHelper.safeApply(scope, function () { + scope.model = newValue; + }); + executeUserCallback(callback, e, acee); + }; + }, + /** + * Creates a blur listener which propagates the editor session + * to the callback from the user option onBlur. It might be + * exchanged during runtime, if this happens the old listener + * will be unbound. + * + * @param callback callback function defined in the user options + * @see onBlurListener + */ + onBlur: function(callback) { + return function() { + executeUserCallback(callback, acee); + }; + } + }; + + attr.$observe('readonly', function(value) { + acee.setReadOnly(!!value || value === ''); + }); + + // Value Blind + if(scope.model) { + session.setValue(scope.model); + } + + // Listen for option updates + var updateOptions = function(current, previous) { + if (current === previous) { + return; + } + + opts = angular.extend({}, options, scope.umbAceEditor); + + opts.callbacks = [opts.onLoad]; + if (opts.onLoad !== options.onLoad) { + // also call the global onLoad handler + opts.callbacks.unshift(options.onLoad); + } + + // EVENTS + + // unbind old change listener + session.removeListener('change', onChangeListener); + + // bind new change listener + onChangeListener = listenerFactory.onChange(opts.onChange); + session.on('change', onChangeListener); + + // unbind old blur listener + //session.removeListener('blur', onBlurListener); + acee.removeListener('blur', onBlurListener); + + // bind new blur listener + onBlurListener = listenerFactory.onBlur(opts.onBlur); + acee.on('blur', onBlurListener); + + setOptions(acee, session, opts); + }; + + scope.$watch(scope.umbAceEditor, updateOptions, /* deep watch */ true); + + // set the options here, even if we try to watch later, if this + // line is missing things go wrong (and the tests will also fail) + updateOptions(options); + + el.on('$destroy', function() { + acee.session.$stopWorker(); + acee.destroy(); + }); + + scope.$watch(function() { + return [el[0].offsetWidth, el[0].offsetHeight]; + }, function() { + acee.resize(); + acee.renderer.updateFull(); + }, true); + + } + + } + + var directive = { + restrict: 'EA', + scope: { + "umbAceEditor": "=", + "model": "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives') + .constant('umbAceEditorConfig', {}) + .directive('umbAceEditor', AceEditorDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js new file mode 100644 index 0000000000..d7e44df113 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -0,0 +1,179 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbDateTimePicker +@restrict E +@scope + +@description +Added in Umbraco version 7.6 +This directive is a wrapper of the bootstrap datetime picker version 3.1.3. Use it to render a date time picker. +For extra details about options and events take a look here: http://eonasdan.github.io/bootstrap-datetimepicker/ + +Use this directive to render a date time picker + +

Markup example

+
+	
+ + + + +
+
+ +

Controller example

+
+	(function () {
+		"use strict";
+
+		function Controller() {
+
+            var vm = this;
+
+            vm.date = "";
+
+            vm.config = {
+                pickDate: true,
+                pickTime: true,
+                useSeconds: true,
+                format: "YYYY-MM-DD HH:mm:ss",
+                icons: {
+                    time: "icon-time",
+                    date: "icon-calendar",
+                    up: "icon-chevron-up",
+                    down: "icon-chevron-down"
+                }
+            };
+
+            vm.datePickerChange = datePickerChange;
+            vm.datePickerError = datePickerError;
+
+            function datePickerChange(event) {
+                // handle change
+                if(event.date && event.date.isValid()) {
+                    var date = event.date.format(vm.datePickerConfig.format);
+                }
+            }
+
+            function datePickerError(event) {
+                // handle error
+            }
+
+        }
+
+		angular.module("umbraco").controller("My.Controller", Controller);
+
+	})();
+
+ +@param {object} options (binding): Config object for the date picker. +@param {callback} onHide (callback): Hide callback. +@param {callback} onShow (callback): Show callback. +@param {callback} onChange (callback): Change callback. +@param {callback} onError (callback): Error callback. +@param {callback} onUpdate (callback): Update callback. +**/ + +(function () { + 'use strict'; + + function DateTimePickerDirective(assetsService) { + + function link(scope, element, attrs, ctrl) { + + function onInit() { + // load css file for the date picker + assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css'); + + // load the js file for the date picker + assetsService.loadJs('lib/datetimepicker/bootstrap-datetimepicker.js').then(function () { + // init date picker + initDatePicker(); + }); + } + + function onHide(event) { + if (scope.onHide) { + scope.$apply(function(){ + // callback + scope.onHide({event: event}); + }); + } + } + + function onShow() { + if (scope.onShow) { + scope.$apply(function(){ + // callback + scope.onShow(); + }); + } + } + + function onChange(event) { + if (scope.onChange && event.date && event.date.isValid()) { + scope.$apply(function(){ + // callback + scope.onChange({event: event}); + }); + } + } + + function onError(event) { + if (scope.onError) { + scope.$apply(function(){ + // callback + scope.onError({event:event}); + }); + } + } + + function onUpdate(event) { + if (scope.onUpdate) { + scope.$apply(function(){ + // callback + scope.onUpdate({event: event}); + }); + } + } + + function initDatePicker() { + // Open the datepicker and add a changeDate eventlistener + element + .datetimepicker(scope.options) + .on("dp.hide", onHide) + .on("dp.show", onShow) + .on("dp.change", onChange) + .on("dp.error", onError) + .on("dp.update", onUpdate); + } + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-date-time-picker.html', + scope: { + options: "=", + onHide: "&", + onShow: "&", + onChange: "&", + onError: "&", + onUpdate: "&" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbDateTimePicker', DateTimePickerDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js new file mode 100644 index 0000000000..f969864ba1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js @@ -0,0 +1,196 @@ +/** + * @ngdoc service + * @name umbraco.resources.templateResource + * @description Loads in data for templates + **/ +function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { + + return { + + /** + * @ngdoc method + * @name umbraco.resources.templateResource#getById + * @methodOf umbraco.resources.templateResource + * + * @description + * Gets a template item with a given id + * + * ##usage + *
+         * templateResource.getById(1234)
+         *    .then(function(template) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {Int} id id of template to retrieve + * @returns {Promise} resourcePromise object. + * + */ + getById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateApiBaseUrl", + "GetById", + [{ id: id }])), + "Failed to retrieve data for template id " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.templateResource#getByAlias + * @methodOf umbraco.resources.templateResource + * + * @description + * Gets a template item with a given alias + * + * ##usage + *
+         * templateResource.getByAlias("upload")
+         *    .then(function(template) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {String} alias Alias of template to retrieve + * @returns {Promise} resourcePromise object. + * + */ + getByAlias: function (alias) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateApiBaseUrl", + "GetByAlias", + [{ alias: alias }])), + "Failed to retrieve data for template with alias: " + alias); + }, + + /** + * @ngdoc method + * @name umbraco.resources.templateResource#getAll + * @methodOf umbraco.resources.templateResource + * + * @description + * Gets all templates + * + * ##usage + *
+         * templateResource.getAll()
+         *    .then(function(templates) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object. + * + */ + getAll: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateApiBaseUrl", + "GetAll")), + "Failed to retrieve data"); + }, + + + /** + * @ngdoc method + * @name umbraco.resources.templateResource#getScaffold + * @methodOf umbraco.resources.templateResource + * + * @description + * Returns a scaffold of an empty template item + * + * The scaffold is used to build editors for templates that has not yet been populated with data. + * + * ##usage + *
+         * templateResource.getScaffold()
+         *    .then(function(template) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object containing the template scaffold. + * + */ + getScaffold: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateApiBaseUrl", + "GetScaffold", + [{ id: id }] )), + "Failed to retrieve data for empty template"); + }, + + /** + * @ngdoc method + * @name umbraco.resources.templateResource#deleteById + * @methodOf umbraco.resources.templateResource + * + * @description + * Deletes a template with a given id + * + * ##usage + *
+         * templateResource.deleteById(1234)
+         *    .then(function() {
+         *        alert('its gone!');
+         *    });
+         * 
+ * + * @param {Int} id id of template to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function(id) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "templateApiBaseUrl", + "DeleteById", + [{ id: id }])), + "Failed to delete item " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.templateResource#save + * @methodOf umbraco.resources.templateResource + * + * @description + * Saves or update a template + * + * ##usage + *
+         * templateResource.save(template)
+         *    .then(function(template) {
+         *        alert('its saved!');
+         *    });
+         * 
+ * + * @param {Object} template object to save + * @returns {Promise} resourcePromise object. + * + */ + save: function (template) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "templateApiBaseUrl", + "PostSave"), + template), + "Failed to save data for template id " + template.id); + } + }; +} + +angular.module("umbraco.resources").factory("templateResource", templateResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/templatequery.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/templatequery.resource.js new file mode 100644 index 0000000000..04569fccff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/templatequery.resource.js @@ -0,0 +1,151 @@ +/** + * @ngdoc service + * @name umbraco.resources.templateQueryResource + * @function + * + * @description + * Used by the query builder + */ +(function () { + 'use strict'; + + function templateQueryResource($http, umbRequestHelper) { + + /** + * @ngdoc function + * @name umbraco.resources.templateQueryResource#getAllowedProperties + * @methodOf umbraco.resources.templateQueryResource + * @function + * + * @description + * Called to get allowed properties + * ##usage + *
+         * templateQueryResource.getAllowedProperties()
+         *    .then(function(response) {
+         *
+         *    });
+         * 
+ */ + function getAllowedProperties() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateQueryApiBaseUrl", + "GetAllowedProperties")), + 'Failed to retrieve properties'); + } + + /** + * @ngdoc function + * @name umbraco.resources.templateQueryResource#getContentTypes + * @methodOf umbraco.resources.templateQueryResource + * @function + * + * @description + * Called to get content types + * ##usage + *
+         * templateQueryResource.getContentTypes()
+         *    .then(function(response) {
+         *
+         *    });
+         * 
+ */ + function getContentTypes() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateQueryApiBaseUrl", + "GetContentTypes")), + 'Failed to retrieve content types'); + } + + /** + * @ngdoc function + * @name umbraco.resources.templateQueryResource#getFilterConditions + * @methodOf umbraco.resources.templateQueryResource + * @function + * + * @description + * Called to the filter conditions + * ##usage + *
+         * templateQueryResource.getFilterConditions()
+         *    .then(function(response) {
+         *
+         *    });
+         * 
+ */ + function getFilterConditions() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "templateQueryApiBaseUrl", + "GetFilterConditions")), + 'Failed to retrieve filter conditions'); + } + + /** + * @ngdoc function + * @name umbraco.resources.templateQueryResource#postTemplateQuery + * @methodOf umbraco.resources.templateQueryResource + * @function + * + * @description + * Called to get content types + * ##usage + *
+         * var query = {
+         *     contentType: {
+         *         name: "Everything"
+         *      },
+         *      source: {
+         *          name: "My website"
+         *      },
+         *      filters: [
+         *          {
+         *              property: undefined,
+         *              operator: undefined
+         *          }
+         *      ],
+         *      sort: {
+         *          property: {
+         *              alias: "",
+         *              name: "",
+         *          },
+         *          direction: "ascending"
+         *      }
+         *  };
+         * 
+         * templateQueryResource.postTemplateQuery(query)
+         *    .then(function(response) {
+         *
+         *    });
+         * 
+ * @param {object} query Query to build result + */ + function postTemplateQuery(query) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "templateQueryApiBaseUrl", + "PostTemplateQuery"), + query), + 'Failed to retrieve query'); + } + + var resource = { + getAllowedProperties: getAllowedProperties, + getContentTypes: getContentTypes, + getFilterConditions: getFilterConditions, + postTemplateQuery: postTemplateQuery + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('templateQueryResource', templateQueryResource); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 2b53fbddfc..c988f9edbb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -111,14 +111,17 @@ @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; @import "components/umb-iconpicker.less"; +@import "components/umb-insert-code-box.less"; @import "components/umb-packages.less"; @import "components/umb-package-local-install.less"; @import "components/umb-lightbox.less"; @import "components/umb-avatar.less"; @import "components/umb-progress-bar.less"; +@import "components/umb-querybuilder.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; +@import "components/buttons/umb-era-button.less"; @import "components/notifications/umb-notifications.less"; @import "components/umb-file-dropzone.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less index 0b063d094d..4d5fd90fad 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less @@ -8,3 +8,25 @@ right: 0; left: auto; } + +// hack for umb-era-button +.umb-era-button-group { + + display: flex; + + .umb-era-button:first-child { + padding-right: 15px; + border-radius: 3px 0 0 3px; + } + + .umb-era-button.umb-button-group__toggle { + padding-right: 10px; + padding-left: 10px; + border-radius: 0 3px 3px 0; + } + + .umb-era-button.umb-button-group__toggle .caret { + margin: 0; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-era-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-era-button.less new file mode 100644 index 0000000000..8087a601bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-era-button.less @@ -0,0 +1,104 @@ +.umb-era-button { + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + //font-weight: bold; + height: 38px; + line-height: 1; + max-width: 100%; + padding: 0 18px; + color: #202129; + background-color: #edeeee; + text-decoration: none !important; + user-select: none; + white-space: nowrap; + overflow: hidden; + border-radius: 3px; + border: 0 none; + box-sizing: border-box; + cursor: pointer; + transition: background-color 80ms ease, color 80ms ease; +} + + +.umb-era-button:hover, +.umb-era-button:active { + color: #484848; + background-color: #e1e2e2; + outline: none; + text-decoration: none; +} + + +.umb-era-button:focus { + outline: none; +} + +.umb-era-button.-blue { + background: @blue; + color: white; +} + +.umb-era-button.-blue:hover { + background-color: @blueDark; +} + +.umb-era-button.-red { + background: @btnDangerBackground; + color: white; +} + +.umb-era-button.-red:hover { + background-color: darken(@btnDangerBackground, 5%); +} + +.umb-era-button.-link { + padding: 0; + background: transparent; +} + +.umb-era-button.-link:hover { + background-color: transparent; + opacity: .6; +} + +.umb-era-button.-inactive { + cursor: not-allowed; + color: #BBB; + background: #EAE7E7; +} + +.umb-era-button.-inactive:hover { + color: #BBB; + background: #EAE7E7; +} + + +.umb-era-button.-full-width { + display: block; + width: 100%; +} + +.umb-era-button.umb-button--s { + height: 30px; + font-size: 13px; +} + +.umb-era-button.-white { + background-color: @white; + + &:hover { + opacity: .9; + } +} + +.umb-era-button.-text-black { + color: @black; +} + +/* icons */ + +.umb-era-button i { + margin-right: 5px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less new file mode 100644 index 0000000000..9d722f82bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-insert-code-box.less @@ -0,0 +1,47 @@ +.umb-insert-code-boxes { + display: flex; + flex-direction: column; +} + +.umb-insert-code-box { + border: 2px solid @grayLighter; + padding: 15px 20px; + margin-bottom: 10px; + border-radius: 3px; +} + +.umb-insert-code-box:hover, +.umb-insert-code-box.-selected { + border-color: @blue; + cursor: pointer; +} + +.umb-insert-code-box__title { + font-size: 15px; + margin-bottom: 5px; + font-weight: bold; + color: @black; +} + +.umb-insert-code-box__description { + font-size: 11px; +} + +.umb-insert-code-box__check { + width: 18px; + height: 18px; + background: @grayLighter; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + float: left; + margin-right: 5px; + margin-top: 1px; +} + +.umb-insert-code-box__check--checked { + background: @green; + color: @white; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index f14df918c0..a043cd5571 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -248,95 +248,6 @@ color: black; } -/* umb-buttons-era */ -.umb-era-button { - display: flex; - justify-content: center; - align-items: center; - - font-size: 14px; - font-weight: bold; - - height: 38px; - line-height: 1; - - max-width: 100%; - padding: 0 18px; - - color: #484848; - background-color: #e0e0e0; - - text-decoration: none !important; - user-select: none; - - white-space: nowrap; - overflow: hidden; - - border-radius: 3px; - border: 0 none; - box-sizing: border-box; - - cursor: pointer; - - transition: background-color 80ms ease, color 80ms ease; -} - - -.umb-era-button:hover, -.umb-era-button:active { - color: #484848; - background-color: #d3d3d3; - outline: none; - text-decoration: none; -} - - -.umb-era-button:focus { - outline: none; -} - -.umb-era-button.-blue { - background: @blue; - color: white; -} - -.umb-era-button.-blue:hover { - background-color: @blueDark; -} - -.umb-era-button.-link { - padding: 0; - background: transparent; -} - -.umb-era-button.-link:hover { - background-color: transparent; - opacity: .6; -} - -.umb-era-button.-inactive { - cursor: not-allowed; - color: #BBB; - background: #EAE7E7; -} - -.umb-era-button.-inactive:hover { - color: #BBB; - background: #EAE7E7; -} - - -.umb-era-button.-full-width { - display: block; - width: 100%; -} - -.umb-era-button.umb-button--s { - height: 30px; - font-size: 13px; -} - - /* CATEGORIES */ .umb-packages-categories { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-querybuilder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-querybuilder.less new file mode 100644 index 0000000000..cf72c04b1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-querybuilder.less @@ -0,0 +1,41 @@ +.umb-querybuilder .row { + font-size: 12px; + line-height: 12px +} + +.umb-querybuilder .row a.btn { + padding: 5px 8px; + margin: 0 5px; + font-weight: bold; + background-color: #f3f9ff; + border: 1px solid #bfdff9; + border-radius: 3px; + text-align: center; + display: inline-block; + +} + +.umb-querybuilder .row a.btn:hover { + background: #e8f1fb; + text-decoration: none; +} + +.umb-querybuilder .row > div { + padding: 20px 0; + border-bottom: 1px solid @grayLighter; +} + +.umb-querybuilder .datepicker input { + width: 90px; +} + +.umb-querybuilder .query-items { + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.umb-querybuilder .query-items > * { + flex: 0 1 auto; + margin: 5px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 716cb40256..dc1212f33d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -197,7 +197,7 @@ pre { font-size: @baseFontSize - 1; // 14px to 13px color: @grayDark; line-height: @baseLineHeight; - white-space: pre; // 1 + white-space: pre-line; // 1 overflow-x: auto; // 1 background-color: #f5f5f5; border: 1px solid #ccc; // fallback for IE7-8 diff --git a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less index 6e82d424bb..d0427d96f7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less +++ b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less @@ -95,111 +95,6 @@ } -/* umb-buttons-era */ -.umb-era-button { - display: flex; - justify-content: center; - align-items: center; - - font-size: 14px; - font-weight: bold; - - height: 40px; - line-height: 1; - - max-width: 100%; - padding: 0 15px; - - color: #484848; - background-color: #e0e0e0; - - text-decoration: none !important; - user-select: none; - - white-space: nowrap; - overflow: hidden; - - border-radius: 3px; - border: 0 none; - box-sizing: border-box; - - cursor: pointer; - - transition: background-color 80ms ease, color 80ms ease, opacity 80ms ease; -} - - -.umb-era-button:hover, -.umb-era-button:active { - color: #484848; - background-color: #d3d3d3; - outline: none; - text-decoration: none; -} - - -.umb-era-button:focus { - outline: none; -} - -.umb-era-button.-blue { - background: @blue; - color: white; -} - -.umb-era-button.-blue:hover { - background-color: @blueDark; -} - -.umb-era-button.-red { - background: @btnDangerBackground; - color: white; -} - -.umb-era-button.-red:hover { - background-color: darken(@btnDangerBackground, 5%); -} - -.umb-era-button.-link { - padding: 0; - background: transparent; -} - -.umb-era-button.-link:hover { - background-color: transparent; - opacity: .6; -} - -.umb-era-button.-inactive { - cursor: not-allowed; - color: #BBB; - background: #EAE7E7; -} - -.umb-era-button.-inactive:hover { - color: #BBB; - background: #EAE7E7; -} - - -.umb-era-button.-full-width { - display: block; - width: 100%; -} - -.umb-era-button.-white { - background-color: @white; - - &:hover { - opacity: .9; - } -} - -.umb-era-button.-text-black { - color: @black; -} - - /* Spacing for boxes */ .umb-air { flex: 0 0 auto; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index c80f72921c..ded8411dea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -480,6 +480,14 @@ input.umb-panel-header-description { } } +.umb-panel-header-locked-description { + font-size: 12px; + margin-left: 2px; + margin-top: 3px; + height: 25px; + line-height: 25px; +} + .umb-editor-drawer-content { display: flex; align-items: center; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js new file mode 100644 index 0000000000..59a00a5797 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js @@ -0,0 +1,136 @@ +(function () { + "use strict"; + + function InsertOverlayController($scope) { + + var vm = this; + + if(!$scope.model.title) { + $scope.model.title = "Insert"; + } + + if(!$scope.model.subtitle) { + $scope.model.subtitle = "Choose what to insert into your template"; + } + + vm.openMacroPicker = openMacroPicker; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openPartialOverlay = openPartialOverlay; + + function openMacroPicker() { + + vm.macroPickerOverlay = { + view: "macropicker", + title: "Insert macro", + dialogData: {}, + show: true, + submit: function(model) { + + $scope.model.insert = { + "type": "macro", + "macroParams": model.macroParams, + "selectedMacro": model.selectedMacro + }; + + $scope.model.submit($scope.model); + + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + + } + }; + + } + + function openPageFieldOverlay() { + vm.pageFieldOverlay = { + title: "Insert value", + description: "Select a value from the currentpage", + submitButtonLabel: "Insert", + closeButtonlabel: "Cancel", + view: "insertfield", + show: true, + submit: function(model) { + + $scope.model.insert = { + "type": "umbracoField", + "umbracoField": model.umbracoField + }; + + $scope.model.submit($scope.model); + + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + }, + close: function (model) { + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + } + }; + } + + function openDictionaryItemOverlay() { + + vm.dictionaryItemOverlay = { + view: "treepicker", + section: "settings", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: "Insert dictionary item", + show: true, + select: function(node){ + + $scope.model.insert = { + "type": "dictionary", + "node": node + }; + + $scope.model.submit($scope.model); + + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + }, + + close: function(model) { + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + } + }; + } + + function openPartialOverlay() { + vm.partialItemOverlay = { + view: "treepicker", + section: "settings", + treeAlias: "partialViews", + entityType: "partialView", + multiPicker: false, + show: true, + title: "Insert partial view", + + select: function(node){ + + $scope.model.insert = { + "type": "partial", + "node": node + }; + + $scope.model.submit($scope.model); + + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + }, + + close: function (model) { + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + } + }; + } + + } + + angular.module("umbraco").controller("Umbraco.Overlays.InsertOverlay", InsertOverlayController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html new file mode 100644 index 0000000000..4b92ea86ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html @@ -0,0 +1,61 @@ +
+ +
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ +
+ + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insertfield/insertfield.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insertfield/insertfield.controller.js new file mode 100644 index 0000000000..141e7184f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insertfield/insertfield.controller.js @@ -0,0 +1,142 @@ +(function () { + "use strict"; + + function InsertFieldController($scope, $http, contentTypeResource) { + + var vm = this; + + vm.field; + vm.altField; + vm.altText; + vm.insertBefore; + vm.insertAfter; + vm.recursive = false; + vm.properties = []; + vm.date = false; + vm.dateTime = false; + vm.dateTimeSeparator = ""; + vm.casingUpper = false; + vm.casingLower = false; + vm.encodeHtml = false; + vm.encodeUrl = false; + vm.convertLinebreaks = false; + vm.removeParagraphTags = false; + + vm.showAltField = false; + vm.showAltText = false; + + vm.setDateOption = setDateOption; + vm.setCasingOption = setCasingOption; + vm.setEncodingOption = setEncodingOption; + vm.generateOutputSample = generateOutputSample; + + function onInit() { + + // set default title + if(!$scope.model.title) { + $scope.model.title = "Insert value"; + } + + // Load all fields + contentTypeResource.getAllPropertyTypeAliases().then(function (array) { + vm.properties = array; + }); + + } + + // date formatting + function setDateOption(option) { + + if (option === 'date') { + if(vm.date) { + vm.date = false; + } else { + vm.date = true; + vm.dateTime = false; + } + } + + if (option === 'dateWithTime') { + if(vm.dateTime) { + vm.dateTime = false; + } else { + vm.date = false; + vm.dateTime = true; + } + } + + } + + // casing formatting + function setCasingOption(option) { + if (option === 'uppercase') { + if(vm.casingUpper) { + vm.casingUpper = false; + } else { + vm.casingUpper = true; + vm.casingLower = false; + } + } + + if (option === 'lowercase') { + if(vm.casingLower) { + vm.casingLower = false; + } else { + vm.casingUpper = false; + vm.casingLower = true; + } + } + } + + // encoding formatting + function setEncodingOption(option) { + if (option === 'html') { + if(vm.encodeHtml) { + vm.encodeHtml = false; + } else { + vm.encodeHtml = true; + vm.encodeUrl = false; + } + } + + if (option === 'url') { + if (vm.encodeUrl) { + vm.encodeUrl = false; + } else { + vm.encodeHtml = false; + vm.encodeUrl = true; + } + } + } + + function generateOutputSample() { + + var pageField = (vm.field !== undefined ? '@Umbraco.Field("' + vm.field + '"' : "") + + (vm.altField !== undefined ? ', altFieldAlias:"' + vm.altField + '"' : "") + + (vm.altText !== undefined ? ', altText:"' + vm.altText + '"' : "") + + (vm.insertBefore !== undefined ? ', insertBefore:"' + vm.insertBefore + '"' : "") + + (vm.insertAfter !== undefined ? ', insertAfter:"' + vm.insertAfter + '"' : "") + + (vm.recursive !== false ? ', recursive: ' + vm.recursive : "") + + (vm.date !== false ? ', formatAsDate: ' + vm.date : "") + + (vm.dateTime !== false ? ', formatAsDateWithTimeSeparator:"' + vm.dateTimeSeparator + '"' : "") + + (vm.casingUpper !== false ? ', casing: ' + "RenderFieldCaseType.Upper" : "") + + (vm.casingLower !== false ? ', casing: ' + "RenderFieldCaseType.Lower" : "") + + (vm.encodeHtml !== false ? ', encoding: ' + "RenderFieldEncodingType.Html" : "") + + (vm.encodeUrl !== false ? ', encoding: ' + "RenderFieldEncodingType.Url" : "") + + (vm.convertLinebreaks !== false ? ', convertLineBreaks: ' + "true" : "") + + (vm.removeParagraphTags !== false ? ', removeParagraphTags: ' + "true": "") + + (vm.field ? ')' : ""); + + $scope.model.umbracoField = pageField; + + return pageField; + + } + + onInit(); + + + } + + angular.module("umbraco").controller("Umbraco.Overlays.InsertFieldController", InsertFieldController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insertfield/insertfield.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insertfield/insertfield.html new file mode 100644 index 0000000000..161be5060a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insertfield/insertfield.html @@ -0,0 +1,185 @@ +
+ + +
+
+ +
+ +
+
+
+ +
+ + +
+ + +
+ + Add fallback field + +
+
+ +
+ +
+
+
+
+ + +
+
+ + Add default value +
+ +
+
+ +
+ +
+
+
+
+ + +
+
+
+ + + Yes, make it recursive +
+
+
+ +
Format and encoding
+ + +
+
+
+
+ +
+ Date only + Date and time + +
+
+
+ + +
+
+
+
+ +
+ Uppercase + Lowercase +
+
+
+ + +
+
+
+
+ +
+ HTML + URL +
+
+
+ +
Modify output
+ + +
+
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+
+
+ +
+ + Yes, convert line breaks +
+
+
+ + +
+
+
+
+ +
+ + Yes, remove paragraph tags +
+
+
+ + +
+
+
+ +
{{ vm.generateOutputSample() }}
+
+
+
+ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js index e46bd26513..69f9851926 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js @@ -7,15 +7,19 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi $scope.macros = []; $scope.model.selectedMacro = null; - $scope.wizardStep = "macroSelect"; $scope.model.macroParams = []; + + $scope.wizardStep = "macroSelect"; $scope.noMacroParams = false; - $scope.changeMacro = function() { + $scope.selectMacro = function (macro) { + + $scope.model.selectedMacro = macro; + if ($scope.wizardStep === "macroSelect") { editParams(); } else { - submitForm(); + $scope.model.submit($scope.model); } }; @@ -25,12 +29,15 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi macroResource.getMacroParameters($scope.model.selectedMacro.id) .then(function (data) { + + //go to next page if there are params otherwise we can just exit if (!angular.isArray(data) || data.length === 0) { - $scope.noMacroParams = true; + $scope.model.submit($scope.model); } else { + $scope.wizardStep = "paramSelect"; $scope.model.macroParams = data; @@ -83,6 +90,10 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) .then(function (data) { + if (angular.isArray(data) && data.length == 0) { + $scope.nomacros = true; + } + //if 'allowedMacros' is specified, we need to filter if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { $scope.macros = _.filter(data, function(d) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html index 318fed41a8..418e6b5ca6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html @@ -2,17 +2,39 @@
- - - - +
+ + + + + + + + There are no macros available to insert + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js new file mode 100644 index 0000000000..3939882969 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js @@ -0,0 +1,180 @@ +(function () { + "use strict"; + + function QueryBuilderOverlayController($scope, templateQueryResource) { + + var vm = this; + + vm.properties = []; + vm.contentTypes = []; + vm.conditions = []; + + vm.datePickerConfig = { + pickDate: true, + pickTime: false, + format: "YYYY-MM-DD" + }; + + vm.query = { + contentType: { + name: "Everything" + }, + source: { + name: "My website" + }, + filters: [ + { + property: undefined, + operator: undefined + } + ], + sort: { + property: { + alias: "", + name: "", + }, + direction: "ascending" + } + }; + + vm.chooseSource = chooseSource; + vm.getPropertyOperators = getPropertyOperators; + vm.addFilter = addFilter; + vm.trashFilter = trashFilter; + vm.changeSortOrder = changeSortOrder; + vm.setSortProperty = setSortProperty; + vm.setContentType = setContentType; + vm.setFilterProperty = setFilterProperty; + vm.setFilterTerm = setFilterTerm; + vm.changeConstraintValue = changeConstraintValue; + vm.datePickerChange = datePickerChange; + + function onInit() { + + templateQueryResource.getAllowedProperties() + .then(function (properties) { + vm.properties = properties; + }); + + templateQueryResource.getContentTypes() + .then(function (contentTypes) { + vm.contentTypes = contentTypes; + }); + + templateQueryResource.getFilterConditions() + .then(function (conditions) { + vm.conditions = conditions; + }); + + throttledFunc(); + + } + + function chooseSource(query) { + vm.contentPickerOverlay = { + view: "contentpicker", + show: true, + submitButtonLabel: "Insert", + submit: function(model) { + + var selectedNodeId = model.selection[0].id; + var selectedNodeName = model.selection[0].name; + + if (selectedNodeId > 0) { + query.source = { id: selectedNodeId, name: selectedNodeName }; + } else { + query.source.name = "My website"; + delete query.source.id; + } + + throttledFunc(); + + vm.contentPickerOverlay.show = false; + vm.contentPickerOverlay = null; + }, + close: function(oldModel) { + vm.contentPickerOverlay.show = false; + vm.contentPickerOverlay = null; + } + }; + } + + function getPropertyOperators(property) { + var conditions = _.filter(vm.conditions, function (condition) { + var index = condition.appliesTo.indexOf(property.type); + return index >= 0; + }); + return conditions; + } + + function addFilter(query) { + query.filters.push({}); + } + + function trashFilter(query) { + query.filters.splice(query, 1); + } + + function changeSortOrder(query) { + if (query.sort.direction === "ascending") { + query.sort.direction = "descending"; + } else { + query.sort.direction = "ascending"; + } + throttledFunc(); + } + + function setSortProperty(query, property) { + query.sort.property = property; + if (property.type === "datetime") { + query.sort.direction = "descending"; + } else { + query.sort.direction = "ascending"; + } + throttledFunc(); + } + + function setContentType(contentType) { + vm.query.contentType = contentType; + throttledFunc(); + } + + function setFilterProperty(filter, property) { + filter.property = property; + filter.term = {}; + filter.constraintValue = ""; + } + + function setFilterTerm(filter, term) { + filter.term = term; + if(filter.constraintValue) { + throttledFunc(); + } + } + + function changeConstraintValue() { + throttledFunc(); + } + + function datePickerChange(event, filter) { + if(event.date && event.date.isValid()) { + filter.constraintValue = event.date.format(vm.datePickerConfig.format); + throttledFunc(); + } + } + + var throttledFunc = _.throttle(function () { + + templateQueryResource.postTemplateQuery(vm.query) + .then(function (response) { + $scope.model.result = response; + }); + + }, 200); + + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Overlays.QueryBuilderController", QueryBuilderOverlayController); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html new file mode 100644 index 0000000000..e9be97b7df --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html @@ -0,0 +1,149 @@ +
+ +
+ +
+ + +
+ + + where + + + and + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
{{model.result.resultCount}} items, returned in {{model.result.executionTime}} ms
+ + + +
+{{model.result.queryExpression}}
+        
+ +
+ + + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.controller.js new file mode 100644 index 0000000000..d8cc93c1b8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.controller.js @@ -0,0 +1,35 @@ +(function () { + "use strict"; + + function TemplateSectionsOverlayController($scope) { + + var vm = this; + + $scope.model.mandatoryRenderSection = false; + + if(!$scope.model.title) { + $scope.model.title = "Sections"; + } + + vm.select = select; + + function onInit() { + + if($scope.model.hasMaster) { + $scope.model.insertType = 'addSection'; + } else { + $scope.model.insertType = 'renderBody'; + } + + } + + function select(type) { + $scope.model.insertType = type; + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Overlays.TemplateSectionsOverlay", TemplateSectionsOverlayController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html new file mode 100644 index 0000000000..1a11d6a46e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html @@ -0,0 +1,71 @@ +
+ +
+
+ +
Render child template
+ +
+ Renders the contents of a child template, by inserting a + @RenderBody() placeholder. +
+
+ +
+ +
+
Render a named section
+
+ Renders a named area of a child template, by insert a @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. +
+ +
+
+ + + +
+ Set the name of the section to render in this area of the template +
+
+ +
+ +
+ If mandatory, the child template must contain a @section definition, otherwise an error is shown. +
+
+
+ +
+ +
+ +
+
Define a named section
+
+ Defines a part of your template as a named section by wrapping it in + a @section { ... }. This can be rendered in a + specific area of the master of this template, by using @RenderSection. +
+ +
+
+ + + +
+ Give the section a name +
+
+
+ +
+ + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index 8e87a2daff..e69b5ee3ce 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -38,6 +38,11 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } } } + + // Search is only working for content, media and member section so we will remove it from everything else + if($scope.section === "content" || $scope.section === "media" || $scope.section === "member" ) { + $scope.enableSearh = true; + } //create the custom query string param for this tree $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; @@ -206,10 +211,14 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", //This is a tree node, so we don't have an entity to pass in, it will need to be looked up //from the server in this method. - select(args.node.name, args.node.id); - - //toggle checked state - args.node.selected = args.node.selected === true ? false : true; + if($scope.model.select){ + $scope.model.select(args.node) + }else{ + select(args.node.name, args.node.id); + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html index 4e9f503013..b009b4d45f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html @@ -2,6 +2,7 @@
-
+
@@ -26,6 +26,7 @@ ng-model="name" ng-class="{'name-is-empty': $parent.name===null || $parent.name===''}" umb-auto-focus + val-server-field="Name" required /> @@ -43,9 +44,11 @@ class="umb-panel-header-description" localize="placeholder" placeholder="@placeholders_enterDescription" - ng-if="!hideDescription" + ng-if="!hideDescription && !descriptionLocked" ng-model="$parent.description" /> +
{{ description }}
+
+ + + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index 75969730a0..c6de405ff6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -106,7 +106,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", var activeNode = appState.getTreeState("selectedNode"); //we need to do a double sync here: first sync to the moved content - but don't activate the node, - //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + //then sync to the currently edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js new file mode 100644 index 0000000000..8e2b8337ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -0,0 +1,549 @@ +(function () { + "use strict"; + + function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper) { + + var vm = this; + var oldMasterTemplateAlias = null; + var localizeSaving = localizationService.localize("general_saving"); + + vm.page = {}; + vm.page.loading = true; + vm.templates = []; + + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + + vm.save = function () { + vm.page.saveButtonState = "busy"; + + vm.template.content = vm.editor.getValue(); + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: templateResource.save, + scope: $scope, + content: vm.template, + //We do not redirect on failure for templates - this is because it is not possible to actually save the doc + // type when server side validation fails - as opposed to content where we are capable of saving the content + // item if server side validation fails + redirectOnFailure: false, + rebindCallback: function (orignal, saved) {} + }).then(function (saved) { + + notificationsService.success("Template saved"); + vm.page.saveButtonState = "success"; + vm.template = saved; + + //sync state + editorState.set(vm.template); + + // sync tree + // if master template alias has changed move the node to it's new location + if(oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { + + // move node to new location in tree + //first we need to remove the node that we're working on + treeService.removeNode(vm.page.menu.currentNode); + + // update stored alias to the new one so the node won't move again unless the alias is changed again + oldMasterTemplateAlias = vm.template.masterTemplateAlias; + + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true, activate: true }).then(function (args) { + vm.page.menu.currentNode = args.node; + }); + + } else { + + // normal tree sync + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + + } + + // clear $dirty state on form + setFormState("pristine"); + + + }, function (err) { + + vm.page.saveButtonState = "error"; + + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + + }); + + }; + + vm.init = function () { + + //we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css"); + + //load templates - used in the master template picker + templateResource.getAll() + .then(function(templates) { + vm.templates = templates; + }); + + if($routeParams.create){ + + templateResource.getScaffold(($routeParams.id)).then(function (template) { + vm.ready(template); + }); + + }else{ + + templateResource.getById($routeParams.id).then(function(template){ + vm.ready(template); + }); + + } + + }; + + + vm.ready = function(template){ + vm.page.loading = false; + vm.template = template; + + //sync state + editorState.set(vm.template); + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + + // save state of master template to use for comparison when syncing the tree on save + oldMasterTemplateAlias = angular.copy(template.masterTemplateAlias); + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px' + }, + onLoad: function(_editor) { + vm.editor = _editor; + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if(!$routeParams.create) { + $timeout(function(){ + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + //change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + } + } + + }; + + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; + vm.openSectionsOverlay = openSectionsOverlay; + vm.openPartialOverlay = openPartialOverlay; + vm.openMasterTemplateOverlay = openMasterTemplateOverlay; + vm.selectMasterTemplate = selectMasterTemplate; + vm.getMasterTemplateName = getMasterTemplateName; + vm.removeMasterTemplate = removeMasterTemplate; + + function openInsertOverlay() { + + vm.insertOverlay = { + view: "insert", + hideSubmitButton: true, + show: true, + submit: function(model) { + + switch(model.insert.type) { + case "macro": + + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + + case "dictionary": + //crappy hack due to dictionary items not in umbracoNode table + var code = "@Umbraco.GetDictionaryValue(\"" + model.insert.node.name + "\")"; + insert(code); + break; + + case "partial": + //crappy hack due to dictionary items not in umbracoNode table + var nodeNameWithPath = model.insert.node.id.replace(".cshtml", ""); + var code = "@Html.Partial(\"" + nodeNameWithPath + "\")"; + insert(code); + break; + + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + + vm.insertOverlay.show = false; + vm.insertOverlay = null; + + }, + close: function(oldModel) { + // close the dialog + vm.insertOverlay.show = false; + vm.insertOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + + } + + + function openMacroOverlay() { + + vm.macroPickerOverlay = { + view: "macropicker", + dialogData: {}, + show: true, + title: "Insert macro", + submit: function (model) { + + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + + }, + close: function(oldModel) { + // close the dialog + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + + + function openPageFieldOverlay() { + vm.pageFieldOverlay = { + submitButtonLabel: "Insert", + closeButtonlabel: "Cancel", + view: "insertfield", + show: true, + submit: function (model) { + insert(model.umbracoField); + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + }, + close: function (model) { + // close the dialog + vm.pageFieldOverlay.show = false; + vm.pageFieldOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + + + function openDictionaryItemOverlay() { + vm.dictionaryItemOverlay = { + view: "treepicker", + section: "settings", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + show: true, + title: "Insert dictionary item", + select: function(node){ + //crappy hack due to dictionary items not in umbracoNode table + var code = "@Umbraco.GetDictionaryValue(\"" + node.name + "\")"; + insert(code); + + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + }, + close: function (model) { + // close dialog + vm.dictionaryItemOverlay.show = false; + vm.dictionaryItemOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + + function openPartialOverlay() { + vm.partialItemOverlay = { + view: "treepicker", + section: "settings", + treeAlias: "partialViews", + entityType: "partialView", + multiPicker: false, + show: true, + title: "Insert Partial view", + select: function(node){ + //crappy hack due to dictionary items not in umbracoNode table + var nodeNameWithPath = node.id.replace(".cshtml", ""); + var code = "@Html.Partial(\"" + nodeNameWithPath + "\")"; + insert(code); + + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + }, + close: function (model) { + // close dialog + vm.partialItemOverlay.show = false; + vm.partialItemOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + + function openQueryBuilderOverlay() { + vm.queryBuilderOverlay = { + view: "querybuilder", + show: true, + title: "Query for content", + + submit: function (model) { + + var code = "\n@{\n" + "\tvar selection = " + model.result.queryExpression + ";\n}\n"; + code += "
    \n" + + "\t@foreach(var item in selection){\n" + + "\t\t
  • \n" + + "\t\t\t@item.Name\n" + + "\t\t
  • \n" + + "\t}\n" + + "
\n\n"; + + insert(code); + + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + }, + + close: function (model) { + // close dialog + vm.queryBuilderOverlay.show = false; + vm.queryBuilderOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + } + + + function openSectionsOverlay() { + + vm.sectionsOverlay = { + view: "templatesections", + hasMaster: vm.template.masterTemplateAlias, + submitButtonLabel: "Insert", + show: true, + submit: function(model) { + + if (model.insertType === 'renderBody') { + insert("@RenderBody()"); + } + + if (model.insertType === 'renderSection') { + insert("@RenderSection(\"" + model.renderSectionName + "\", " + model.mandatoryRenderSection + ")"); + } + + if (model.insertType === 'addSection') { + wrap("@section " + model.sectionName + "\r\n{\r\n\r\n\t{0}\r\n\r\n}\r\n"); + } + + vm.sectionsOverlay.show = false; + vm.sectionsOverlay = null; + + }, + close: function(model) { + // close dialog + vm.sectionsOverlay.show = false; + vm.sectionsOverlay = null; + // focus editor + vm.editor.focus(); + } + } + } + + function openMasterTemplateOverlay() { + + // make collection of available master templates + var availableMasterTemplates = []; + + // filter out the current template and the selected master template + angular.forEach(vm.templates, function(template){ + if(template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { + availableMasterTemplates.push(template); + } + }); + + vm.masterTemplateOverlay = { + view: "itempicker", + title: "Choose master template", + availableItems: availableMasterTemplates, + show: true, + submit: function(model) { + + var template = model.selectedItem; + + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + + vm.masterTemplateOverlay.show = false; + vm.masterTemplateOverlay = null; + }, + close: function(oldModel) { + // close dialog + vm.masterTemplateOverlay.show = false; + vm.masterTemplateOverlay = null; + // focus editor + vm.editor.focus(); + } + }; + + } + + function selectMasterTemplate(template) { + + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + + } + + function getMasterTemplateName(masterTemplateAlias, templates) { + + if(masterTemplateAlias) { + + var templateName = ""; + + angular.forEach(templates, function(template){ + if(template.alias === masterTemplateAlias) { + templateName = template.name; + } + }); + + return templateName; + + } else { + return "No master"; + } + + } + + function removeMasterTemplate() { + + vm.template.masterTemplateAlias = null; + + // call set layout with no paramters to set layout to null + setLayout(); + + } + + function setLayout(templatePath){ + + var templateCode = vm.editor.getValue(); + var newValue = templatePath; + var layoutDefRegex = new RegExp("(@{[\\s\\S]*?Layout\\s*?=\\s*?)(\"[^\"]*?\"|null)(;[\\s\\S]*?})", "gi"); + + if (newValue !== undefined && newValue !== "") { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, "$1\"" + newValue + "\"$3"); + } else { + // Declaration doesn't exist, so prepend to start of doc + //TODO: Maybe insert at the cursor position, rather than just at the top of the doc? + templateCode = "@{\n\tLayout = \"" + newValue + "\";\n}\n" + templateCode; + } + } else { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, "$1null$3"); + } + } + + vm.editor.setValue(templateCode); + vm.editor.clearSelection(); + vm.editor.navigateFileStart(); + + vm.editor.focus(); + // set form state to $dirty + setFormState("dirty"); + + } + + + function insert(str) { + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + vm.editor.focus(); + + // set form state to $dirty + setFormState("dirty"); + } + + function wrap(str) { + + var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); + str = str.replace("{0}", selectedContent); + vm.editor.insert(str); + vm.editor.focus(); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if(state === "dirty") { + currentForm.$setDirty(); + } else if(state === "pristine") { + currentForm.$setPristine(); + } + } + + vm.init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Templates.EditController", TemplatesEditController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html new file mode 100644 index 0000000000..81dc9042b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -0,0 +1,174 @@ +
+ + + +
+ + + + + + + + + +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ + + + + + +
+ +
+ +
+
+ +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js new file mode 100644 index 0000000000..8208999536 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js @@ -0,0 +1,146 @@ +/// +/// +/// +/// + +(function() { + "use strict"; + + describe("templates editor controller", + function() { + + + var scope, + controllerFactory, + q, + ace, + controller, + nada = function() {}; + + // UNCOMMENT TO RUN WITH RESHARPERS TESTRUNNER FOR JS + //beforeEach(function() { + // angular.module('umbraco.filters', []); + // angular.module('umbraco.directives', []); + // angular.module('umbraco.resources', []); + // angular.module('umbraco.services', []); + // angular.module('umbraco.packages', []); + // angular.module('umbraco.views', []); + // angular.module('ngCookies', []); + // angular.module('ngSanitize', []); + // angular.module('ngMobile', []); + // angular.module('tmh.dynamicLocale', []); + // angular.module('ngFileUpload', []); + // angular.module('LocalStorageModule', []); + //}); + + beforeEach(module("umbraco")); + + beforeEach(inject(function($controller, $rootScope, $q) { + + controllerFactory = $controller; + scope = $rootScope.$new(); + q = $q; + + ace = { + on: function(){}, + navigateFileEnd: function() {}, + getCursorPosition: function() {}, + getValue: function() {}, + setValue: function() {}, + focus: function() {}, + clearSelection: function() {}, + navigateFileStart: function() {} + }; + + controller = createController(); + scope.$digest(); + controller.aceOption.onLoad(ace); + + })); + + function resolvedPromise(obj) { + return function() { + var def = q.defer(); + def.resolve(obj); + return def.promise; + } + } + + function createController() { + return controllerFactory("Umbraco.Editors.Templates.EditController", + { + $scope: scope, + $routeParams: {}, + templateResource: { + getById: resolvedPromise({}), + getAll: resolvedPromise({}) + }, + assetsService: { + loadCss: function() {} + }, + notificationsService: { + }, + editorState: { + set: function(){} + }, + navigationService: { + syncTree: resolvedPromise({}) + }, + appState: { + getSectionState : function() { return {}; } + }, + macroService: {}, + contentEditingHelper: {}, + localizationService: { + localize: resolvedPromise({}) + }, + angularHelper: { + getCurrentForm: function() { + return { + $setDirty: function() {}, + $setPristine: function() {} + } + } + } + }); + } + + it("has ace editor", function () { + expect(controller.editor).toBe(ace); + }); + + it("sets masterpage on template", function () { + controller.setLayout = function() {}; + + controller.openMasterTemplateOverlay(); + controller.masterTemplateOverlay.submit({ + selectedItem: { + alias: "NewMasterPage" + } + }); + expect(controller.template.masterTemplateAlias).toBe("NewMasterPage"); + }); + + it("changes layout value when masterpage is selected", function() { + var newTemplate; + ace.clearSelection = nada; + ace.navigateFileStart = nada; + ace.getValue = function () { + return "@{ Layout = null; }"; + } + ace.setValue = function (value) { + newTemplate = value; + } + + controller.openMasterTemplateOverlay(); + controller.masterTemplateOverlay.submit({ + selectedItem: { + alias: "NewMasterPage" + } + }); + expect(newTemplate).toBe("@{ Layout = \"NewMasterPage.cshtml\"; }"); + }); + + }); + +}()); diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index f7ac08d837..2021be1d9d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1025,17 +1025,48 @@ To manage your website, simply open the Umbraco back office and start adding con Preview Styles + Edit template + + Sections Insert content area Insert content area placeholder - Insert dictionary item - Insert Macro - Insert Umbraco page field + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template - Quick Guide to Umbraco template tags + + Query builder + items returned, in + + I want + from + where + and + everything + + Template + Choose type of content Choose a layout diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 199ffd9bed..c092811a08 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -332,6 +332,10 @@ namespace Umbraco.Web.Editors "tagApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllTags(null)) }, + { + "templateApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(0)) + }, { "memberTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetNodes("-1", null)) @@ -359,6 +363,10 @@ namespace Umbraco.Web.Editors { "healthCheckBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllHealthChecks()) + }, + { + "templateQueryApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.PostTemplateQuery(null)) } } }, diff --git a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs index f4350bc596..0910ec936e 100644 --- a/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeNotificationsController.cs @@ -1,3 +1,4 @@ +using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors @@ -8,6 +9,7 @@ namespace Umbraco.Web.Editors /// currently in the request. /// [AppendCurrentEventMessages] + [PrefixlessBodyModelValidator] public abstract class BackOfficeNotificationsController : UmbracoAuthorizedJsonController { protected BackOfficeNotificationsController() diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 45a2821130..d6e6cbccf4 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -106,16 +106,7 @@ namespace Umbraco.Web.Editors /// Returns the avilable compositions for this content type /// This has been wrapped in a dto instead of simple parameters to support having multiple parameters in post request body /// - /// - /// - /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out - /// along with any content types that have matching property types that are included in the filtered content types - /// - /// - /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. - /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot - /// be looked up via the db, they need to be passed in. - /// + /// /// [HttpPost] public HttpResponseMessage GetAvailableCompositeContentTypes(GetAvailableCompositionsFilter filter) diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 931d00968b..b3e306a48c 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] [EnableOverrideAuthorization] - public class DataTypeController : UmbracoAuthorizedJsonController + public class DataTypeController : BackOfficeNotificationsController { /// /// Gets data type by name diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs new file mode 100644 index 0000000000..40effc10fa --- /dev/null +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -0,0 +1,172 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using AutoMapper; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.Templates)] + public class TemplateController : BackOfficeNotificationsController + { + /// + /// Gets data type by alias + /// + /// + /// + public TemplateDisplay GetByAlias(string alias) + { + var template = Services.FileService.GetTemplate(alias); + return template == null ? null : Mapper.Map(template); + } + + /// + /// Get all templates + /// + /// + public IEnumerable GetAll() + { + return Services.FileService.GetTemplates().Select(Mapper.Map); + } + + /// + /// Gets the content json for the content id + /// + /// + /// + public TemplateDisplay GetById(int id) + { + var template = Services.FileService.GetTemplate(id); + if (template == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + return Mapper.Map(template); + } + + /// + /// Deletes a template wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var template = Services.FileService.GetTemplate(id); + if (template == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + Services.FileService.DeleteTemplate(template.Alias); + return Request.CreateResponse(HttpStatusCode.OK); + } + + public TemplateDisplay GetScaffold(int id) + { + //empty default + var dt = new Template("", ""); + dt.Path = "-1"; + + if (id > 0) + { + var master = Services.FileService.GetTemplate(id); + if(master != null) + { + dt.SetMasterTemplate(master); + } + } + + var content = ViewHelper.GetDefaultFileContent( layoutPageAlias: dt.MasterTemplateAlias ); + var scaffold = Mapper.Map(dt); + + scaffold.Content = content + "\r\n\r\n@* the fun starts here *@\r\n\r\n"; + return scaffold; + } + + /// + /// Saves the data type + /// + /// + /// + public TemplateDisplay PostSave(TemplateDisplay display) + { + + //Checking the submitted is valid with the Required attributes decorated on the ViewModel + if (ModelState.IsValid == false) + { + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); + } + + + if (display.Id > 0) + { + // update + var template = Services.FileService.GetTemplate(display.Id); + if (template == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + var changeMaster = template.MasterTemplateAlias != display.MasterTemplateAlias; + var changeAlias = template.Alias != display.Alias; + + Mapper.Map(display, template); + + if (changeMaster) + { + if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) + { + + var master = Services.FileService.GetTemplate(display.MasterTemplateAlias); + if(master == null || master.Id == display.Id) + { + template.SetMasterTemplate(null); + }else + { + template.SetMasterTemplate(master); + } + + } + else + { + //remove the master + template.SetMasterTemplate(null); + } + } + + Services.FileService.SaveTemplate(template); + + if (changeAlias) + { + template = Services.FileService.GetTemplate(template.Id); + } + + Mapper.Map(template, display); + } + else + { + //create + ITemplate master = null; + if (string.IsNullOrEmpty(display.MasterTemplateAlias) == false) + { + master = Services.FileService.GetTemplate(display.MasterTemplateAlias); + if (master == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var template = Services.FileService.CreateTemplateWithIdentity(display.Name, display.Content, master); + //template = Services.FileService.GetTemplate(template.Id); + Mapper.Map(template, display); + } + + return display; + } + } +} diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index 2eaf63f159..e3c63dd22e 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -64,9 +64,9 @@ namespace Umbraco.Web.Editors var queryResult = new QueryResultModel(); var sb = new StringBuilder(); - - sb.Append("CurrentPage.Site()"); - + var indention = Environment.NewLine + "\t\t\t\t\t\t"; + + sb.Append("Model.Content.Site()"); var timer = new Stopwatch(); timer.Start(); @@ -103,7 +103,7 @@ namespace Umbraco.Web.Editors { // we did not find the path sb.Clear(); - sb.AppendFormat("Umbraco.Content({0})", model.Source.Id); + sb.AppendFormat("Umbraco.TypedContent({0})", model.Source.Id); pointerNode = targetNode; } } @@ -126,10 +126,12 @@ namespace Umbraco.Web.Editors timer.Start(); contents = pointerNode.Children; timer.Stop(); - sb.Append(".Children"); + sb.Append(".Children()"); } + //setup 2 clauses, 1 for returning, 1 for testing var clause = string.Empty; + var tokenizedClause = string.Empty; // WHERE var token = 0; @@ -141,12 +143,13 @@ namespace Umbraco.Web.Editors foreach (var condition in model.Filters) { if(string.IsNullOrEmpty( condition.ConstraintValue)) continue; - - - - var operation = condition.BuildCondition(token); + + //x is passed in as the parameter alias for the linq where statement clause + var operation = condition.BuildCondition("x"); + var tokenizedOperation = condition.BuildTokenizedCondition(token); clause = string.IsNullOrEmpty(clause) ? operation : string.Concat(new[] { clause, " && ", operation }); + tokenizedClause = string.IsNullOrEmpty(tokenizedClause) ? tokenizedOperation : string.Concat(new[] { tokenizedClause, " && ", tokenizedOperation }); token++; } @@ -156,19 +159,21 @@ namespace Umbraco.Web.Editors timer.Start(); - //clause = "Visible && " + clause; - - contents = contents.AsQueryable().Where(clause, model.Filters.Select(this.GetConstraintValue).ToArray()); - // contents = contents.Where(clause, values.ToArray()); + //trial-run the tokenized clause to time the execution + //for review - this uses a tonized query rather then the normal linq query. + contents = contents.AsQueryable().Where(tokenizedClause, model.Filters.Select(this.GetConstraintValue).ToArray()); contents = contents.Where(x => x.IsVisible()); timer.Stop(); - clause = string.Format("\"Visible && {0}\",{1}", clause, - string.Join(",", model.Filters.Select(x => x.Property.Type == "string" ? - string.Format("\"{0}\"", x.ConstraintValue) : x.ConstraintValue).ToArray())); + + //the query to output to the editor + sb.Append(indention); + sb.Append(".Where(x => x.IsVisible())"); + + sb.Append(indention); + sb.AppendFormat(".Where(x => {0})", clause); - sb.AppendFormat(".Where({0})", clause); } else { @@ -178,7 +183,8 @@ namespace Umbraco.Web.Editors timer.Stop(); - sb.Append(".Where(\"Visible\")"); + sb.Append(indention); + sb.Append(".Where(x => x.IsVisible())"); } @@ -192,6 +198,7 @@ namespace Umbraco.Web.Editors var direction = model.Sort.Direction == "ascending" ? string.Empty : " desc"; + sb.Append(indention); sb.AppendFormat(".OrderBy(\"{0}{1}\")", model.Sort.Property.Alias, direction); } @@ -203,6 +210,7 @@ namespace Umbraco.Web.Editors timer.Stop(); + sb.Append(indention); sb.AppendFormat(".Take({0})", model.Take); } } @@ -217,7 +225,7 @@ namespace Umbraco.Web.Editors }); - return queryResult; + return queryResult; } private object GetConstraintValue(QueryCondition condition) diff --git a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs index d9bc169c8f..61d61f2108 100644 --- a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs +++ b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -3,7 +3,18 @@ namespace Umbraco.Web.Models.ContentEditing public class GetAvailableCompositionsFilter { public int ContentTypeId { get; set; } + + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// public string[] FilterPropertyTypes { get; set; } + + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// public string[] FilterContentTypes { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs new file mode 100644 index 0000000000..8e813bd3d3 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models.Validation; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "template", Namespace = "")] + public class TemplateDisplay : INotificationModel + { + + [DataMember(Name = "id")] + public int Id { get; set; } + + [Required] + [DataMember(Name = "name")] + public string Name { get; set; } + + [Required] + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "content")] + public string Content { get; set; } + + [DataMember(Name = "path")] + public string Path { get; set; } + + [DataMember(Name = "virtualPath")] + public string VirtualPath { get; set; } + + [DataMember(Name = "masterTemplateAlias")] + public string MasterTemplateAlias { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/TemplateModelMapper.cs b/src/Umbraco.Web/Models/Mapping/TemplateModelMapper.cs new file mode 100644 index 0000000000..d673e573a8 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/TemplateModelMapper.cs @@ -0,0 +1,28 @@ +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Mapping; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class TemplateModelMapper : MapperConfiguration + { + public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) + { + config.CreateMap() + .ForMember(x => x.Notifications, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(x => x.Key, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.CreateDate, exp => exp.Ignore()) + .ForMember(x => x.UpdateDate, exp => exp.Ignore()) + .ForMember(x => x.VirtualPath, exp => exp.Ignore()) + .ForMember(x => x.Path, exp => exp.Ignore()) + .ForMember(x => x.MasterTemplateId, exp => exp.Ignore()) // ok, assigned when creating the template + .ForMember(x => x.IsMasterTemplate, exp => exp.Ignore()) + .ForMember(x => x.HasIdentity, exp => exp.Ignore()); + } + } +} diff --git a/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs b/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs index 284518bf1e..36593736d3 100644 --- a/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs +++ b/src/Umbraco.Web/Models/TemplateQuery/QueryCondition.cs @@ -11,16 +11,48 @@ internal static class QueryConditionExtensions { - private static string MakeBinaryOperation(this QueryCondition condition, string operand, int token) + + public static string BuildTokenizedCondition(this QueryCondition condition, int token) { - return string.Format("{0}{1}@{2}", condition.Property.Name, operand, token); + return condition.BuildConditionString(string.Empty, token); } - - public static string BuildCondition(this QueryCondition condition, int token) + public static string BuildCondition(this QueryCondition condition, string parameterAlias) { + return condition.BuildConditionString(parameterAlias + "."); + } + + private static string BuildConditionString(this QueryCondition condition, string prefix, int token = -1) + { + + + var operand = string.Empty; var value = string.Empty; + var constraintValue = string.Empty; + + + //if a token is used, use a token placeholder, otherwise, use the actual value + if(token >= 0){ + constraintValue = string.Format("@{0}", token); + }else { + + //modify the format of the constraint value + switch (condition.Property.Type) + { + case "string": + constraintValue = string.Format("\"{0}\"", condition.ConstraintValue); + break; + case "datetime": + constraintValue = string.Format("DateTime.Parse(\"{0}\")", condition.ConstraintValue); + break; + default: + constraintValue = condition.ConstraintValue; + break; + } + + // constraintValue = condition.Property.Type == "string" ? string.Format("\"{0}\"", condition.ConstraintValue) : condition.ConstraintValue; + } switch (condition.Term.Operathor) { @@ -43,17 +75,23 @@ operand = " <= "; break; case Operathor.Contains: - value = string.Format("{0}.Contains(@{1})", condition.Property.Name, token); + value = string.Format("{0}{1}.Contains({2})", prefix, condition.Property.Alias, constraintValue); break; case Operathor.NotContains: - value = string.Format("!{0}.Contains(@{1})", condition.Property.Name, token); + value = string.Format("!{0}{1}.Contains({2})", prefix, condition.Property.Alias, constraintValue); break; default : operand = " == "; break; } - return string.IsNullOrEmpty(value) ? condition.MakeBinaryOperation(operand, token) : value; + + if (string.IsNullOrEmpty(value) == false) + return value; + + + + return string.Format("{0}{1}{2}{3}", prefix, condition.Property.Alias, operand, constraintValue); } } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 5caa08729a..7972f109f9 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1777,6 +1777,17 @@ namespace Umbraco.Web return content.Children().FirstOrDefault(); } + /// + /// Gets the first child of the content, of a given content type. + /// + /// The content. + /// The content type alias. + /// The first child of content, of the given content type. + public static IPublishedContent FirstChild(this IPublishedContent content, string alias) + { + return content.Children( alias ).FirstOrDefault(); + } + public static IPublishedContent FirstChild(this IPublishedContent content, Func predicate) { return content.Children(predicate).FirstOrDefault(); diff --git a/src/Umbraco.Web/Trees/TemplatesTreeController.cs b/src/Umbraco.Web/Trees/TemplatesTreeController.cs index 61cff8f862..04458cbc20 100644 --- a/src/Umbraco.Web/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web/Trees/TemplatesTreeController.cs @@ -48,12 +48,13 @@ namespace Umbraco.Web.Trees nodes.AddRange(found.Select(template => CreateTreeNode( template.Id.ToString(CultureInfo.InvariantCulture), //TODO: Fix parent ID stuff for templates - "-1", + "-1", queryStrings, template.Name, template.IsMasterTemplate ? "icon-newspaper" : "icon-newspaper-alt", - template.IsMasterTemplate, - GetEditorPath(template, queryStrings)))); + template.IsMasterTemplate, + GetEditorPath(template, queryStrings) + ))); return nodes; } @@ -67,15 +68,14 @@ namespace Umbraco.Web.Trees protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { var menu = new MenuItemCollection(); + //Create the normal create action + var item = menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); + item.NavigateToRoute(string.Format("{0}/templates/edit/{1}?create=true", queryStrings.GetValue("application"), id)); + if (id == Constants.System.Root.ToInvariantString()) { - //Create the normal create action - menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)) - //Since we haven't implemented anything for templates in angular, this needs to be converted to - //use the legacy format - .ConvertLegacyMenuItem(null, "inittemplates", queryStrings.GetValue("application")); - + //refresh action menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); @@ -86,12 +86,6 @@ namespace Umbraco.Web.Trees if (template == null) return new MenuItemCollection(); var entity = FromTemplate(template); - //Create the create action for creating sub layouts - menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)) - //Since we haven't implemented anything for templates in angular, this needs to be converted to - //use the legacy format - .ConvertLegacyMenuItem(entity, "templates", queryStrings.GetValue("application")); - //don't allow delete if it has child layouts if (template.IsMasterTemplate == false) { @@ -132,8 +126,7 @@ namespace Umbraco.Web.Trees return Services.FileService.DetermineTemplateRenderingEngine(template) == RenderingEngine.WebForms ? "/" + queryStrings.GetValue("application") + "/framed/" + Uri.EscapeDataString("settings/editTemplate.aspx?templateID=" + template.Id) - : "/" + queryStrings.GetValue("application") + "/framed/" + - Uri.EscapeDataString("settings/Views/EditView.aspx?treeType=" + Constants.Trees.Templates + "&templateID=" + template.Id); + : null; } } } diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 0314c65fae..3be8a58983 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -16,6 +16,8 @@ 'lib/ng-file-upload/ng-file-upload.min.js', 'lib/angular-local-storage/angular-local-storage.min.js', + //"lib/ace-builds/src-min-noconflict/ace.js", + 'lib/bootstrap/js/bootstrap.2.3.2.min.js', 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', 'lib/umbraco/Extensions.js', diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index af4cebdcb2..0a8d253ee5 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -285,6 +285,7 @@ + @@ -335,10 +336,12 @@ + + diff --git a/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs b/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs index b54d6102c0..de1383706e 100644 --- a/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs +++ b/src/Umbraco.Web/WebApi/JsonCamelCaseFormatter.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json.Serialization; namespace Umbraco.Web.WebApi { /// - /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter with a camelCase formatter /// public class JsonCamelCaseFormatter : Attribute, IControllerConfiguration { diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs index e018881f8a..2593acfbe0 100644 --- a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs +++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs @@ -6,7 +6,7 @@ using System.Web.Http.Validation; namespace Umbraco.Web.WebApi { /// - /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// Applying this attribute to any webapi controller will ensure that the is of type /// internal class PrefixlessBodyModelValidatorAttribute : Attribute, IControllerConfiguration { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs index bbddcd9de8..8c1cab8e78 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/create/templateTasks.cs @@ -13,8 +13,7 @@ using umbraco.cms.businesslogic.member; namespace umbraco { public class templateTasks : LegacyDialogTask - { - + { public override bool PerformSave() { var masterId = ParentID;