From bef117ac4c2c19cb8bbb01e5402df3c88ce79ceb Mon Sep 17 00:00:00 2001 From: Craig Noble Date: Wed, 10 Apr 2019 13:39:18 +0100 Subject: [PATCH] #5215 - Set up the initial build for typescript I have rewritten the notificationsService and the AngularHelper (used by the NotificationsService) in Typescript. This gives a very good example of how the notificationsService can reference a concrete type even though the angularHelper is being injected in the constructor. Open the Umbraco.Web.UI.Client in an editor that supports Typescript, such as VS Code or Atom. In Umbraco.Web.UI.Client, run npm install. Then run "npm run dev" and it will do the following: 1. Compile the typescript into Umbraco.Web.UI.Client/src/common/services/build/temp/, into their respective files such as notifications.service.js 2. The existing JS build picks up the compiled JS files and merges them into umbraco.services.js, then is saved in Umbraco.Web.UI/Umbraco/js (all stuff it currently does) I converted the controller into a class, within the umbraco.services namespace. The benefit of this is having the ability to generate a definitions file that will retain all namespaces and therefore make it extremely easy to find what you are looking for when creating Umbraco extensions such as custom property types, dashboards etc. Note: I have set up a definitions folder for global variables that are declared outside of Typescript. We can add typings in for Angular etc at a later date if needed. --- .gitignore | 1 + src/Umbraco.Web.UI.Client/gulp/config.js | 4 + src/Umbraco.Web.UI.Client/gulp/tasks/build.js | 2 +- src/Umbraco.Web.UI.Client/gulp/tasks/dev.js | 2 +- .../gulp/tasks/fastdev.js | 2 +- .../gulp/tasks/typescript.js | 31 ++ .../gulp/util/processTypescript.js | 21 ++ src/Umbraco.Web.UI.Client/package.json | 2 + .../src/common/definitions/global.d.ts | 3 + .../src/common/definitions/string.ts | 4 + ...er.service.js => angularhelper.service.ts} | 325 ++++++++---------- .../common/services/notifications.service.js | 300 ---------------- .../common/services/notifications.service.ts | 242 +++++++++++++ src/Umbraco.Web.UI.Client/tsconfig.json | 16 + .../Umbraco/js/notifications.service.js | 168 +++++++++ 15 files changed, 629 insertions(+), 494 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/gulp/tasks/typescript.js create mode 100644 src/Umbraco.Web.UI.Client/gulp/util/processTypescript.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/definitions/global.d.ts create mode 100644 src/Umbraco.Web.UI.Client/src/common/definitions/string.ts rename src/Umbraco.Web.UI.Client/src/common/services/{angularhelper.service.js => angularhelper.service.ts} (63%) delete mode 100644 src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/notifications.service.ts create mode 100644 src/Umbraco.Web.UI.Client/tsconfig.json create mode 100644 src/Umbraco.Web.UI/Umbraco/js/notifications.service.js diff --git a/.gitignore b/.gitignore index 23ba01ebf008..557273b36589 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,7 @@ build.tmp/ build/hooks/ build/temp/ +/src/Umbraco.Web.UI.Client/src/common/**/build/temp/ # eof diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index c27a2c5f5313..e044c5876410 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -25,6 +25,10 @@ module.exports = { security: { files: ["./src/common/interceptors/**/*.js"], out: "umbraco.interceptors.js" } }, + ts: { + services: { files: ["./src/common/services/**/*.ts"], out: "./src/common/services/build/temp/" } + }, + //selectors for copying all views into the build //processed in the views task views:{ diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/build.js b/src/Umbraco.Web.UI.Client/gulp/tasks/build.js index 32bf71f5f71d..61fec7a9e8e9 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/build.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/build.js @@ -6,5 +6,5 @@ var runSequence = require('run-sequence'); // Build - build the files ready for production gulp.task('build', function(cb) { - runSequence(["js", "dependencies", "less", "views"], "test:unit", cb); + runSequence(["typescript", "js", "dependencies", "less", "views"], "test:unit", cb); }); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js index bca4da8c43cf..053aa962ffbd 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js @@ -6,5 +6,5 @@ var runSequence = require('run-sequence'); // Dev - build the files ready for development and start watchers gulp.task('dev', function(cb) { - runSequence(["dependencies", "js", "less", "views"], "watch", cb); + runSequence(["dependencies", "typescript", "js", "less", "views"], "watch", cb); }); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js b/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js index 888ed38fec28..00b99c3bc082 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/fastdev.js @@ -9,5 +9,5 @@ gulp.task('fastdev', function(cb) { global.isProd = false; - runSequence(["dependencies", "js", "less", "views"], "watch", cb); + runSequence(["dependencies", "typescript", "js", "less", "views"], "watch", cb); }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/typescript.js b/src/Umbraco.Web.UI.Client/gulp/tasks/typescript.js new file mode 100644 index 000000000000..e42832791bb5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/typescript.js @@ -0,0 +1,31 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); + +var processTs = require('../util/processTypescript'); + +/************************** + * Copies all angular JS files into their seperate umbraco.*.js file + **************************/ +gulp.task('typescript', function () { + + //we run multiple streams, so merge them all together + var stream = new MergeStream(); + + stream.add( + gulp.src(config.sources.globs.js) + .pipe(gulp.dest(config.root + config.targets.js)) + ); + + // compile TS + _.forEach(config.sources.ts, function (group) { + if(group.files.length > 0) + stream.add (processTs(group.files, group.out)); + }); + + return stream; +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processTypescript.js b/src/Umbraco.Web.UI.Client/gulp/util/processTypescript.js new file mode 100644 index 000000000000..be8955daa402 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/util/processTypescript.js @@ -0,0 +1,21 @@ + +var config = require('../config'); +var gulp = require('gulp'); + +var ts = require('gulp-typescript'); + +module.exports = function(files, out) { + + var task = gulp.src(files); + + var tsProject = ts.createProject('tsconfig.json'); + + + // sort files in stream by path or any custom sort comparator + task = task.pipe(tsProject()) + .pipe(gulp.dest(out)); + + + return task; + +}; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 3a5ff22f04ad..aa42524e5abb 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -41,6 +41,7 @@ "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.2", "typeahead.js": "0.11.1", + "typescript": "^3.2.1", "underscore": "1.9.1" }, "devDependencies": { @@ -64,6 +65,7 @@ "gulp-postcss": "8.0.0", "gulp-rename": "1.4.0", "gulp-sort": "2.0.0", + "gulp-typescript": "^5.0.1", "gulp-watch": "5.0.1", "gulp-wrap": "0.14.0", "gulp-wrap-js": "0.4.1", diff --git a/src/Umbraco.Web.UI.Client/src/common/definitions/global.d.ts b/src/Umbraco.Web.UI.Client/src/common/definitions/global.d.ts new file mode 100644 index 000000000000..11569bfe7473 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/definitions/global.d.ts @@ -0,0 +1,3 @@ +declare var angular: any; +declare var _: any; +declare var $q: any; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/definitions/string.ts b/src/Umbraco.Web.UI.Client/src/common/definitions/string.ts new file mode 100644 index 000000000000..beee7a636379 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/definitions/string.ts @@ -0,0 +1,4 @@ + +interface StringConstructor { + CreateGuid(): string; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.ts similarity index 63% rename from src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js rename to src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.ts index 455857c1e1f8..2d75916fcbde 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.ts @@ -1,191 +1,134 @@ -/** - * @ngdoc service - * @name umbraco.services.angularHelper - * @function - * - * @description - * Some angular helper/extension methods - */ -function angularHelper($q) { - return { - - /** - * Method used to re-run the $parsers for a given ngModel - * @param {} scope - * @param {} ngModel - * @returns {} - */ - revalidateNgModel: function (scope, ngModel) { - this.safeApply(scope, function() { - angular.forEach(ngModel.$parsers, function (parser) { - parser(ngModel.$viewValue); - }); - }); - }, - - /** - * Execute a list of promises sequentially. Unlike $q.all which executes all promises at once, this will execute them in sequence. - * @param {} promises - * @returns {} - */ - executeSequentialPromises: function (promises) { - - //this is sequential promise chaining, it's not pretty but we need to do it this way. - //$q.all doesn't execute promises in sequence but that's what we want to do here. - - if (!angular.isArray(promises)) { - throw "promises must be an array"; - } - - //now execute them in sequence... sorry there's no other good way to do it with angular promises - var j = 0; - function pExec(promise) { - j++; - return promise.then(function (data) { - if (j === promises.length) { - return $q.when(data); //exit - } - else { - return pExec(promises[j]); //recurse - } - }); - } - if (promises.length > 0) { - return pExec(promises[0]); //start the promise chain - } - else { - return $q.when(true); // just exit, no promises to execute - } - }, - - /** - * @ngdoc function - * @name safeApply - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * This checks if a digest/apply is already occuring, if not it will force an apply call - */ - safeApply: function (scope, fn) { - if (scope.$$phase || (scope.$root && scope.$root.$$phase)) { - if (angular.isFunction(fn)) { - fn(); - } - } - else { - if (angular.isFunction(fn)) { - scope.$apply(fn); - } - else { - scope.$apply(); - } - } - }, - - /** - * @ngdoc function - * @name getCurrentForm - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * Returns the current form object applied to the scope or null if one is not found - */ - getCurrentForm: function (scope) { - - //NOTE: There isn't a way in angular to get a reference to the current form object since the form object - // is just defined as a property of the scope when it is named but you'll always need to know the name which - // isn't very convenient. If we want to watch for validation changes we need to get a form reference. - // The way that we detect the form object is a bit hackerific in that we detect all of the required properties - // that exist on a form object. - // - //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it - // is to inject the $element object and use: $element.inheritedData('$formController'); - - var form = null; - var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; - - // a method to check that the collection of object prop names contains the property name expected - function propertyExists(objectPropNames) { - //ensure that every required property name exists on the current scope property - return _.every(requiredFormProps, function (item) { - - return _.contains(objectPropNames, item); - }); - } - - for (var p in scope) { - - if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { - //get the keys of the property names for the current property - var props = _.keys(scope[p]); - //if the length isn't correct, try the next prop - if (props.length < requiredFormProps.length) { - continue; - } - - //ensure that every required property name exists on the current scope property - var containProperty = propertyExists(props); - - if (containProperty) { - form = scope[p]; - break; - } - } - } - - return form; - }, - - /** - * @ngdoc function - * @name validateHasForm - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * This will validate that the current scope has an assigned form object, if it doesn't an exception is thrown, if - * it does we return the form object. - */ - getRequiredCurrentForm: function (scope) { - var currentForm = this.getCurrentForm(scope); - if (!currentForm || !currentForm.$name) { - throw "The current scope requires a current form object (or ng-form) with a name assigned to it"; - } - return currentForm; - }, - - /** - * @ngdoc function - * @name getNullForm - * @methodOf umbraco.services.angularHelper - * @function - * - * @description - * Returns a null angular FormController, mostly for use in unit tests - * NOTE: This is actually the same construct as angular uses internally for creating a null form but they don't expose - * any of this publicly to us, so we need to create our own. - * NOTE: The properties has been added to the null form because we use them to get a form on a scope. - * - * @param {string} formName The form name to assign - */ - getNullForm: function (formName) { - return { - $error: {}, - $dirty: false, - $pristine: true, - $valid: true, - $submitted: false, - $pending: undefined, - $addControl: angular.noop, - $removeControl: angular.noop, - $setValidity: angular.noop, - $setDirty: angular.noop, - $setPristine: angular.noop, - $name: formName - }; - } - }; -} -angular.module('umbraco.services').factory('angularHelper', angularHelper); +/// + +module umbraco.services { + + export class angularHelper { + + public revalidateNgModel(scope, ngModel) { + this.safeApply(scope, function() { + angular.forEach(ngModel.$parsers, function (parser) { + parser(ngModel.$viewValue); + }); + }); + } + + public executeSequentialPromises(promises) { + + //this is sequential promise chaining, it's not pretty but we need to do it this way. + //$q.all doesn't execute promises in sequence but that's what we want to do here. + + if (!angular.isArray(promises)) { + throw "promises must be an array"; + } + + //now execute them in sequence... sorry there's no other good way to do it with angular promises + var j = 0; + function pExec(promise) { + j++; + return promise.then(function (data) { + if (j === promises.length) { + return $q.when(data); //exit + } + else { + return pExec(promises[j]); //recurse + } + }); + } + if (promises.length > 0) { + return pExec(promises[0]); //start the promise chain + } + else { + return $q.when(true); // just exit, no promises to execute + } + } + + public safeApply(scope, fn) { + if (scope.$$phase || (scope.$root && scope.$root.$$phase)) { + if (angular.isFunction(fn)) { + fn(); + } + } + else { + if (angular.isFunction(fn)) { + scope.$apply(fn); + } + else { + scope.$apply(); + } + } + } + + public getCurrentForm(scope) { + + //NOTE: There isn't a way in angular to get a reference to the current form object since the form object + // is just defined as a property of the scope when it is named but you'll always need to know the name which + // isn't very convenient. If we want to watch for validation changes we need to get a form reference. + // The way that we detect the form object is a bit hackerific in that we detect all of the required properties + // that exist on a form object. + // + //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it + // is to inject the $element object and use: $element.inheritedData('$formController'); + + var form = null; + var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; + + // a method to check that the collection of object prop names contains the property name expected + function propertyExists(objectPropNames) { + //ensure that every required property name exists on the current scope property + return _.every(requiredFormProps, function (item) { + + return _.contains(objectPropNames, item); + }); + } + + for (var p in scope) { + + if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { + //get the keys of the property names for the current property + var props = _.keys(scope[p]); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + continue; + } + + //ensure that every required property name exists on the current scope property + var containProperty = propertyExists(props); + + if (containProperty) { + form = scope[p]; + break; + } + } + } + + return form; + } + + public getRequiredCurrentForm(scope) { + var currentForm = this.getCurrentForm(scope); + if (!currentForm || !currentForm.$name) { + throw "The current scope requires a current form object (or ng-form) with a name assigned to it"; + } + return currentForm; + } + + public getNullForm(formName) { + return { + $error: {}, + $dirty: false, + $pristine: true, + $valid: true, + $submitted: false, + $pending: undefined, + $addControl: angular.noop, + $removeControl: angular.noop, + $setValidity: angular.noop, + $setDirty: angular.noop, + $setPristine: angular.noop, + $name: formName + }; + } + } +} + +angular.module('umbraco.services').service('angularHelper', umbraco.services.angularHelper); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js deleted file mode 100644 index c123ac6cea03..000000000000 --- a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js +++ /dev/null @@ -1,300 +0,0 @@ -/** - * @ngdoc service - * @name umbraco.services.notificationsService - * - * @requires $rootScope - * @requires $timeout - * @requires angularHelper - * - * @description - * Application-wide service for handling notifications, the umbraco application - * maintains a single collection of notications, which the UI watches for changes. - * By default when a notication is added, it is automaticly removed 7 seconds after - * This can be changed on add() - * - * ##usage - * To use, simply inject the notificationsService into any controller that needs it, and make - * sure the umbraco.services module is accesible - which it should be by default. - * - *
- *		notificationsService.success("Document Published", "hooraaaay for you!");
- *      notificationsService.error("Document Failed", "booooh");
- * 
- */ -angular.module('umbraco.services') -.factory('notificationsService', function ($rootScope, $timeout, angularHelper) { - - var nArray = []; - function setViewPath(view){ - if(view.indexOf('/') < 0) - { - view = "views/common/notifications/" + view; - } - - if(view.indexOf('.html') < 0) - { - view = view + ".html"; - } - return view; - } - - var service = { - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#add - * @methodOf umbraco.services.notificationsService - * - * @description - * Lower level api for adding notifcations, support more advanced options - * @param {Object} item The notification item - * @param {String} item.headline Short headline - * @param {String} item.message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @param {String} item.type Notification type, can be: "success","warning","error" or "info" - * @param {String} item.url url to open when notification is clicked - * @param {String} item.view path to custom view to load into the notification box - * @param {Array} item.actions Collection of button actions to append (label, func, cssClass) - * @param {Boolean} item.sticky if set to true, the notification will not auto-close - * @returns {Object} args notification object - */ - - add: function(item) { - angularHelper.safeApply($rootScope, function () { - - if(item.view){ - item.view = setViewPath(item.view); - item.sticky = true; - item.type = "form"; - item.headline = null; - } - - - //add a colon after the headline if there is a message as well - if (item.message) { - item.headline += ": "; - if(item.message.length > 200) { - item.sticky = true; - } - } - - //we need to ID the item, going by index isn't good enough because people can remove at different indexes - // whenever they want. Plus once we remove one, then the next index will be different. The only way to - // effectively remove an item is by an Id. - item.id = String.CreateGuid(); - - nArray.push(item); - - if(!item.sticky) { - $timeout( - function() { - var found = _.find(nArray, function(i) { - return i.id === item.id; - }); - if (found) { - var index = nArray.indexOf(found); - nArray.splice(index, 1); - } - } - , 10000); - } - - return item; - }); - - }, - - hasView : function(view){ - if(!view){ - return _.find(nArray, function(notification){ return notification.view;}); - }else{ - view = setViewPath(view).toLowerCase(); - return _.find(nArray, function(notification){ return notification.view.toLowerCase() === view;}); - } - }, - addView: function(view, args){ - var item = { - args: args, - view: view - }; - - service.add(item); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#showNotification - * @methodOf umbraco.services.notificationsService - * - * @description - * Shows a notification based on the object passed in, normally used to render notifications sent back from the server - * - * @returns {Object} args notification object - */ - showNotification: function(args) { - if (!args) { - throw "args cannot be null"; - } - if (args.type === undefined || args.type === null) { - throw "args.type cannot be null"; - } - if (!args.header) { - throw "args.header cannot be null"; - } - - switch(args.type) { - case 0: - //save - this.success(args.header, args.message); - break; - case 1: - //info - this.success(args.header, args.message); - break; - case 2: - //error - this.error(args.header, args.message); - break; - case 3: - //success - this.success(args.header, args.message); - break; - case 4: - //warning - this.warning(args.header, args.message); - break; - } - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#success - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a green success notication to the notications collection - * This should be used when an operations *completes* without errors - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - success: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'success', time: new Date() }); - }, - /** - * @ngdoc method - * @name umbraco.services.notificationsService#error - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a red error notication to the notications collection - * This should be used when an operations *fails* and could not complete - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - error: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'error', time: new Date() }); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#warning - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a yellow warning notication to the notications collection - * This should be used when an operations *completes* but something was not as expected - * - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - warning: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'warning', time: new Date() }); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#warning - * @methodOf umbraco.services.notificationsService - * - * @description - * Adds a yellow warning notication to the notications collection - * This should be used when an operations *completes* but something was not as expected - * - * - * @param {String} headline Headline of the notification - * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded - * @returns {Object} notification object - */ - info: function (headline, message) { - return service.add({ headline: headline, message: message, type: 'info', time: new Date() }); - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#remove - * @methodOf umbraco.services.notificationsService - * - * @description - * Removes a notification from the notifcations collection at a given index - * - * @param {Int} index index where the notication should be removed from - */ - remove: function (index) { - if(angular.isObject(index)){ - var i = nArray.indexOf(index); - angularHelper.safeApply($rootScope, function() { - nArray.splice(i, 1); - }); - }else{ - angularHelper.safeApply($rootScope, function() { - nArray.splice(index, 1); - }); - } - }, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#removeAll - * @methodOf umbraco.services.notificationsService - * - * @description - * Removes all notifications from the notifcations collection - */ - removeAll: function () { - angularHelper.safeApply($rootScope, function() { - nArray = []; - }); - }, - - /** - * @ngdoc property - * @name umbraco.services.notificationsService#current - * @propertyOf umbraco.services.notificationsService - * - * @description - * Returns an array of current notifications to display - * - * @returns {string} returns an array - */ - current: nArray, - - /** - * @ngdoc method - * @name umbraco.services.notificationsService#getCurrent - * @methodOf umbraco.services.notificationsService - * - * @description - * Method to return all notifications from the notifcations collection - */ - getCurrent: function(){ - return nArray; - } - }; - - return service; -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.ts b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.ts new file mode 100644 index 000000000000..f461b0f8e885 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.ts @@ -0,0 +1,242 @@ +/// +/// +/// + +namespace umbraco.services { + + /** + * @ngdoc service + * @name umbraco.services.notificationsService + * + * @requires $rootScope + * @requires $timeout + * @requires angularHelper + * + * @description + * Application-wide service for handling notifications, the umbraco application + * maintains a single collection of notications, which the UI watches for changes. + * By default when a notication is added, it is automaticly removed 7 seconds after + * This can be changed on add() + * + * ##usage + * To use, simply inject the notificationsService into any controller that needs it, and make + * sure the umbraco.services module is accesible - which it should be by default. + * + *
+	*		notificationsService.success("Document Published", "hooraaaay for you!");
+	*      notificationsService.error("Document Failed", "booooh");
+	* 
+ */ + export class notificationsService { + private nArray: Array = new Array(); + + public current: Array = this.nArray; + + private $rootScope: any; + private $timeout: any; + private angularHelper: umbraco.services.angularHelper; + + public constructor($rootScope, $timeout, angularHelper: umbraco.services.angularHelper) { + this.$rootScope = $rootScope; + this.$timeout = $timeout; + this.angularHelper = angularHelper; + } + + public add(item: models.iNotification) { + var that = this; + this.angularHelper.safeApply(this.$rootScope, function () { + + if(item.view){ + item.view = this.setViewPath(item.view); + item.sticky = true; + item.type = models.notificationType.form; + item.headline = null; + } + + + //add a colon after the headline if there is a message as well + if (item.message) { + item.headline += ": "; + if(item.message.length > 200) { + item.sticky = true; + } + } + + //we need to ID the item, going by index isn't good enough because people can remove at different indexes + // whenever they want. Plus once we remove one, then the next index will be different. The only way to + // effectively remove an item is by an Id. + item.id = String.CreateGuid(); + + that.nArray.push(item); + + if(!item.sticky) { + this.$timeout( + function() { + var found = _.find(this.nArray, function(i) { + return i.id === item.id; + }); + if (found) { + var index = this.nArray.indexOf(found); + this.nArray.splice(index, 1); + } + } + , 10000); + } + + return item; + }); + } + + public hasView(view){ + if(!view){ + return _.find(this.nArray, function(notification){ return notification.view;}); + }else{ + view = this.setViewPath(view).toLowerCase(); + return _.find(this.nArray, function(notification){ return notification.view.toLowerCase() === view;}); + } + } + + public addView(view: string, args: Array){ + var item = { + args: args, + view: view + }; + + this.add(item); + } + + public showNotification(notification: umbraco.services.models.iGenericNotification) { + if (!notification) { + throw "notification cannot be null"; + } + if (notification.type === undefined || notification.type === null) { + throw "notification.type cannot be null"; + } + if (!notification.header) { + throw "notification.header cannot be null"; + } + + switch(notification.type) { + case models.genericNotificationType.save: + this.success(notification.header, notification.message); + break; + case models.genericNotificationType.info: + this.success(notification.header, notification.message); + break; + case models.genericNotificationType.error: + this.error(notification.header, notification.message); + break; + case models.genericNotificationType.success: + this.success(notification.header, notification.message); + break; + case models.genericNotificationType.warning: + this.warning(notification.header, notification.message); + break; + } + } + + public success(headline: string, message: string) { + return this.add({ headline: headline, message: message, type: models.notificationType.success, time: new Date() }); + } + + public error(headline: string, message: string) { + return this.add({ headline: headline, message: message, type: models.notificationType.error, time: new Date() }); + } + + public warning(headline: string, message: string) { + return this.add({ headline: headline, message: message, type: models.notificationType.warning, time: new Date() }); + } + + public info(headline: string, message: string) { + return this.add({ headline: headline, message: message, type: models.notificationType.info, time: new Date() }); + } + + public remove(index) { + if(angular.isObject(index)){ + var i = this.nArray.indexOf(index); + this.angularHelper.safeApply(this.$rootScope, function() { + this.nArray.splice(i, 1); + }); + }else{ + this.angularHelper.safeApply(this.$rootScope, function() { + this.nArray.splice(index, 1); + }); + } + } + + public removeAll() { + this.angularHelper.safeApply(this.$rootScope, function() { + this.nArray = []; + }); + } + + public getCurrent(){ + return this.nArray; + } + + private setViewPath(view: string) : string { + if(view.indexOf('/') < 0) + { + view = "views/common/notifications/" + view; + } + + if(view.indexOf('.html') < 0) + { + view = view + ".html"; + } + return view; + } + } + + /* + Models for Notification Service + */ + export namespace models { + + export interface iNotification { + id?: string; + headline?: string; + message?: string; + type?: notificationType; + url?: string; + view?: string; + actions?: Array; + sticky?: boolean; + time?: Date; + args?: Array; + } + + export enum notificationType { + info = 'info', + error = 'error', + success = 'success', + warning = 'warning', + form = 'form' + } + + export interface iGenericNotification { + header?: string; + message?: string; + type?: genericNotificationType; + url?: string; + view?: string; + actions?: Array; + sticky?: boolean; + time?: Date; + } + + export enum genericNotificationType { + save = 0, + info = 1, + error = 2, + success = 3, + warning = 4 + } + + + } + +} + +angular.module('umbraco.services').service('notificationsService', ['$rootScope', '$timeout', 'angularHelper', umbraco.services.notificationsService]); + diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json new file mode 100644 index 000000000000..1f3bb1029969 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "sourceMap": true, + "rootDir": "src" + }, + //"include": [ + // "src/common/**/*.ts" + //], + "exclude": [ + "node_modules" + ], + "compileOnSave": true, + "allowJS": true +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/js/notifications.service.js b/src/Umbraco.Web.UI/Umbraco/js/notifications.service.js new file mode 100644 index 000000000000..409edcab304e --- /dev/null +++ b/src/Umbraco.Web.UI/Umbraco/js/notifications.service.js @@ -0,0 +1,168 @@ +/// +/// +/// +var umbraco; +(function (umbraco) { + var services; + (function (services) { + /** + * @ngdoc service + * @name umbraco.services.notificationsService + * + * @requires $rootScope + * @requires $timeout + * @requires angularHelper + * + * @description + * Application-wide service for handling notifications, the umbraco application + * maintains a single collection of notications, which the UI watches for changes. + * By default when a notication is added, it is automaticly removed 7 seconds after + * This can be changed on add() + * + * ##usage + * To use, simply inject the notificationsService into any controller that needs it, and make + * sure the umbraco.services module is accesible - which it should be by default. + * + *
+        *		notificationsService.success("Document Published", "hooraaaay for you!");
+        *      notificationsService.error("Document Failed", "booooh");
+        * 
+ */ + class notificationsService { + constructor($rootScope, $timeout, angularHelper) { + this.nArray = new Array(); + this.current = this.nArray; + this.$rootScope = $rootScope; + this.$timeout = $timeout; + this.angularHelper = angularHelper; + } + add(item) { + this.angularHelper.safeApply(this.$rootScope, function () { + if (item.view) { + item.view = this.setViewPath(item.view); + item.sticky = true; + item.type = "form"; + item.headline = null; + } + //add a colon after the headline if there is a message as well + if (item.message) { + item.headline += ": "; + if (item.message.length > 200) { + item.sticky = true; + } + } + //we need to ID the item, going by index isn't good enough because people can remove at different indexes + // whenever they want. Plus once we remove one, then the next index will be different. The only way to + // effectively remove an item is by an Id. + item.id = String.CreateGuid(); + this.nArray.push(item); + if (!item.sticky) { + this.$timeout(function () { + var found = _.find(this.nArray, function (i) { + return i.id === item.id; + }); + if (found) { + var index = this.nArray.indexOf(found); + this.nArray.splice(index, 1); + } + }, 10000); + } + return item; + }); + } + hasView(view) { + if (!view) { + return _.find(this.nArray, function (notification) { return notification.view; }); + } + else { + view = this.setViewPath(view).toLowerCase(); + return _.find(this.nArray, function (notification) { return notification.view.toLowerCase() === view; }); + } + } + addView(view, args) { + var item = { + args: args, + view: view + }; + this.add(item); + } + showNotification(args) { + if (!args) { + throw "args cannot be null"; + } + if (args.type === undefined || args.type === null) { + throw "args.type cannot be null"; + } + if (!args.header) { + throw "args.header cannot be null"; + } + switch (args.type) { + case 0: + //save + this.success(args.header, args.message); + break; + case 1: + //info + this.success(args.header, args.message); + break; + case 2: + //error + this.error(args.header, args.message); + break; + case 3: + //success + this.success(args.header, args.message); + break; + case 4: + //warning + this.warning(args.header, args.message); + break; + } + } + success(headline, message) { + return this.add({ headline: headline, message: message, type: 'success', time: new Date() }); + } + error(headline, message) { + return this.add({ headline: headline, message: message, type: 'error', time: new Date() }); + } + warning(headline, message) { + return this.add({ headline: headline, message: message, type: 'warning', time: new Date() }); + } + info(headline, message) { + return this.add({ headline: headline, message: message, type: 'info', time: new Date() }); + } + remove(index) { + if (angular.isObject(index)) { + var i = this.nArray.indexOf(index); + this.angularHelper.safeApply(this.$rootScope, function () { + this.nArray.splice(i, 1); + }); + } + else { + this.angularHelper.safeApply(this.$rootScope, function () { + this.nArray.splice(index, 1); + }); + } + } + removeAll() { + this.angularHelper.safeApply(this.$rootScope, function () { + this.nArray = []; + }); + } + getCurrent() { + return this.nArray; + } + setViewPath(view) { + if (view.indexOf('/') < 0) { + view = "views/common/notifications/" + view; + } + if (view.indexOf('.html') < 0) { + view = view + ".html"; + } + return view; + } + } + services.notificationsService = notificationsService; + })(services = umbraco.services || (umbraco.services = {})); +})(umbraco || (umbraco = {})); +angular.module('umbraco.services').service('notificationsService', ['$rootScope', '$timeout', 'angularHelper', umbraco.services.notificationsService]);