From c75dad92a969d8b2645862c4032268c6ca596b6e Mon Sep 17 00:00:00 2001 From: Manuel Geier Date: Fri, 13 Feb 2015 15:12:42 +0100 Subject: [PATCH] create a separate substate for editing (.edit) and create (.create) cleanup and better splitting of detail and edit mode create state has to be registered before (!) the detail state, because it has a more specific url: /new vs. /{:id} --- .../controller/cat-base-edit-controller.js | 147 ++++++++++++++++++ .../controller/cat-base-list-controller.js | 7 +- .../javascript/service/cat-route-service.js | 99 +++++++++--- .../service/cat-view-config-service.js | 2 +- .../template/cat-base-detail.tpl.html | 26 +--- .../resources/template/cat-base-edit.tpl.html | 27 ++++ .../service/cat-view-config-service.spec.js | 21 ++- 7 files changed, 275 insertions(+), 54 deletions(-) create mode 100644 src/main/javascript/controller/cat-base-edit-controller.js create mode 100644 src/main/resources/template/cat-base-edit.tpl.html diff --git a/src/main/javascript/controller/cat-base-edit-controller.js b/src/main/javascript/controller/cat-base-edit-controller.js new file mode 100644 index 0000000..30d941c --- /dev/null +++ b/src/main/javascript/controller/cat-base-edit-controller.js @@ -0,0 +1,147 @@ +'use strict'; + +angular.module('cat.controller.base.edit', []) + +/** + * @ngdoc controller + * @name cat.controller.base.edit:CatBaseEditController + * + * @description + * The CatBaseEditController takes care of providing several common properties and functions to the scope + * of every edit page. It also instantiates the controller given via the config.controller parameter and shares + * the same scope with it. + * + * Common properties include: + * * detail - the actual object to view + * * editDetail - a copy of the detail object used for editing + * * breadcrumbs - the breadcrumbs array + * * uiStack - the ui stack array if parents exist + * * editTemplate - the url of the edit template + * * mainViewTemplate - the url of the main view template + * * additionalViewTemplate - the url of the additional view template if it exists + * * $fieldErrors - a map of validation errors returned by the server + * + * Common functions include: + * * save - the save function to update / create an object + * * cancelEdit - a function to switch from edit to view mode (discarding all changes) + * * title - a function to resolve a 'title' of the current object + * + * @param {object} $scope DOCTODO + * @param {object} $state DOCTODO + * @param {object} $stateParams DOCTODO + * @param {object} $window DOCTODO + * @param {object} $globalMessages DOCTODO + * @param {object} $controller DOCTODO + * @param {object} $log DOCTODO + * @param {object} catBreadcrumbsService DOCTODO + * @param {Object} config holds data like the current api endpoint, template urls, base url, the model constructor, etc. + * @param {object} catValidationService DOCTODO + * @constructor + */ + .controller('CatBaseEditController', CatBaseEditController); + +function CatBaseEditController($scope, $state, $stateParams, $window, $globalMessages, $controller, $log, catBreadcrumbsService, catValidationService, config) { + $scope.detail = config.detail; + $scope.editDetail = undefined; + $scope.$fieldErrors = {}; + + $scope.config = config; + var endpoint = config.endpoint; + var baseUrl = config.baseUrl; + var templateUrls = config.templateUrls; + var Model = config.Model; + + $scope.uiStack = catBreadcrumbsService.generateFromConfig(config); + + if ($stateParams.id === 'new' || $stateParams.id === undefined) { + catBreadcrumbsService.push({ + title: 'New', + key: 'cc.catalysts.general.new' + }); + } else { + catBreadcrumbsService.push({}); + } + + $scope.editTemplate = templateUrls.edit; + + if (_.isObject(templateUrls.view)) { + $scope.mainViewTemplate = templateUrls.view.main; + $scope.additionalViewTemplate = templateUrls.view.additional; + } else { + $scope.mainViewTemplate = templateUrls.view; + } + + $scope.baseUrl = baseUrl; + + /** + * @returns {String|Number} A title of the current object or the 'id' as fallback + */ + $scope.title = function () { + var data = $scope.detail; + if (_.isUndefined(data)) { + return ''; + } + return !!data.breadcrumbTitle ? data.breadcrumbTitle() : (!!data.name ? data.name : data.id); + }; + + /** + * reloads the current object from the server + */ + $scope.reloadDetails = function () { + endpoint.get($stateParams.id).then(function (data) { + $scope.detail = data; + update(); + }); + }; + + $scope.exists = !!$stateParams.id && $stateParams.id !== 'new'; + + $scope.cancelEdit = function () { + $scope.$broadcast('formReset'); // TODO necessary to call? + if ($scope.exists) { + $state.go(config.name + '.detail', {id: config.detail.id}); + } else { + $window.history.back(); + } + }; + + /** + * Calls the save function of the current endpoint. + * Upon success the view mode of the details of the currently created / updated object will be shown. + * Upon an error the reported errors (global & field errors) will be shown to the user and the edit mode + * will remain active. + * + * * @param {object} stayInEdit If true the view stays in detail edit state after save instead of switching to + * detail view state. + */ + $scope.save = function (stayInEdit) { + endpoint.save($scope.editDetail).then(function (data) { + catValidationService.clearValidationErrors(); + $scope.$broadcast('formReset'); + $globalMessages.addMessage('success', 'Saved successfully.', true); + if (stayInEdit) { + $state.go('^.edit', {id: data.id}); + } else { + $state.go('^.detail', {id: data.id}); + } + }); + }; + + try { + // extend with custom controller + $controller(config.controller, { + $scope: $scope, + detail: config.detail, + parents: config.parents, + config: config + }); + } catch (unused) { + $log.info('Couldn\'t instantiate controller with name ' + config.controller); + } + + // set edit object with parent relations + $scope.editDetail = $scope.detail; + if (_.isFunction($scope.editDetail.setParent)) { + $scope.editDetail.setParent(config.parents[0]); + } +} diff --git a/src/main/javascript/controller/cat-base-list-controller.js b/src/main/javascript/controller/cat-base-list-controller.js index 27c6cb2..86274ea 100644 --- a/src/main/javascript/controller/cat-base-list-controller.js +++ b/src/main/javascript/controller/cat-base-list-controller.js @@ -1,6 +1,5 @@ 'use strict'; - /** * @ngdoc controller * @name cat.controller.base.list:CatBaseListController @@ -25,8 +24,9 @@ * @param {object} catBreadcrumbsService catBreadcrumbsService * @param {object} catListDataLoadingService catListDataLoadingService * @param {object} config holds data like the listData object, the template url, base url, the model constructor, etc. + * @param {object} $globalMessages $globalMessages */ -function CatBaseListController($scope, $state, $controller, $log, catBreadcrumbsService, catListDataLoadingService, config) { +function CatBaseListController($scope, $state, $controller, $log, catBreadcrumbsService, catListDataLoadingService, $globalMessages, config) { if (!_.isUndefined(config.listData)) { this.titleKey = 'cc.catalysts.cat-breadcrumbs.entry.' + config.listData.endpoint.getEndpointName(); @@ -58,6 +58,7 @@ function CatBaseListController($scope, $state, $controller, $log, catBreadcrumbs this.remove = function (id) { config.listData.endpoint.remove(id) .then(function () { + $globalMessages.addMessage('success', 'Successfully deleted entry.', true); catListDataLoadingService.load(config.listData.endpoint, config.listData.searchRequest).then( function (data) { _.assign($scope.listData, data); @@ -77,4 +78,4 @@ function CatBaseListController($scope, $state, $controller, $log, catBreadcrumbs angular.module('cat.controller.base.list', ['cat.service.breadcrumbs']) .controller('CatBaseListController', - ['$scope', '$state', '$controller', '$log', 'catBreadcrumbsService', 'catListDataLoadingService', 'config', CatBaseListController]); + ['$scope', '$state', '$controller', '$log', 'catBreadcrumbsService', 'catListDataLoadingService', '$globalMessages', 'config', CatBaseListController]); diff --git a/src/main/javascript/service/cat-route-service.js b/src/main/javascript/service/cat-route-service.js index 9ce2027..771aaa3 100644 --- a/src/main/javascript/service/cat-route-service.js +++ b/src/main/javascript/service/cat-route-service.js @@ -1,5 +1,12 @@ 'use strict'; +angular.module('cat.service.route', [ + 'ui.router', + 'cat.service.message', + 'cat.service.breadcrumbs', + 'cat.service.validation' +]) + /** * @ngdoc service * @name cat.service.route:catRouteServiceProvider @@ -7,6 +14,21 @@ * This service provider delegates to the $stateProvider and actually creates 2 separate routes after applying various * conventions / defaults */ + .provider('catRouteService', CatRouteServiceProvider) + + .run(['$rootScope', '$log', '$globalMessages', 'catBreadcrumbsService', 'catValidationService', + function ($rootScope, $log, $globalMessages, catBreadcrumbsService, catValidationService) { + $rootScope.$on('$stateChangeError', function () { + var exception = arguments[arguments.length - 1]; + $globalMessages.addMessage('warning', exception); + $log.warn(exception); + }); + $rootScope.$on('$stateChangeSuccess', function () { + catBreadcrumbsService.clear(); + catValidationService.clearValidationErrors(); + }); + }]); + function CatRouteServiceProvider($stateProvider) { var viewNames = []; @@ -54,6 +76,20 @@ function CatRouteServiceProvider($stateProvider) { } } + function _registerCreateState(config, name) { + var stateName = _getStateName(name, config); + var createConfig = _getCreateConfig(config, name); + $stateProvider + .state(stateName + '.create', createConfig); + } + + function _registerEditState(config, name) { + var stateName = _getStateName(name, config); + var editConfig = _getEditConfig(config, name); + $stateProvider + .state(stateName + '.edit', editConfig); + } + function _registerListState(config, name) { var stateName = _getStateName(name, config); var listConfig = _getListConfig(config, name); @@ -87,6 +123,46 @@ function CatRouteServiceProvider($stateProvider) { }; } + /** + * A helper function which basically just overrides the controller, url and template of the detail config. + * @param config + * @param name + * @returns {{templateUrl: (string), controller: string, reloadOnSearch: (boolean), resolve: {config: (object)}}} + * @private + */ + function _getCreateConfig(config, name) { + var _config = _.assign({name: name}, config); + var detailConfig = _getEditConfig(config, name); + + detailConfig.url = _config.url || '/new'; + + return detailConfig; + } + + /** + * A helper function which basically just overrides the controller, url and template of the detail config. + * @param config + * @param name + * @returns {{templateUrl: (string), controller: string, reloadOnSearch: (boolean), resolve: {config: (object)}}} + * @private + */ + function _getEditConfig(config, name) { + var _config = _.assign({name: name}, config); + + return { + url: _config.url || '/:id/edit', + templateUrl: _config.templateUrl || 'template/cat-base-edit.tpl.html', + controller: 'CatBaseEditController', + reloadOnSearch: _config.reloadOnSearch, + resolve: { + config: function ($stateParams, catViewConfigService) { + // TODO $stateParams needs to be passed from here because otherwise it's empty... + return catViewConfigService.getDetailConfig(_config, $stateParams); + } + } + }; + } + /** * A helper function for list routes which applies a few optimizations and some auto configuration. * In the current state it handles 4 settings: @@ -167,7 +243,9 @@ function CatRouteServiceProvider($stateProvider) { var listUrl = _getListUrl(baseUrl, name, config); _registerAbstractState(listUrl, stateName); + _registerCreateState(_.assign({}, config.create, viewData), name); _registerDetailState(_.assign({}, config.details, viewData), name); + _registerEditState(_.assign({}, config.list, viewData), name); _registerListState(_.assign({}, config.list, viewData), name); }; @@ -184,24 +262,3 @@ function CatRouteServiceProvider($stateProvider) { return viewNames; }; } - -angular - .module('cat.service.route', [ - 'ui.router', - 'cat.service.message', - 'cat.service.breadcrumbs', - 'cat.service.validation' - ]) - .provider('catRouteService', CatRouteServiceProvider) - .run(['$rootScope', '$log', '$globalMessages', 'catBreadcrumbsService', 'catValidationService', - function ($rootScope, $log, $globalMessages, catBreadcrumbsService, catValidationService) { - $rootScope.$on('$stateChangeError', function () { - var exception = arguments[arguments.length - 1]; - $globalMessages.addMessage('warning', exception); - $log.warn(exception); - }); - $rootScope.$on('$stateChangeSuccess', function () { - catBreadcrumbsService.clear(); - catValidationService.clearValidationErrors(); - }); - }]); \ No newline at end of file diff --git a/src/main/javascript/service/cat-view-config-service.js b/src/main/javascript/service/cat-view-config-service.js index 093b402..c333cc1 100644 --- a/src/main/javascript/service/cat-view-config-service.js +++ b/src/main/javascript/service/cat-view-config-service.js @@ -16,7 +16,7 @@ function CatViewConfigService($q, catApiService, catListDataLoadingService) { var detailPromise; var detailId = $stateParams.id; - if (detailId === 'new') { + if (detailId === 'new' || detailId === undefined) { detailPromise = $q.when(new Model()); } else { detailPromise = endpoint.get(detailId); diff --git a/src/main/resources/template/cat-base-detail.tpl.html b/src/main/resources/template/cat-base-detail.tpl.html index 1609fcb..0041ff7 100644 --- a/src/main/resources/template/cat-base-detail.tpl.html +++ b/src/main/resources/template/cat-base-detail.tpl.html @@ -4,7 +4,7 @@
-
+
+
  New
-
{{title()}}
-
New
+
{{title()}}
-
-
-
- -
-
-
-
\ No newline at end of file diff --git a/src/main/resources/template/cat-base-edit.tpl.html b/src/main/resources/template/cat-base-edit.tpl.html new file mode 100644 index 0000000..38f6311 --- /dev/null +++ b/src/main/resources/template/cat-base-edit.tpl.html @@ -0,0 +1,27 @@ + + +
+
+
{{title()}}
+
New
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/src/test/javascript/service/cat-view-config-service.spec.js b/src/test/javascript/service/cat-view-config-service.spec.js index 2967cd8..55e8aaa 100644 --- a/src/test/javascript/service/cat-view-config-service.spec.js +++ b/src/test/javascript/service/cat-view-config-service.spec.js @@ -96,26 +96,26 @@ describe('CatViewConfigService', function () { describe('.getDetailConfig', function () { it('should correctly initialize name and model', function () { - testDetailConfig({name: 'Test', model: 'model'}, {}, function (config) { + testDetailConfig({name: 'Test', model: 'model'}, {id: 42}, function (config) { expect(config.Model).toEqual('model'); expect(config.name).toEqual('Test'); }); }); it('should correctly apply controller', function () { - testDetailConfig({name: 'Test', model: 'model', controller: 'CTRL'}, {}, function (config) { + testDetailConfig({name: 'Test', model: 'model', controller: 'CTRL'}, {id: 42}, function (config) { expect(config.controller).toEqual('CTRL'); }); }); it('should correctly apply default controller', function () { - testDetailConfig({name: 'Test', model: 'model'}, {}, function (config) { + testDetailConfig({name: 'Test', model: 'model'}, {id: 42}, function (config) { expect(config.controller).toEqual('TestDetailsController'); }); }); it('should apply correct endpoint', function () { - testDetailConfig({name: 'Test', model: 'model', endpoint: 'endpoint'}, {}, function (config) { + testDetailConfig({name: 'Test', model: 'model', endpoint: 'endpoint'}, {id: 42}, function (config) { expect(config.endpoint).toEqual(mockEndpoint); }); }); @@ -128,7 +128,7 @@ describe('CatViewConfigService', function () { endpoint: { name: 'endpointname' } - }, {}, function (config) { + }, {id: 42}, function (config) { expect(config.endpoint).toEqual(mockEndpoint); }); }); @@ -143,6 +143,7 @@ describe('CatViewConfigService', function () { parents: ['parent1', 'parent2'] } }, { + id: 42, parent1Id: 1, parent2Id: 2 }, function (config) { @@ -158,7 +159,7 @@ describe('CatViewConfigService', function () { view: 'endpoint/endpoint-details-view.tpl.html' }; - testDetailConfig({name: 'Test', model: 'model', endpoint: 'endpoint'}, {}, function (config) { + testDetailConfig({name: 'Test', model: 'model', endpoint: 'endpoint'}, {id: 42}, function (config) { expect(config.templateUrls.view).toBe(resultObj.view); expect(config.templateUrls.edit).toBe(resultObj.edit); }); @@ -177,7 +178,9 @@ describe('CatViewConfigService', function () { model: 'model', endpoint: 'endpoint', additionalViewTemplate: true - }, {}, function (config) { + }, { + id: 42 + }, function (config) { expect(config.templateUrls.view.main).toBe(resultObj.view); }); @@ -187,7 +190,9 @@ describe('CatViewConfigService', function () { endpoint: 'endpoint', additionalViewTemplate: 'tabs', additionalViewTemplateTabs: 'result' - }, {}, function (config) { + }, { + id: 42 + }, function (config) { expect(config.tabs).toBe('result'); }); });