diff --git a/contrib-notes/release-process/playground-validation.md b/contrib-notes/release-process/playground-validation.md index a6757d6250..fdeec60f4a 100644 --- a/contrib-notes/release-process/playground-validation.md +++ b/contrib-notes/release-process/playground-validation.md @@ -187,6 +187,76 @@ Select the main model file and delete it - Change ACL file resource to be “org.acme.model” - Error should disappear and deploy button should be active +Reset to the basic sample network, we will test the addition and edit of an existing model file + - Select the model file + - Within editor select the edit icon + - Change the name to include illegal characters (non-alphanumeric) + - Validation error should show on click away + - Change the name to be valid, but different from original name. On click away: + - No validation errors fof file name or any associated BND files + - Name of selected file should update in side tab + - Deploy button should become active + - Click deploy + - Deploy success message should show + - Deploy button should become diabled + - Newly renamed file should be visible in side tab + - Click export + - BND should export as a BNA + - BNA should contain renamed file in models direcotory of archive + +Reset to the basic sample network, we will test the addition and edit of a new model file + - Add a new model file + - Within editor select the edit icon + - Change the name to include illegal characters (non-alphanumeric) + - Validation error should show on click away + - Change the name to be valid, but different from original name. On click away: + - No validation errors + - Name of selected file should update in side tab + - Deploy button should be active + - Click deploy + - Deploy success message should show + - Deploy button should become diabled + - Newly renamed file should be visible in side tab + - Click export + - BND should export as a BNA + - BNA should include new file in models directory of archive +- Add an existing model file from disc and repeat above steps + +Reset to the basic sample network, we will test the addition and edit of an existing script file + - Select the script file + - Within editor select the edit icon + - Change the name to include illegal characters (non-alphanumeric) + - Validation error should show on click away + - Change the name to be valid, but different from original name. On click away: + - No validation errors fof file name or any associated BND files + - Name of selected file should update in side tab + - Deploy button should become active + - Click deploy + - Deploy success message should show + - Deploy button should become diabled + - Newly renamed file should be visible in side tab + - Click export + - BND should export as a BNA + - BNA should contain renamed file in lib direcotory of archive + +Reset to the basic sample network, we will test the addition and edit of a new script file + - Add a new script file via "new script file" selection + - Within editor select the edit icon + - Change the name to include illegal characters (non-alphanumeric) + - Validation error should show on click away + - Change the name to be valid, but different from original name. On click away: + - No validation errors + - Name of selected file should update in side tab + - Deploy button should be active + - Click deploy + - Deploy success message should show + - Deploy button should become diabled + - Newly renamed file should be visible in side tab + - Click export + - BND should export as a BNA + - BNA should include new file in lib directory of archive + - Add an existing script file from disc and repeat above steps + ### Test and ID Page The test page enables testing of the currently deployed Business Network Definition, using a web runtime. The ID page enables access to resources based upon a selected ID existing within the BND. The Admin ID is a default ID, though in testing we will create new IDs and interact with resources based on the newly defined IDs that have ACL rules applied. diff --git a/packages/composer-common/lib/businessnetworkdefinition.js b/packages/composer-common/lib/businessnetworkdefinition.js index 253d5f13a2..7b9bdd0ede 100644 --- a/packages/composer-common/lib/businessnetworkdefinition.js +++ b/packages/composer-common/lib/businessnetworkdefinition.js @@ -273,7 +273,9 @@ class BusinessNetworkDefinition { let modelManager = this.getModelManager(); let modelFiles = modelManager.getModelFiles(); modelFiles.forEach(function(file) { - zip.folder('models').file(file.namespace + '.cto', file.definitions); + let fileIdentifier = file.fileName; + let fileName = fsPath.parse(fileIdentifier).base; + zip.folder('models').file(fileName, file.definitions); }); let scriptManager = this.getScriptManager(); diff --git a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts index dba5111859..9a47107f95 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.spec.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.spec.ts @@ -229,7 +229,7 @@ describe('AddFileComponent', () => { ); let file = new File([b], 'newfile.cto'); let dataBuffer = new Buffer('/**CTO File**/ namespace test'); - let mockModel = new ModelFile(mockModelManager, dataBuffer.toString(), file.name); + let mockModel = new ModelFile(mockModelManager, dataBuffer.toString(), 'models/' + file.name); component.createModel(file, dataBuffer); component.fileType.should.equal('cto'); component.currentFile.should.deep.equal(mockModel); @@ -237,7 +237,7 @@ describe('AddFileComponent', () => { })); it('should use the addModelFileName variable as the file name', async(() => { - let fileName = 'testFileName.cto'; + let fileName = 'models/testFileName.cto'; component.addModelFileName = fileName; component.businessNetwork = mockBusinessNetwork; let b = new Blob( @@ -300,7 +300,7 @@ describe('AddFileComponent', () => { namespace org.acme.model`], {type: 'text/plain'} ); - let file = new File([b], 'org.acme.model.cto'); + let file = new File([b], 'models/org.acme.model.cto'); let dataBuffer = new Buffer(`/** * New model file */ @@ -312,7 +312,7 @@ namespace org.acme.model`); component.businessNetwork = mockBusinessNetwork; component.changeCurrentFileType(); - component.currentFileName.should.equal('org.acme.model.cto'); + component.currentFileName.should.equal('models/org.acme.model.cto'); component.currentFile.should.deep.equal(mockModel); })); @@ -342,7 +342,7 @@ namespace org.acme.model`); component.businessNetwork = mockBusinessNetwork; component.changeCurrentFileType(); - component.currentFileName.should.equal('org.acme.model0.cto'); + component.currentFileName.should.equal('models/org.acme.model0.cto'); }); it('should fill in template model name indices for a cto file name', async(() => { @@ -379,7 +379,7 @@ namespace org.acme.model`); component.businessNetwork = mockBusinessNetwork; component.changeCurrentFileType(); - component.currentFileName.should.equal('org.acme.model2.cto'); + component.currentFileName.should.equal('models/org.acme.model2.cto'); })); }); diff --git a/packages/composer-playground/src/app/add-file/add-file.component.ts b/packages/composer-playground/src/app/add-file/add-file.component.ts index 6f41e3ad40..48b4b2dafe 100644 --- a/packages/composer-playground/src/app/add-file/add-file.component.ts +++ b/packages/composer-playground/src/app/add-file/add-file.component.ts @@ -24,7 +24,8 @@ export class AddFileComponent { supportedFileTypes: string[] = ['.js', '.cto']; addModelNamespace: string = 'org.acme.model'; - addModelFileName: string = 'lib/org.acme.model'; + addModelFileName: string = 'models/org.acme.model'; + addModelPath: string = 'models/'; addModelFileExtension: string = '.cto'; addScriptFileName: string = 'lib/script'; addScriptFileExtension: string = '.js'; @@ -90,14 +91,16 @@ export class AddFileComponent { createScript(file: File, dataBuffer) { this.fileType = 'js'; let scriptManager = this.businessNetwork.getScriptManager(); - this.currentFile = scriptManager.createScript(file.name || this.addScriptFileName, 'JS', dataBuffer.toString()); + let filename = file.name ? 'lib/' + file.name : this.addScriptFileName; + this.currentFile = scriptManager.createScript('lib/' + file.name || this.addScriptFileName, 'JS', dataBuffer.toString()); this.currentFileName = this.currentFile.getIdentifier(); } createModel(file: File, dataBuffer) { this.fileType = 'cto'; let modelManager = this.businessNetwork.getModelManager(); - this.currentFile = new ModelFile(modelManager, dataBuffer.toString(), file.name || this.addModelFileName); + let filename = file.name ? 'models/' + file.name : this.addModelFileName; + this.currentFile = new ModelFile(modelManager, dataBuffer.toString(), filename); this.currentFileName = this.currentFile.getFileName(); } @@ -143,7 +146,7 @@ export class AddFileComponent { namespace ${newModelNamespace}`; - this.currentFile = new ModelFile(modelManager, code, newModelNamespace + this.addModelFileExtension); + this.currentFile = new ModelFile(modelManager, code, this.addModelPath + newModelNamespace + this.addModelFileExtension); this.currentFileName = this.currentFile.getFileName(); } } diff --git a/packages/composer-playground/src/app/basic-modals/replace-confirm/replace-confirm.component.spec.ts b/packages/composer-playground/src/app/basic-modals/replace-confirm/replace-confirm.component.spec.ts index 7c376ced87..c55b0703e4 100644 --- a/packages/composer-playground/src/app/basic-modals/replace-confirm/replace-confirm.component.spec.ts +++ b/packages/composer-playground/src/app/basic-modals/replace-confirm/replace-confirm.component.spec.ts @@ -31,4 +31,17 @@ describe('DeleteComponent', () => { it('should create', () => { component.should.be.ok; }); + + describe('onInit', () => { + + it('should set default messages', () => { + + component['ngOnInit'](); + + component['headerMessage'].should.be.equal('Current definition will be replaced'); + component['mainMessage'].should.be.equal('Your Business Network Definition currently in the Playground will be removed & replaced.'); + component['supplementaryMessage'].should.be.equal('Please ensure that you have exported any current model files in the Playground.'); + }); + + }); }); diff --git a/packages/composer-playground/src/app/editor-file/editor-file.component.ts b/packages/composer-playground/src/app/editor-file/editor-file.component.ts index 2a0973c9e9..bc4ba3e607 100644 --- a/packages/composer-playground/src/app/editor-file/editor-file.component.ts +++ b/packages/composer-playground/src/app/editor-file/editor-file.component.ts @@ -120,7 +120,6 @@ export class EditorFileComponent { this.clientService.setBusinessNetworkPackageJson(packageObject); this.clientService.businessNetworkChanged$.next(true); } - this.currentError = this.clientService.updateFile(this._editorFile.id, this.editorContent, type); } catch (e) { this.currentError = e.toString(); diff --git a/packages/composer-playground/src/app/editor/editor.component.html b/packages/composer-playground/src/app/editor/editor.component.html index ccab791c01..7a77d4355f 100644 --- a/packages/composer-playground/src/app/editor/editor.component.html +++ b/packages/composer-playground/src/app/editor/editor.component.html @@ -11,7 +11,7 @@

Model File

Script File

Access Control

About

-
{{file.displayID}}
+
{{file.displayID}}

Editing package.json

-

{{deployedPackageName}}

+

{{ fileType(currentFile) }} File

+

{{ deployedPackageName }}

-
{{deployedPackageVersion}} -
-
+
{{currentFile.displayID}}
+
{{deployedPackageVersion}}
+
-
+
@@ -91,6 +92,24 @@

{{deployedPackageName}}

+
+
+
+ +
models/
+
lib/
+
+ + +
+
.cto
+
.js
+
+
+ {{fileNameError}} +
+
+
diff --git a/packages/composer-playground/src/app/editor/editor.component.scss b/packages/composer-playground/src/app/editor/editor.component.scss index 0576cbb220..2c2a72b361 100644 --- a/packages/composer-playground/src/app/editor/editor.component.scss +++ b/packages/composer-playground/src/app/editor/editor.component.scss @@ -45,7 +45,7 @@ app-editor { .business-network-details { display: flex; height:50px; - margin-bottom: 0.5rem; + margin-bottom: 0.2rem; .business-network-version { margin-right: $space-smedium; @@ -59,6 +59,35 @@ app-editor { .edit-label{ line-height: 2.5rem; font-weight:bold; + flex-shrink:1; + margin-top: 0.2rem; + } + + .edit-file-hidden{ + line-height: 2.3rem; + color: $secondary-text; + flex-shrink:1; + } + + .edit-file-label{ + line-height: 2.5rem; + font-weight: bold; + color: $primary-text; + flex-shrink: 1; + margin-right: 1rem; + font-size:14px; + } + + .edit-file-row{ + flex:2; + display:flex; + padding-bottom:$space-medium; + + .edit-file-value{ + flex-basis:50%; + display:flex; + flex-direction: column; + } } } diff --git a/packages/composer-playground/src/app/editor/editor.component.spec.ts b/packages/composer-playground/src/app/editor/editor.component.spec.ts index c75740ab6f..3ea5c69fff 100644 --- a/packages/composer-playground/src/app/editor/editor.component.spec.ts +++ b/packages/composer-playground/src/app/editor/editor.component.spec.ts @@ -111,7 +111,7 @@ describe('EditorComponent', () => { } }) }; - mockClientService.fileNameChanged$ = { + mockClientService.namespaceChanged$ = { takeWhile: sinon.stub().returns({ subscribe: (callback) => { callback('new-name'); @@ -261,7 +261,7 @@ describe('EditorComponent', () => { let mockUpdatePackage = sinon.stub(component, 'updatePackageInfo'); let mockUpdateFiles = sinon.stub(component, 'updateFiles'); - let file = {testFile: true}; + let file = {id: 'testFile', displayID: 'script.js'}; component['editorService'].setCurrentFile(file); component.ngOnInit(); @@ -335,19 +335,19 @@ describe('EditorComponent', () => { describe('setCurrentFile', () => { it('should set current file', () => { - component['currentFile'] = {displayID: 'oldFile'}; - let file = {file: 'myFile'}; + component['currentFile'] = {displayID: 'oldFile', id: 'oldID'}; + let file = {displayID: 'newFile', id: 'newID'}; component.setCurrentFile(file); component['currentFile'].should.deep.equal(file); }); it('should set current file', () => { - component['currentFile'] = {displayID: 'oldFile'}; + component['currentFile'] = {displayID: 'oldFile', id: 'oldID'}; component['editingPackage'] = true; let mockUpdatePackage = sinon.stub(component, 'updatePackageInfo'); - let file = {displayID: 'myFile'}; + let file = {displayID: 'myFile', id: 'newID'}; component.setCurrentFile(file); component['currentFile'].should.deep.equal(file); @@ -405,8 +405,10 @@ describe('EditorComponent', () => { describe('updateFiles', () => { it('should update the files', () => { mockClientService.getModelFiles.returns([ - {getNamespace: sinon.stub().returns('model 2')}, - {getNamespace: sinon.stub().returns('model 1')} + {getNamespace: sinon.stub().returns('model 2'), + getFileName: sinon.stub().returns('models/model2.cto')}, + {getNamespace: sinon.stub().returns('model 1'), + getFileName: sinon.stub().returns('models/model1.cto')}, ]); mockClientService.getScripts.returns([ @@ -432,13 +434,13 @@ describe('EditorComponent', () => { component['files'][1].should.deep.equal({ model: true, id: 'model 1', - displayID: 'models/model 1.cto', + displayID: 'models/model1.cto', }); component['files'][2].should.deep.equal({ model: true, id: 'model 2', - displayID: 'models/model 2.cto', + displayID: 'models/model2.cto', }); component['files'][3].should.deep.equal({ @@ -974,11 +976,17 @@ describe('EditorComponent', () => { component['editActive'].should.equal(true); }); - it('should make edit fields visible when true', () => { + it('should make edit package fields visible when true for README', () => { component['editActive'] = false; component['editingPackage'] = false; + component['editingPackage'] = false; component['deployedPackageName'] = 'TestPackageName'; component['deployedPackageVersion'] = '1.0.0'; + + // Specify README file + let file = {readme: true, id: 'readme', displayID: 'README.md'}; + component.setCurrentFile(file); + fixture.detectChanges(); // Expect to see "deployedPackageName" visible within class="business-network-details" @@ -1004,9 +1012,13 @@ describe('EditorComponent', () => { }); - it('should make edit fields interactable when true', () => { + it('should make edit fields interactable when true for README', () => { component['editActive'] = true; + // Specify README file + let file = {readme: true, id: 'readme', displayID: 'README.md'}; + component['currentFile'] = file; + fixture.detectChanges(); // Expect edit fields: @@ -1108,28 +1120,36 @@ describe('EditorComponent', () => { describe('fileType', () => { - it('should initialise model file parameters', () => { + it('should identify model file via parameters', () => { let testItem = {model: true, displayID: 'test_name'}; let result = component['fileType'](testItem); - result.should.equal('Model File'); + result.should.equal('Model'); }); - it('should initialise script file parameters', () => { + it('should identify script file via parameters', () => { let testItem = {script: true, displayID: 'test_name'}; let result = component['fileType'](testItem); - result.should.equal('Script File'); + result.should.equal('Script'); + }); + + it('should identify ACL file via parameters', () => { + let testItem = {acl: true, displayID: 'test_name'}; + + let result = component['fileType'](testItem); + + result.should.equal('ACL'); }); - it('should initialise unknown file parameters', () => { + it('should identify unknown file via parameters as README', () => { let testItem = {displayID: 'test_name'}; let result = component['fileType'](testItem); - result.should.equal('File'); + result.should.equal('Readme'); }); }); @@ -1259,11 +1279,11 @@ describe('EditorComponent', () => { // Create file array of length 5 let fileArray = []; - fileArray.push({acl: true, displayID: 'acl0'}); - fileArray.push({script: true, displayID: 'script0'}); - fileArray.push({script: true, displayID: 'script1'}); - fileArray.push({model: true, displayID: 'model1'}); - fileArray.push({script: true, displayID: 'script2'}); + fileArray.push({acl: true, id: 'acl file', displayID: 'acl0'}); + fileArray.push({script: true, id: 'script 0', displayID: 'script0'}); + fileArray.push({script: true, id: 'script 1', displayID: 'script1'}); + fileArray.push({model: true, id: 'model 1', displayID: 'model1'}); + fileArray.push({script: true, id: 'script 2', displayID: 'script2'}); component['files'] = fileArray; }); @@ -1328,10 +1348,14 @@ describe('EditorComponent', () => { it('should delete the correct script file', fakeAsync(() => { component['currentFile'] = component['files'][2]; + let mockSetIntialFile = sinon.stub(component, 'setInitialFile'); component.openDeleteFileModal(); tick(); + // Check innitial file set + mockSetIntialFile.should.have.been.called; + // Check services called mockClientService.businessNetworkChanged$.next.should.have.been.called; mockAlertService.successStatus$.next.should.have.been.called; @@ -1351,10 +1375,14 @@ describe('EditorComponent', () => { it('should delete the correct model file', fakeAsync(() => { component['currentFile'] = component['files'][3]; + let mockSetIntialFile = sinon.stub(component, 'setInitialFile'); component.openDeleteFileModal(); tick(); + // Check innitial file set + mockSetIntialFile.should.have.been.called; + // Check services called mockClientService.businessNetworkChanged$.next.should.have.been.called; mockAlertService.successStatus$.next.should.have.been.called; @@ -1422,4 +1450,214 @@ describe('EditorComponent', () => { index.should.equal(-1); })); }); + + describe('editFileName', () => { + + it('should prevent user creating invalid file names during edit', () => { + let invalidNames = []; + invalidNames.push('name with spaces'); + invalidNames.push('name!'); + invalidNames.push('name#'); + invalidNames.push('/name'); + invalidNames.push('name/name'); + invalidNames.push('/name'); + invalidNames.push('na]me'); + invalidNames.push('na:me'); + invalidNames.push('na`me'); + + invalidNames.forEach( (fileName) => { + component['inputFileNameArray'] = [ '', fileName, '']; + component['editFileName'](); + component['fileNameError'].should.be.equal('Error: Invalid filename, file must be alpha-numeric with no spaces'); + }); + }); + + it('should prevent edit of acl file', () => { + // Attempt edit of ACL + component['inputFileNameArray'] = ['', 'permissions', '.acl']; + component['currentFile'] = { acl: true}; + + component['editFileName'](); + component['fileNameError'].should.be.equal('Error: Unable to process rename on current file type'); + }); + + it('should prevent edit of readme file', () => { + // Attempt edit of README + component['inputFileNameArray'] = ['', 'README', '.md']; + component['currentFile'] = { readme: true }; + + component['editFileName'](); + component['fileNameError'].should.be.equal('Error: Unable to process rename on current file type'); + }); + + it('should prevent renaming file to existing file', () => { + // Attempt edit of model + component['inputFileNameArray'] = ['', 'myModelFile', '.cto']; + component['currentFile'] = { model: true, displayID: 'oldNameID.cto' }; + + component['files'] = [{displayID: 'muchRandom'}, + {displayID: 'oldNameID.cto'}, + {displayID: 'myModelFile.cto'}]; + + component['editFileName'](); + component['fileNameError'].should.be.equal('Error: Filename already exists'); + }); + + it('should not rename script file if name unchanged', () => { + // Attempt edit of script + component['inputFileNameArray'] = ['', 'myScriptFile', '.js']; + component['currentFile'] = { script: true, id: 'myScriptFile.js' }; + + component['files'] = [{id: 'muchRandom'}, + {id: 'myScriptFile.js'}, + {id: 'oldNameID'}]; + + component['editFileName'](); + }); + + it('should not rename model file if name unchanged', () => { + // Attempt edit of model + component['inputFileNameArray'] = ['', 'myModelFile', '.cto']; + component['currentFile'] = { model: true, displayID: 'myModelFile.cto' }; + + component['files'] = [{displayID: 'muchRandom'}, + {displayID: 'myModelFile.cto'}, + {displayID: 'oldNameID'}]; + + component['editFileName'](); + }); + + it('should enable script file rename by replacing script', () => { + // Should call: + // - this.clientService.replaceFile(this.currentFile.id, inputFileName, contents, 'script'); + // - this.updateFiles(); + // - this.setCurrentFile(this.files[index]); + // Should set: + // - this.dirty = true; + let mockUpdateFiles = sinon.stub(component, 'updateFiles'); + let mockSetCurrentFile = sinon.stub(component, 'setCurrentFile'); + let mockFindIndex = sinon.stub(component, 'findFileIndex'); + mockFindIndex.onCall(0).returns(-1); + mockFindIndex.onCall(1).returns(2); + + mockClientService.getScriptFile.returns({ + getContents: sinon.stub().returns('my script content') + }); + + component['inputFileNameArray'] = ['', 'myNewScriptFile', '.js']; + component['currentFile'] = { script: true, id: 'myCurrentScriptFile.js' }; + + component['files'] = [{id: 'muchRandom'}, + {id: 'myCurrentScriptFile.js'}, + {id: 'otherScriptFile.js'}, + {id: 'oldNameID'}]; + + // Call Method + component['editFileName'](); + + mockClientService.replaceFile.should.have.been.calledWith('myCurrentScriptFile.js', 'myNewScriptFile.js', 'my script content', 'script'); + mockUpdateFiles.should.have.been.called; + mockSetCurrentFile.should.have.been.calledWith({id: 'otherScriptFile.js'}); + component['dirty'].should.be.equal(true); + }); + + it('should enable model file rename by editing filename', () => { + // Should call: + // - this.clientService.replaceFile(this.currentFile.id, inputFileName, contents, 'script'); + // - this.updateFiles(); + // - this.setCurrentFile(this.files[index]); + // Should set: + // - this.dirty = true; + let mockUpdateFiles = sinon.stub(component, 'updateFiles'); + let mockSetCurrentFile = sinon.stub(component, 'setCurrentFile'); + let mockFindIndex = sinon.stub(component, 'findFileIndex'); + mockFindIndex.onCall(0).returns(-1); + mockFindIndex.onCall(1).returns(2); + + mockClientService.getModelFile.returns({ + getDefinitions: sinon.stub().returns('My ModelFile content') + }); + component['inputFileNameArray'] = ['', 'myNewModelFile', '.cto']; + component['currentFile'] = { model: true, id: 'myCurrentModelFile.cto' }; + + component['files'] = [{id: 'muchRandom'}, + {displayID: 'myCurrentModelFile.cto'}, + {displayID: 'otherModelFile.cto'}, + {id: 'oldNameID'}]; + + // Call Method + component['editFileName'](); + + mockClientService.replaceFile.should.have.been.calledWith('myCurrentModelFile.cto', 'myNewModelFile.cto', 'My ModelFile content', 'model'); + mockUpdateFiles.should.have.been.called; + mockSetCurrentFile.should.have.been.calledWith({displayID: 'otherModelFile.cto'}); + component['dirty'].should.be.equal(true); + }); + + }); + + describe('findFileIndex', () => { + + it('should find a file index by id', () => { + component['files'] = [{id: 'match0'}, + {id: 'match1'}, + {id: 'match2'}, + {id: 'match3'}, + {id: 'match4'}]; + + for (let i = 0; i < 4; i++) { + let match = component['findFileIndex'](true, 'match' + i); + match.should.be.equal(i); + } + }); + + it('should find a file index by displayID', () => { + component['files'] = [{displayID: 'match0'}, + {displayID: 'match1'}, + {displayID: 'match2'}, + {displayID: 'match3'}, + {displayIDid: 'match4'}]; + + for (let i = 0; i < 4; i++) { + let match = component['findFileIndex'](false, 'match' + i); + match.should.be.equal(i); + } + }); + + it('should find a file index by id within mixed items', () => { + component['files'] = [{id: 'match0'}, + {displayID: 'match0'}, + {id: 'match1'}, + {displayID: 'match1'}, + {id: 'match2'}, + {displayID: 'match2'}, + {id: 'match3'}, + {displayID: 'match3'}]; + let j = 0; + for (let i = 0; i < 4; i++) { + let match = component['findFileIndex'](true, 'match' + i); + match.should.be.equal(j); + j += 2; + } + }); + + it('should find a file index by displayID within mixed items', () => { + component['files'] = [{id: 'match0'}, + {displayID: 'match0'}, + {id: 'match1'}, + {displayID: 'match1'}, + {id: 'match2'}, + {displayID: 'match2'}, + {id: 'match3'}, + {displayID: 'match3'}]; + let j = 1; + for (let i = 0; i < 4; i++) { + let match = component['findFileIndex'](false, 'match' + i); + match.should.be.equal(j); + j += 2; + } + }); + + }); + }); diff --git a/packages/composer-playground/src/app/editor/editor.component.ts b/packages/composer-playground/src/app/editor/editor.component.ts index c7b3c0a583..da86c21850 100644 --- a/packages/composer-playground/src/app/editor/editor.component.ts +++ b/packages/composer-playground/src/app/editor/editor.component.ts @@ -32,7 +32,7 @@ export class EditorComponent implements OnInit, OnDestroy { private currentFile: any = null; private deletableFile: boolean = false; - private addModelNamespace: string = 'org.acme.model'; + private addModelNamespace: string = 'models/org.acme.model'; private addScriptFileName: string = 'lib/script'; private addScriptFileExtension: string = '.js'; @@ -52,6 +52,9 @@ export class EditorComponent implements OnInit, OnDestroy { private alive: boolean = true; // used to prevent memory leaks on subscribers within ngOnInit/ngOnDestory + private inputFileNameArray: string[] = null ; // This is the input 'FileName' before the currentFile is updated + private fileNameError: string = null; + constructor(private adminService: AdminService, private clientService: ClientService, private initializationService: InitializationService, @@ -84,11 +87,11 @@ export class EditorComponent implements OnInit, OnDestroy { } }); - this.clientService.fileNameChanged$.takeWhile(() => this.alive) + this.clientService.namespaceChanged$.takeWhile(() => this.alive) .subscribe((newName) => { if (this.currentFile !== null) { this.updateFiles(); - let index = this.files.findIndex((file) => file.id === newName); + let index = this.findFileIndex(true, newName); this.setCurrentFile(this.files[index]); } }); @@ -129,7 +132,7 @@ export class EditorComponent implements OnInit, OnDestroy { } setCurrentFile(file) { - if (this.currentFile === null || this.currentFile.displayID !== file.displayID || file.readme) { + if (this.currentFile === null || this.currentFile.id !== file.id || file.readme) { if (this.editingPackage) { this.updatePackageInfo(); this.editingPackage = false; @@ -144,16 +147,32 @@ export class EditorComponent implements OnInit, OnDestroy { // Set selected file this.editorService.setCurrentFile(file); this.currentFile = file; + + // Update inputFileName + this.inputFileNameArray = this.formatFileName(file.displayID); + // re-validate, since we do not persist bad files- they revert when navigated away if (this.editorFilesValidate()) { this.noError = true; } + + // remove fileError flag + this.fileNameError = null; } } + formatFileName(fullname: string): string[] { + let name = []; + let startIdx = fullname.indexOf('/') + 1; + let endIdx = fullname.lastIndexOf('.'); + name.push(fullname.substring(0, startIdx)); + name.push(fullname.substring(startIdx, endIdx)); + name.push(fullname.substring(endIdx, fullname.length)); + return name; + } + updateFiles() { let newFiles = []; - // deal with model files let modelFiles = this.clientService.getModelFiles(); let newModelFiles = []; @@ -161,11 +180,11 @@ export class EditorComponent implements OnInit, OnDestroy { newModelFiles.push({ model: true, id: modelFile.getNamespace(), - displayID: 'models/' + modelFile.getNamespace() + '.cto', + displayID: modelFile.getFileName(), }); }); newModelFiles.sort((a, b) => { - return a.displayID.localeCompare(b.displayID); + return a.displayID.localeCompare(b.displayID); }); newFiles.push.apply(newFiles, newModelFiles); @@ -215,7 +234,7 @@ export class EditorComponent implements OnInit, OnDestroy { if (!contents) { let newModelNamespace = this.addModelNamespace; let increment = 0; - while ( this.files.findIndex((file) => file.id === newModelNamespace) !== -1) { + while ( this.findFileIndex(true, newModelNamespace) !== -1) { newModelNamespace = this.addModelNamespace + increment; increment++; } @@ -232,7 +251,7 @@ export class EditorComponent implements OnInit, OnDestroy { let newFile = modelManager.addModelFile(code); this.updateFiles(); - let index = this.files.findIndex((file) => file.id === newFile.getNamespace()); + let index = this.findFileIndex(true, newFile.getNamespace()); this.setCurrentFile(this.files[index]); this.dirty = true; } @@ -263,7 +282,7 @@ export class EditorComponent implements OnInit, OnDestroy { scriptManager.addScript(script); this.updateFiles(); - let index = this.files.findIndex((file) => file.id === script.getIdentifier()); + let index = this.findFileIndex(true, script.getIdentifier()); this.setCurrentFile(this.files[index]); this.dirty = true; } @@ -370,6 +389,44 @@ export class EditorComponent implements OnInit, OnDestroy { } } + /* + * When user edits the file name (in the input box), the underlying file needs to be updated, and the BND needs to be updated + */ + editFileName() { + this.fileNameError = null; + let regEx = new RegExp(/^(([a-z_\-0-9\.]|[A-Z_\-0-9\.])+)$/); + if (regEx.test(this.inputFileNameArray[1]) === true) { + let inputFileName = this.inputFileNameArray[0] + this.inputFileNameArray[1] + this.inputFileNameArray[2]; + if ( (this.findFileIndex(false, inputFileName) !== -1) && (this.currentFile.displayID !== inputFileName) ) { + this.fileNameError = 'Error: Filename already exists'; + } else if (this.currentFile.script) { + if (this.currentFile.id !== inputFileName) { + // Replace Script + let contents = this.clientService.getScriptFile(this.currentFile.id).getContents(); + this.clientService.replaceFile(this.currentFile.id, inputFileName, contents, 'script'); + this.updateFiles(); + let index = this.findFileIndex(true, inputFileName); + this.setCurrentFile(this.files[index]); + this.dirty = true; + } + } else if (this.currentFile.model) { + if (this.currentFile.displayID !== inputFileName) { + // Update Model filename + let contents = this.clientService.getModelFile(this.currentFile.id).getDefinitions(); + this.clientService.replaceFile(this.currentFile.id, inputFileName, contents, 'model'); + this.updateFiles(); + let index = this.findFileIndex(false, inputFileName); + this.setCurrentFile(this.files[index]); + this.dirty = true; + } + } else { + this.fileNameError = 'Error: Unable to process rename on current file type'; + } + } else { + this.fileNameError = 'Error: Invalid filename, file must be alpha-numeric with no spaces'; + } + } + /* * When user edits the package version (in the input box), the package.json needs to be updated, and the BND needs to be updated */ @@ -412,7 +469,7 @@ export class EditorComponent implements OnInit, OnDestroy { } // remove file from list view - let index = this.files.findIndex((x) => { return x.displayID === deleteFile.displayID; }); + let index = this.findFileIndex(false, deleteFile.displayID); this.files.splice(index, 1); // Make sure we set a file to remove the deleted file from the view @@ -427,7 +484,7 @@ export class EditorComponent implements OnInit, OnDestroy { // Send alert this.alertService.busyStatus$.next(null); - this.alertService.successStatus$.next({title : 'Delete Successful', text : this.fileType(deleteFile) + ' ' + deleteFile.displayID + ' was deleted.', icon : '#icon-trash_32'}); + this.alertService.successStatus$.next({title : 'Delete Successful', text : this.fileType(deleteFile) + ' File ' + deleteFile.displayID + ' was deleted.', icon : '#icon-trash_32'}); } }, (reason) => { if (reason && reason !== 1) { @@ -443,11 +500,21 @@ export class EditorComponent implements OnInit, OnDestroy { fileType(resource: any): string { if (resource.model) { - return 'Model File'; + return 'Model'; } else if (resource.script) { - return 'Script File'; + return 'Script'; + } else if (resource.acl) { + return 'ACL'; + } else { + return 'Readme'; + } + } + + findFileIndex(byId: boolean, matcher) { + if (byId) { + return this.files.findIndex((file) => file.id === matcher); } else { - return 'File'; + return this.files.findIndex((file) => file.displayID === matcher); } } diff --git a/packages/composer-playground/src/app/import/import.component.html b/packages/composer-playground/src/app/import/import.component.html index 317c143ffe..fea433b21c 100644 --- a/packages/composer-playground/src/app/import/import.component.html +++ b/packages/composer-playground/src/app/import/import.component.html @@ -38,7 +38,7 @@

Import/Replace Business Network

Files: