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}}
-
{{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 @@