This is the continuation of the blog started here.
In this second part we are going to see how to create a new SAP Web IDE template after having already created our first plugin and a plugin repository in the previous part.
Let's continue then with the other steps.
Step 08 - Create a second plugin for extending an existing template
A plugin structure needs to be created even in the case you want to extend an existing template with some new features.
1 - Open SAP Web IDE. Do File --> New --> Project from Template
2 - Select the Empty Plugin template and click Next
3 - Enter the name of the project (i.e. “templatepluginproject”) and click Next
4 - Enter the name of the plugin and a short description; please notice that this time we are NOT including the sample implementation code because we don’t need it. We just need the plugin structure in order to host our template. Click Finish
5 - Now we can extend the template inside this new plugin. Right click on the plugin's name in the Project Explorer and choose New --> Template from Existing Template
6 - Provide the name for the new template and a short description. Let the type for this template be “project” and choose to create a new category where this template will be put into. Then click Next
7 - Select one of the available templates and click Next. In this case we are choosing SAPUI5 Application
8 - Click on Finish
9 - A new template ("mynicetemplate") has been added in this second plugin. This template has its own folder structure. If you click on the plugin.json file in this new plugin project, you see that some extra code has been automatically added for the template configuration
10 - As a last step here, add the services filesystem.documentProvider, projectType and setting.project in the requires section of the plugin.json file. These are required in order to use the template. Then save the file. Don't forget to put a comma just after the templateCustomizationStep service when adding the new lines
{
"name": "templateplugin",
"description": "This is a plugin for templates",
"i18n": "templateplugin/i18n/i18n",
"requires": {
"services": [
"template",
"templateCustomizationStep",
"filesystem.documentProvider",
"projectType",
"setting.project"
]
},
...
11 - You have successfully created your second plugin for extending an existing template.
Step 09 - Make some changes and test the new template
Let’s make some easy changes to the template so that we can see that we can really change it according to our requirements. We will:
- change the sample screenshot shown during the wizard procedure
- change the default view name from View1 to Main
1 - Right click on the image subfolder inside the <plugin project>/<template project> folder and choose Import --> From File System
2 - Browse for a new image file and click on OK. I've previously prepared a new .png file named "flower.png" for this example
3 - Once the new file has been uploaded, you can delete the old one by right clicking on it and choosing Delete
4 - Open the plugin.json file and change the name of the preview image to be the same of the uploaded image, then save the file
5 - Now let's try to change the name of the only view which is created during the wizard of the SAPUI5 Application template. The default name is View1, we want to change it to be Main. Open the mynicetemplate.js file and change the viewName variable from View1 to Main, then save the file
6 - Open the model.json file and change the value at line 53 from View1 to Main, then save the file
7 - Run the plugin and, in the Debug Mode tab, try to create a new application from template. You should be able to select the new mynicecategory category and see the mynicetemplate. Click Next
8 - Going forward with the next screens, you should be also able to see that the name for the default view is now set to Main
9 - You have successfully tested the new template, now you can close the tab where SAP Web IDE is running in debug mode.
Step 10 - Copy the new plugin in the repository folder
We need simply to copy the new plugin under the plugin repository folder.
1 - Select the plugin project and right click on it. Then choose Copy
2 - Select the plugin repository folder, right click on it and choose Paste
3 - At the end you should have two plugins in your plugin repository folder
Step 11 - Update the catalog.json file
Now that we have added this new plugin to the repository we have also to update the catalog.json file in order to make it available.
1 - Double click on the catalog.json file in the project explorer
2 - Replace the code in the editor with this one and save the file; this will declare the new plugin in the repository.
{
"name" : "Big Plugin Repository",
"plugins" : [
{
"name" : "coolplugin",
"description" : "This is my cool plugin",
"path" : "/coolpluginproject",
"version" : "1.0.0"
},
{
"name" : "templateplugin",
"description" : "This is my template plugin",
"path" : "/templatepluginproject",
"version" : "1.0.0"
}
]
}
NOTE: be careful if you copy & paste this code because it could be pasted with wrong characters
3 - You have successfully updated your catalog.json file
Step 12 - Update the project repository on SAP HANA Cloud
Since we have done some changes to the project repository in SAP Web IDE, we need also to update the project repository on SAP HANA Cloud.
1 - Right click on the plugin repository project in the project explorer and click on Deploy --> Deploy to SAP HANA Cloud Platform
2 - Enter your password if required and click on Login
3 - Select Update an existing application and let the version be the proposed one. Click on Deploy
4 - Click on Close when finished
5 - You have successfully updated the project on SAP HANA Cloud.
Step 13 - Activate the plugin in SAP Web IDE
The final step is to activate this second plugin.
1 - Reopen or refresh SAP Web IDE
2 - Click on the Settings button on in the left side toolbar and select the Plugins branch. Choose the plugin repository("My Plugin Repository") you have created and enable this second plugin ("templateplugin"), then click on Save
3 - Click on Refresh
4 - If you try now to create a new project from templates you should be able to find the new template
5 - Congratulations! You have successfully extended an existing template.
Appendix
In this appendix I would like to give you some further details regarding how a plugin project is structured.
The file and folder structure is created by the wizard template according to the Plugin File and Folder Structure paradigm, which is something like this:
<plugin_name> - the plugin root folder
|command - command implementations
|service - service implementations & service interfaces
|lib - open source libs
|image - all image files
|css - all css files
|i18n - all internationalization files
|model - all model classes
|control - all UI5 controls
|view - all UI5 views
|Plugin.js - the plugin module
|plugin.json - the plugin configuration file
The plugin.json file
As you can see in the plugin folder's root there is a file named plugin.json. It contains:
- the name of the plugin class
- the dependencies of the plugin
- the inherent service configurations for the plugin
In general, its content are settings inherent to the plugin not likely to be changed by an administrator or by the user via personalization: this file is used to tell to SAP Web IDE what services are needed by the plugin and what services the plugin itself provides. A plugin is defined by this plugin.json file with the following properties:
Property | Description |
---|---|
name:<string> | the unique name of the plugin (e.g. sap.watt.uitools.myplugin) |
description:<string> | the plugin's description |
i18n:<string> | optional. The path to a i18n file, which consists of a namespace identifier, a folder name (normally i18n) and a file name (e.g. sap.watt.uitools.myplugin/i18n/i18n) |
module:<string> | optional. Path to a module that is used as a private event handler target of the plugin. By convention this name must be named Plugin.js |
requires:<object> | all required dependencies of the plugin are listed here.
|
provides:<object> | public contributions of the plugin to the overall system. The following contributions are possible:
|
configures:<object> | the plugin configurations
|
subscribes:<object> | subscriptions to events of required or provided services, they can be only made to the plugin module or provided services |
If you open the existing plugin.json file, you see that it has exactly this structure:
{
"name": "coolplugin",
"description": "This is a cool plugin",
"i18n": "coolplugin/i18n/i18n",
"requires": {
"services": [
"usernotification",
"log",
"command",
"commandGroup"
]
},
"provides": {
"services": {
"sample": {
"implements": "coolplugin.service.Sample",
"module": "coolplugin/service/Sample"
}
},
"interfaces": {
"coolplugin.service.Sample": "coolplugin/service/Sample"
}
},
"configures": {
"services": {
"command:commands": [{
"id": "coolplugin.helloWorld",
"label": "{i18n>command_helloWorld}",
"service": "coolplugin/command/HelloWorld"
}],
"commandGroup:groups": [{
"id": "edit.sample",
"label": "{i18n>commandgroup_sample}"
}, {
"id": "edit.sample.helloWorld"
}],
"commandGroup:items": [{
"parent": "edit",
"type": "menu",
"group": "edit.sample",
"prio": 100
}, {
"parent": "edit.sample",
"type": "inline",
"group": "edit.sample.helloWorld",
"prio": 10
}, {
"parent": "edit.sample.helloWorld",
"type": "action",
"command": "coolplugin.helloWorld",
"prio": 10
}]
}
},
"subscribes": {
"sample:notificationDisplayed": "sample:onAfterNotificationDisplayed"
}
}
The first information we have to provide is the name of the plugin, a short description and the name of the file containing the internationalization strings. This file is normally located under the i18n folder and has the following structure:
#__ldi.translation.uuid=e27c95ce-405c-406e-1c47-71995165ee23
# XTXT:
sample_helloMessage = Hello {0}!
# XMIT:
command_helloWorld = Welcome
# XMIT:
commandgroup_sample = Greetings
The first line contains an ID to uniquely identify the translation file. All the other lines are some comments (starting with “#”), a unique parameter that identifies the command and the value associated to that parameter. You can have as many i18n files as the languages you want for your plugin. In the source code of your plugin, when the parser encounters a string like "label": "{i18n>command_helloWorld}", it searches in the i18n files of your language for the string command_helloWorld and replaces it with the corresponding value, which is in our case “Welcome”. This is how the translation mechanism works.
The second block to examine is the requires section. In this section it’s specified what services this plugin requires.
The plugin in this particular example requires
- usernotification - because it wants to count how many times the greeting message is shown
- log - because it wants to print the number of the shown messages to the console
- command - because we need to add a command to the menu
- commandGroup - because we want to add a new group "requires":
"requires": {
"services": [
"usernotification",
"log",
"command",
"commandGroup"
]
},
Which services will this plugin provide? We define this in the following section. This section specifies the names of the services that will be later available to other plugins in the “context.service”, the interfaces that are implemented and the module with the implementation. The interfaces section is important because it also specifies the path to the service. Notice that the strings are case sensitive.
In this example, our plugin will provide a service named “sample”, which implements the interface described in the Sample.json file and uses the module Sample.js file, both located under the service folder of the plugin.
"provides": {
"services": {
"sample": {
"implements": "coolplugin.service.Sample",
"module": "coolplugin/service/Sample"
}
},
"interfaces": {
"coolplugin.service.Sample": "coolplugin/service/Sample"
}
},
The configures section defines two things: the command that we are going to call from the menu item and the menu item itself. The command we are going to execute has the id = myfirstplugin.helloWorld and is located under the command folder in the Sample.js file. We will see this file later. The command will be inserted in the Edit menu under a subgroup labeled Sample and it will appear with the label defined in the i18n file by the label command_helloWorld. The priority of 100 means that it will be placed after all other commands with lower priority.
"configures": {
"services": {
"command:commands": [{
"id": "coolplugin.helloWorld",
"label": "{i18n>command_helloWorld}",
"service": "coolplugin/command/HelloWorld"
}],
"commandGroup:groups": [{
"id": "edit.sample",
"label": "{i18n>commandgroup_sample}"
}, {
"id": "edit.sample.helloWorld"
}],
"commandGroup:items": [{
"parent": "edit",
"type": "menu",
"group": "edit.sample",
"prio": 100
}, {
"parent": "edit.sample",
"type": "inline",
"group": "edit.sample.helloWorld",
"prio": 10
}, {
"parent": "edit.sample.helloWorld",
"type": "action",
"command": "coolplugin.helloWorld",
"prio": 10
}]
}
},
The latest section is the one related to the subscribes. It means that every time a notification is displayed the onAfterNotificationDisplayed event is fired and the inner code of this event is executed.
"subscribes": {
"sample:notificationDisplayed": "sample:onAfterNotificationDisplayed"
}
The command file (HelloWorld.js)
When you click on the menu Edit --> Greetings --> Welcome the file that is executed is located under the command folder and it’s named HelloWorld.js.
There are three functions that we need to implement:
- execute: this will call the required function (“sayHello”) inside the implementation file (Sample.js) in order to execute the command;
- isAvailable: this function specifies if the command needs to be always available or if it’s available only under certain conditions: in this case we have made it always available. This means that it will be always visible in the menu;
- isEnabled: this function specifies if the command needs to be always enabled or if it’s enabled only under certain conditions: in this case we have made it always enabled.
/**
* A command sample for calling the 'sample' service.
*
* The command is added to the menu bar at 'Tools->Sample->Hello World' as defined in the plugin.json file.
*/
define({
execute: function() {
return this.context.service.sample.sayHello("World");
},
isAvailable: function() {
return true;
},
isEnabled: function() {
return true;
}
});
The interface file (Sample.json)
This file is located under the service folder. In this file are exposed all the methods that the plugin’s service needs to implement. In this case for example the service needs to implement the following two functions:
- sayHello: to display the greeting message
- getNotificationCount: to get the number of the times the greeting is displayed on the screen.
Apart these two methods, the service will have to implement the handler for the notificationDisplayed event.
{
"name": "coolplugin.service.Sample",
"description": "The sample service interface",
"methods": {
"sayHello": {
"description": "Display a greeting message notification",
"params": [{
"name": "sName",
"type": "string",
"description": "The name of the user to greet"
}]
},
"getNotificationCount": {
"description": "Get the number of greeting notifications displayed so far",
"returns": {
"type": "number",
"description": "Number of greeting notifications displayed so far"
}
}
},
"events": {
"notificationDisplayed": {
"params": [{
"name": "notificationCount",
"type": "number",
"description": "The number of greeting notifications displayed so far"
}]
}
}
}
The service implementation file (Sample.js)
This file is located under the service folder as well. It contains a service implementation for displaying a greeting notification and counting the number of alerts displayed. The service provides a public API, which is defined in its interface (in this example, Sample.json file) and can be used by other plugins. Every method call on a service is asynchronous and returns a Q-promise. If not done explicitly by the method, the return value is automatically wrapped with a promise object. Please refer to the following guide for further information about asynchronous Javascript (http://documentup.com/kriskowal/q/).
Other services (which are required by this service plugin, as defined in the plugin.json file) can be accessed using this.context.service property.
A service can fire events that are defined in its interface. These events can be handled by any other service. A service can also handle events from any other service (including its own). The events subscription along with the handler methods must be defined in the plugin.json file.
In particular in this file we have the following functions implemented:
- init: this function is automatically executed when the service is initialized;
- sayHello: this is the function which does the job of showing the popup message and increasing the counter of the times when the message is displayed;
- getNotificationCount: returns the number of times the popup message is shown;
- onAfterNotificationDisplayed: each time the popup message is displayed an event is fired: this event takes care of displaying the number of notifications in the log console.
define({
_iNotificationCount: null,
init: function() {
this._iNotificationCount = 0;
},
sayHello: function(sName) {
var that = this;
this._iNotificationCount++;
var sMessage = this.context.i18n.getText("i18n", "sample_helloMessage", [sName]);
// Display greeting notification and fire event
return this.context.service.usernotification.info(sMessage).then(function() {
return that.context.event.fireNotificationDisplayed({
notificationCount: that.getNotificationCount()
});
});
},
getNotificationCount: function() {
return this._iNotificationCount;
},
onAfterNotificationDisplayed: function(oEvent) {
var iCount = oEvent.params.notificationCount;
// Display log message to the SAP River RDE console (accessed via 'View->Console' menu)
// Log messages don't need to be translatable
this.context.service.log.info("Sample", "Number of Hello notifications shown so far: " + iCount, ["user"]).done();
}
});
Good luck with SAP Web IDE plugin development!