diff --git a/dashboard/src/app/factories/create-factory/create-factory.controller.ts b/dashboard/src/app/factories/create-factory/create-factory.controller.ts index afa8e3c375..0565796bc6 100644 --- a/dashboard/src/app/factories/create-factory/create-factory.controller.ts +++ b/dashboard/src/app/factories/create-factory/create-factory.controller.ts @@ -22,7 +22,7 @@ export class CreateFactoryCtrl { private $log: ng.ILogService; private cheAPI: CheAPI; private cheNotification: CheNotification; - private lodash: _.LoDashStatic; + private lodash: any; private $filter: ng.IFilterService; private $document: ng.IDocumentService; private isLoading: boolean; @@ -42,7 +42,7 @@ export class CreateFactoryCtrl { * Default constructor that is using resource injection * @ngInject for Dependency injection */ - constructor($location: ng.ILocationService, cheAPI: CheAPI, $log: ng.ILogService, cheNotification: CheNotification, $scope: ng.IScope, $filter: ng.IFilterService, lodash: _.LoDashStatic, $document: ng.IDocumentService) { + constructor($location: ng.ILocationService, cheAPI: CheAPI, $log: ng.ILogService, cheNotification: CheNotification, $scope: ng.IScope, $filter: ng.IFilterService, lodash: any, $document: ng.IDocumentService) { this.$location = $location; this.cheAPI = cheAPI; this.$log = $log; diff --git a/dashboard/src/app/factories/create-factory/create-factory.html b/dashboard/src/app/factories/create-factory/create-factory.html index ceb806e1ac..de926a5dd4 100644 --- a/dashboard/src/app/factories/create-factory/create-factory.html +++ b/dashboard/src/app/factories/create-factory/create-factory.html @@ -27,10 +27,12 @@ ng-trim ng-minlength="3" ng-maxlength="20" - ng-pattern="/^[ A-Za-z0-9_\-\.]+$/"> + ng-pattern="/^[ A-Za-z0-9_\-\.]+$/" + unique-factory-name="">
Factory name may contain digits, latin letters, spaces, _ , . , - and should start only with digits, latin letters or underscores
The name has to be more than 3 characters long.
The name has to be less than 20 characters long.
+
This factory name is already used.
diff --git a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts index abf8064d57..bc1b9b9fa5 100644 --- a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts +++ b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.controller.ts @@ -24,7 +24,7 @@ export class FactoryInformationController { private $location: ng.ILocationService; private $log: ng.ILogService; private $timeout: ng.ITimeoutService; - private lodash: _.LoDashStatic; + private lodash: any; private $filter: ng.IFilterService; private timeoutPromise: ng.IPromise; @@ -40,13 +40,14 @@ export class FactoryInformationController { private workspaceName: string; private stackId: string; private workspaceConfig: any; + private origName: string; /** * Default constructor that is using resource injection * @ngInject for Dependency injection */ constructor($scope: ng.IScope, cheAPI: CheAPI, cheNotification: CheNotification, $location: ng.ILocationService, $log: ng.ILogService, - $timeout: ng.ITimeoutService, lodash: _.LoDashStatic, $filter: ng.IFilterService, $q: ng.IQService, confirmDialogService: any) { + $timeout: ng.ITimeoutService, lodash: any, $filter: ng.IFilterService, $q: ng.IQService, confirmDialogService: any) { this.cheAPI = cheAPI; this.cheNotification = cheNotification; this.$location = $location; @@ -93,6 +94,7 @@ export class FactoryInformationController { this.environmentName = this.factory.workspace.defaultEnv; this.copyOriginFactory = angular.copy(this.factory); + this.origName = this.factory.name; if (this.copyOriginFactory.links) { delete this.copyOriginFactory.links; } @@ -100,7 +102,7 @@ export class FactoryInformationController { let factoryContent = this.$filter('json')(this.copyOriginFactory); if (factoryContent !== this.factoryContent) { if (!this.factoryContent) { - this.editorLoadedPromise.then((instance) => { + this.editorLoadedPromise.then((instance: any) => { this.$timeout(() => { instance.refresh(); }, 500); @@ -139,12 +141,29 @@ export class FactoryInformationController { } /** - * Update factory data. + * Update factory name. + * + * @param {string} name new factory name. + * @raram {ng.IFormController} form */ - updateFactory(): void { + updateName(name: string, form: ng.IFormController): void { + if (form.$invalid) { + return; + } + + this.copyOriginFactory.name = name; + + this.updateFactory(form); + } + + /** + * Update factory data. + * @param {ng.IFormController} form + */ + updateFactory(form: ng.IFormController): void { this.factoryContent = this.$filter('json')(this.copyOriginFactory); - if (this.factoryInformationForm.$invalid || !this.isFactoryChanged()) { + if (form.$invalid || !this.isFactoryChanged()) { return; } diff --git a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html index dbaa2e2d43..f2a04a274b 100644 --- a/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html +++ b/dashboard/src/app/factories/factory-details/information-tab/factory-information/factory-information.html @@ -16,16 +16,16 @@
+ ng-pattern="/^[ A-Za-z0-9_\-\.]+$/" + unique-factory-name="factoryInformationController.origName">
A name is required.
Factory name may contain digits, latin letters, spaces, _ , . , - and should start only @@ -33,6 +33,7 @@
The name has to be more than 3 characters long.
The name has to be less than 20 characters long.
+
This factory name is already used.
@@ -94,7 +95,7 @@ che-place-holder="Name of the workspace" aria-label="Name of the workspace" ng-model="factoryInformationController.copyOriginFactory.workspace.name" - ng-change="factoryInformationController.updateFactory()" + ng-change="factoryInformationController.updateFactory(factoryInformationForm)" required ng-minlength="3" ng-maxlength="20" @@ -123,7 +124,7 @@ MACHINE: {{machineKey}} + che-on-change="factoryInformationController.updateFactory(factoryInformationForm)"> @@ -136,7 +137,7 @@ + cdvy-on-change="factoryInformationController.updateFactory(factoryInformationForm)"> @@ -151,7 +152,7 @@ + cdvy-on-change="factoryInformationController.updateFactory(factoryInformationForm)"> diff --git a/dashboard/src/components/api/che-factory.factory.ts b/dashboard/src/components/api/che-factory.factory.ts index 452dbfe4b1..be21643199 100644 --- a/dashboard/src/components/api/che-factory.factory.ts +++ b/dashboard/src/components/api/che-factory.factory.ts @@ -532,6 +532,17 @@ export class CheFactory { return this.factoriesById.get(factoryId); } + /** + * Get the factory by factoryName and userId + * @param factoryName {string} the factory name + * @param userId {string} the user ID + * @returns factory {che.IFactory} + */ + getFactoryByName(factoryName: string, userId: string): che.IFactory { + const key = `${userId}:${factoryName}`; + return this.factoriesByName.get(key); + } + /** * Set the factory * @param factory {che.IFactory} diff --git a/dashboard/src/components/api/test/che-http-backend.ts b/dashboard/src/components/api/test/che-http-backend.ts index 701b0143b5..f86a0cce7a 100644 --- a/dashboard/src/components/api/test/che-http-backend.ts +++ b/dashboard/src/components/api/test/che-http-backend.ts @@ -469,6 +469,9 @@ export class CheHttpBackend { this.httpBackend.when('DELETE', '/api/factory/' + factory.id).respond(() => { return [200, {success: true, errors: []}]; }); + if (this.defaultUser) { + this.httpBackend.when('GET', `/api/factory/find?creator.userId=${this.defaultUser.id}&name=${factory.name}`).respond([factory]); + } allFactories.push(factory); } diff --git a/dashboard/src/components/typings/che.d.ts b/dashboard/src/components/typings/che.d.ts index e2222542a7..07b19bbf19 100755 --- a/dashboard/src/components/typings/che.d.ts +++ b/dashboard/src/components/typings/che.d.ts @@ -459,6 +459,7 @@ declare namespace che { ide?: any; button?: any; policies?: any; + links: string[]; } export interface IRegistry { diff --git a/dashboard/src/components/validator/unique-factory-name-validator.directive.ts b/dashboard/src/components/validator/unique-factory-name-validator.directive.ts new file mode 100644 index 0000000000..27540944e1 --- /dev/null +++ b/dashboard/src/components/validator/unique-factory-name-validator.directive.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; + +import {CheAPI} from '../api/che-api.factory'; + +interface IFactoryNameValidatorAsyncModelValidators extends ng.IAsyncModelValidators { + uniqueFactoryName: (modelValue: any, viewValue?: any) => ng.IPromise; +} + +interface IFactoryNameValidatorAttributes extends ng.IAttributes { + uniqueFactoryName: string; +} + +/** + * Defines a directive for checking if the factory name is not already taken + * @author Oleksii Kurinnyi + */ +export class UniqueFactoryNameValidator implements ng.IDirective { + $q: ng.IQService; + cheAPI: CheAPI; + + restrict: string = 'A'; + require: string = 'ngModel'; + + user: che.IUser; + + /** + * Default constructor that is using resource + * @ngInject for Dependency injection + */ + constructor (cheAPI: CheAPI, $q: ng.IQService) { + this.cheAPI = cheAPI; + this.$q = $q; + + this.user = this.cheAPI.getUser().getUser(); + } + + /** + * Check that the name of workspace is unique + */ + link($scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: IFactoryNameValidatorAttributes, ngModel: ng.INgModelController) { + + const asyncValidators = ngModel.$asyncValidators as IFactoryNameValidatorAsyncModelValidators; + + // validate only input element + if ('input' === element[0].localName) { + + asyncValidators.uniqueFactoryName = (modelValue: any, viewValue: any) => { + + // create promise + const deferred = this.$q.defer(); + + if (!this.user) { + deferred.reject(false); + return deferred.promise; + } + + // parent scope ? + let scopingTest = $scope.$parent; + if (!scopingTest) { + scopingTest = $scope; + } + + const currentFactoryName = scopingTest.$eval(attributes.uniqueFactoryName); + + if (!modelValue || modelValue === currentFactoryName) { + deferred.resolve(true); + } else if (this.cheAPI.getFactory().getFactoryByName(modelValue, this.user.id)) { + deferred.reject(false); + } else { + this.cheAPI.getFactory().fetchFactoryByName(modelValue, this.user.id).finally(() => { + if (this.cheAPI.getFactory().getFactoryByName(modelValue, this.user.id)) { + deferred.reject(false); + } else { + deferred.resolve(true); + } + }); + } + + // return promise + return deferred.promise; + }; + } + } + +} diff --git a/dashboard/src/components/validator/unique-factory-name-validator.spec.ts b/dashboard/src/components/validator/unique-factory-name-validator.spec.ts new file mode 100644 index 0000000000..d33ddd9b53 --- /dev/null +++ b/dashboard/src/components/validator/unique-factory-name-validator.spec.ts @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2015-2017 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +'use strict'; +import {CheFactory} from '../api/che-factory.factory'; +import {CheAPIBuilder} from '../api/builder/che-api-builder.factory'; +import {CheHttpBackend} from '../api/test/che-http-backend'; + +interface ITestRootScope extends ng.IRootScopeService { + factoryName: string; + myForm: ng.IFormController; +} + +interface ITestFormController extends ng.IFormController { + name: ng.INgModelController; +} + +/** + * Test the factory name uniqueness + * @author Oleksii Kurinnyi + */ + +describe('unique-factory-name-validator >', function() { + let $rootScope: ITestRootScope, + $compile: ng.ICompileService, + myForm: ITestFormController; + + /** + * Factory API + */ + let cheFactory: CheFactory; + + /** + * API builder. + */ + let cheAPIBuilder: CheAPIBuilder; + + /** + * Che backend + */ + let cheHttpBackend: CheHttpBackend; + + /** + * Backend for handling http operations + */ + let $httpBackend: ng.IHttpBackendService; + + beforeEach(angular.mock.module('userDashboard')); + + beforeEach(inject((_$compile_: ng.ICompileService, + _$rootScope_: ITestRootScope, + _cheFactory_: CheFactory, + _cheAPIBuilder_: CheAPIBuilder, + _cheHttpBackend_: CheHttpBackend) => { + $rootScope = _$rootScope_; + $compile = _$compile_; + cheFactory = _cheFactory_; + cheAPIBuilder = _cheAPIBuilder_; + cheHttpBackend = _cheHttpBackend_; + $httpBackend = _cheHttpBackend_.getHttpBackend(); + + })); + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + describe('validate factory name >', function() { + const factoryName1 = 'factoryName1', + loadedFactoryName = factoryName1, + factoryName2 = 'factoryName2', + uniqueName = 'uniqueName', + userId = 'userId'; + + beforeEach(() => { + // setup default user + const user = cheAPIBuilder.getUserBuilder().withId(userId).build(); + cheHttpBackend.setDefaultUser(user); + + cheHttpBackend.usersBackendSetup(); + + // setup factories + const factoryId1 = 'factoryId1', + factory1 = cheAPIBuilder.getFactoryBuilder().withId(factoryId1).withName(factoryName1).build(); + cheHttpBackend.addUserFactory(factory1); + + const factoryId2 = 'factoryId2', + factory2 = cheAPIBuilder.getFactoryBuilder().withId(factoryId2).withName(factoryName2).build(); + cheHttpBackend.addUserFactory(factory2); + + cheHttpBackend.factoriesBackendSetup(); + + cheHttpBackend.setup(); + + // setup backend + $httpBackend.whenGET( `/api/factory/find?creator.userId=${userId}&name=${uniqueName}`).respond(404, {error: 'not found'}); + + // fetch first factory + cheFactory.fetchFactoryByName(factoryName1, userId); + + // flush HTTP backend + $httpBackend.flush(); + }); + + describe('initially factory has an unique name >', () => { + + beforeEach(() => { + $rootScope.factoryName = uniqueName; + + const element = angular.element(` +
+ +
+`); + $compile(element)($rootScope); + myForm = $rootScope.myForm as ITestFormController; + }); + + it('the form should be valid >', () => { + // flush HTTP backend + $httpBackend.flush(); + + // check form (expect valid) + expect(myForm.name.$invalid).toBe(false); + expect(myForm.name.$valid).toBe(true); + }); + + it('the HTTP request should be made >', () => { + $httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${uniqueName}`); + + // flush HTTP backend + $httpBackend.flush(); + }); + + describe('change name to non-unique, factory is already downloaded >', () => { + + beforeEach(() => { + myForm.name.$setViewValue(loadedFactoryName); + }); + + it ('the form should be invalid >', () => { + // check form (expect invalid) + expect(myForm.name.$invalid).toBe(true); + expect(myForm.name.$valid).toBe(false); + }); + + it('the HTTP request should not be made >', () => { + spyOn(cheFactory, 'fetchFactoryByName'); + expect(cheFactory.fetchFactoryByName).not.toHaveBeenCalled(); + }); + + }); + + describe('change name to non-unique, factory is not downloaded yet >', () => { + + it ('the form should be invalid >', () => { + myForm.name.$setViewValue(factoryName2); + + // flush HTTP backend + $httpBackend.flush(); + + // check form (expect invalid) + expect(myForm.name.$invalid).toBe(true); + expect(myForm.name.$valid).toBe(false); + }); + + it('the HTTP request should be made >', () => { + $httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${factoryName2}`); + + myForm.name.$setViewValue(factoryName2); + + // flush HTTP backend + $httpBackend.flush(); + }); + + }); + + }); + + describe('initially factory has a non-unique name, factory is already downloaded >', () => { + + beforeEach(() => { + $rootScope.factoryName = loadedFactoryName; + + const element = angular.element(` +
+ +
+`); + $compile(element)($rootScope); + myForm = $rootScope.myForm as ITestFormController; + + $rootScope.$digest(); + }); + + it('the form should be invalid >', () => { + // check form (expect invalid) + expect(myForm.name.$invalid).toBe(true); + expect(myForm.name.$valid).toBe(false); + }); + + it('the HTTP request should not be made >', () => { + spyOn(cheFactory, 'fetchFactoryByName'); + expect(cheFactory.fetchFactoryByName).not.toHaveBeenCalled(); + }); + + describe('change name to unique> ', () => { + + it ('the form should be valid >', () => { + myForm.name.$setViewValue(uniqueName); + + // flush HTTP backend + $httpBackend.flush(); + + // check form (expect valid) + expect(myForm.name.$invalid).toBe(false); + expect(myForm.name.$valid).toBe(true); + }); + + it('the HTTP request should be made >', () => { + $httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${uniqueName}`); + + myForm.name.$setViewValue(uniqueName); + + // flush HTTP backend + $httpBackend.flush(); + }); + + }); + + }); + + describe('initially factory has a non-unique name, factory is not downloaded yet >', () => { + + beforeEach(() => { + $rootScope.factoryName = factoryName2; + + const element = angular.element(` +
+ +
+`); + $compile(element)($rootScope); + myForm = $rootScope.myForm as ITestFormController; + }); + + it('the form should be invalid >', () => { + // flush HTTP backend + $httpBackend.flush(); + + // check form (expect invalid) + expect(myForm.name.$invalid).toBe(true); + expect(myForm.name.$valid).toBe(false); + }); + + it('the HTTP request should be made >', () => { + $httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${factoryName2}`); + + // flush HTTP backend + $httpBackend.flush(); + }); + + describe('change name to unique> ', () => { + + it ('the form should be valid >', () => { + myForm.name.$setViewValue(uniqueName); + + // flush HTTP backend + $httpBackend.flush(); + + // check form (expect valid) + expect(myForm.name.$invalid).toBe(false); + expect(myForm.name.$valid).toBe(true); + }); + + it('the HTTP request should be made >', () => { + $httpBackend.expectGET(`/api/factory/find?creator.userId=${userId}&name=${uniqueName}`); + + myForm.name.$setViewValue(uniqueName); + + // flush HTTP backend + $httpBackend.flush(); + }); + + }); + + }); + + }); + +}); diff --git a/dashboard/src/components/validator/validator-config.ts b/dashboard/src/components/validator/validator-config.ts index 063e8a4d6b..b572338894 100644 --- a/dashboard/src/components/validator/validator-config.ts +++ b/dashboard/src/components/validator/validator-config.ts @@ -18,18 +18,20 @@ import {UniqueStackNameValidator} from './unique-stack-name-validator.directive' import {CityNameValidator} from './city-name-validator.directive'; import {CustomAsyncValidator} from './custom-async-validator.directive'; import {UniqueTeamNameValidator} from './unique-team-name-validator.directive'; +import {UniqueFactoryNameValidator} from './unique-factory-name-validator.directive'; export class ValidatorConfig { constructor(register: che.IRegisterService) { - register.directive('gitUrl', GitUrlValidator) - .directive('cityNameValidator', CityNameValidator) - .directive('uniqueProjectName', UniqueProjectNameValidator) - .directive('uniqueWorkspaceName', UniqueWorkspaceNameValidator) - .directive('customValidator', CustomValidator) - .directive('customAsyncValidator', CustomAsyncValidator) - .directive('uniqueStackName', UniqueStackNameValidator) - .directive('uniqueTeamName', UniqueTeamNameValidator); + register.directive('gitUrl', GitUrlValidator); + register.directive('cityNameValidator', CityNameValidator); + register.directive('uniqueProjectName', UniqueProjectNameValidator); + register.directive('uniqueWorkspaceName', UniqueWorkspaceNameValidator); + register.directive('customValidator', CustomValidator); + register.directive('customAsyncValidator', CustomAsyncValidator); + register.directive('uniqueStackName', UniqueStackNameValidator); + register.directive('uniqueTeamName', UniqueTeamNameValidator); + register.directive('uniqueFactoryName', UniqueFactoryNameValidator); } } diff --git a/dashboard/src/components/widget/input/che-input.directive.ts b/dashboard/src/components/widget/input/che-input.directive.ts index fe743a9e6f..49979b6f79 100644 --- a/dashboard/src/components/widget/input/che-input.directive.ts +++ b/dashboard/src/components/widget/input/che-input.directive.ts @@ -22,6 +22,7 @@ interface ICheInputAttrs extends ng.IAttributes { cheReadonly: string; cheDisabled: string; cheWidth: string; + ngChange: string; } /** @@ -53,13 +54,12 @@ export class CheInput implements ng.IDirective { placeHolder: '@chePlaceHolder', pattern: '@chePattern', myForm: '=cheForm', - isChanged: '&ngChange', + isChanged: '&?ngChange', readonly: '=cheReadonly', disabled: '=cheDisabled' }; } - /** * Template for the current toolbar * @param element {ng.IAugmentedJQuery} @@ -85,6 +85,9 @@ export class CheInput implements ng.IDirective { if (attrs.cheDisabled) { template = template + ' ng-disabled="disabled"'; } + if (attrs.ngChange) { + template = template + ' ng-change="isChanged({$value: valueModel})" '; + } template = template + ' ng-trim="false" ng-model="valueModel" >' + '' + '' @@ -107,6 +110,9 @@ export class CheInput implements ng.IDirective { if (attrs.cheDisabled) { template = template + ' ng-disabled="disabled"'; } + if (attrs.ngChange) { + template = template + ' ng-change="isChanged({$value: valueModel})" '; + } template = template + '>'; if (attrs.cheWidth === 'auto') { template = template + '
{{valueModel ? valueModel : placeHolder}}
'; @@ -191,17 +197,5 @@ export class CheInput implements ng.IDirective { element.removeClass(desktopPristineClass); } }); - - const ngChange = 'ngChange'; - if (!attrs.$attr[ngChange]) { - return; - } - - // for ngChange attribute only - $scope.$watch(() => { - return $scope.valueModel; - }, () => { - $scope.isChanged(); - }); } }