Applicable: | This tutorial applies to Confluence 5.9.1 and higher. |
Level of experience: | Intermediate. |
In this tutorial, you’ll learn how to create an intermediate blueprint plugin for your Confluence instance. Developing for Confluence Cloud? Learn how to create a blueprint for Confluence Cloud
If you successfully completed Write a simple Confluence blueprint plugin you have a functional but basic blueprint plugin. In this tutorial, you create a blueprint that populates its template with data. You can populate a template by implementing a ContextProvider class in your plugin. You can also populate a template by creating an interactive wizard using JavaScript. In this tutorial, you do both.
Before starting this tutorial, be sure you've completed Write a simple Confluence blueprint plugin tutorial first.
If you want to skip ahead or check your work when you finish, you can find the plugin source code on Atlassian Bitbucket. Alternatively, you can download the source as a ZIP archive. To clone the repository, run the following command:
1 2git clone https://bitbucket.org/atlassian_tutorial/confluence-populated-blueprint.git
About these instructions
You can use any supported combination of operating system and IDE to create this plugin. These instructions were written using IntelliJ IDEA 2017.2 on macOS Sierra. If you are using another operating system or IDE combination, you should use the equivalent operations for your specific environment.
This tutorial was last tested with Confluence 6.7.1 using the Atlassian SDK 6.3.10.
In this step, you'll add a variable to the template you created in the Write a simple Confluence blueprint plugin. If you don't have that code, you can can download the source as a ZIP archive of the project to use.
Open Terminal and navigate to the root of the project.
In the src/main/resources/templates/mytemplate.xml
file replace the Enter your name here
text with a variable:
1 2<td><at:var at:name="vName"/></td>
To make it instructional, add a placeholder
around the Enter today's date here
text.
1 2<td><ac:placeholder>Enter today's date here</ac:placeholder></td>
The following code snippet shows the table with the previous example added.
1 2<table> <tbody> <tr> <th>Name</th> <th>Date</th> </tr> <tr> <td><at:var at:name="vName"/></td> <td><ac:placeholder>Enter today's date here</ac:placeholder></td> </tr> </tbody> </table>
Save and close the file.
Confluence does not support adding <at:var>
elements inside of other <at:>
namespace elements. For example, an <at:var>
inside
a macro parameter renders correctly for the end-user creating a page, but an admin subsequently editing the template will not be able
to edit the variable as the macro will not render.
The recommended workaround is to make the entire macro element an <at:var>
in the template. To correctly insert XHTML from a variable into the template, you'll need to define the variable with an extra "rawxhtml
" attribute, like so:
1 2<at:var at:name="myNameWithSomeXhtmlInIt" at:rawxhtml="true"/>
If the at:rawxhtml
attribute is not present, the system escapes all XHTML in the attribute value when rendering.
In this step, you'll add new dependencies to your pom.xml
file. The confluence-create-content-plugin
provides the blueprint API.
To interact with the API, you need to add a dependency in your project's pom.xml
.
In the pom.xml
file in your project's root, add dependency
element to the confluence-create-content-plugin
artifact.
Make sure you added it in the <dependencies/>
section.
1 2<dependency> <groupId>com.atlassian.confluence.plugins</groupId> <artifactId>confluence-create-content-plugin</artifactId> <version>${create.content.version}</version> <scope>provided</scope> </dependency>
Scroll to file's end and find the <properites/>
block.
Add the create-content-version
element.
1 2<create.content.version>7.0.0</create.content.version>
Here is example of the whole pom.xml
file, including the previous element.
1 2<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.plugins.tutorial.confluence.simplebp</groupId> <artifactId>simplebp</artifactId> <version>1.0-SNAPSHOT</version> <organization> <name>Example Company</name> <url>http://www.example.com/</url> </organization> <name>simplebp</name> <description>This is the com.example.plugins.tutorial.confluence.simplebp:simplebp plugin for Atlassian Confluence.</description> <packaging>atlassian-plugin</packaging> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.10</version> <scope>test</scope> </dependency> <dependency> <groupId>com.atlassian.confluence</groupId> <artifactId>confluence</artifactId> <version>${confluence.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.atlassian.confluence.plugins</groupId> <artifactId>confluence-create-content-plugin</artifactId> <version>${create.content.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.atlassian.plugin</groupId> <artifactId>atlassian-spring-scanner-annotation</artifactId> <version>${atlassian.spring.scanner.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.atlassian.plugin</groupId> <artifactId>atlassian-spring-scanner-runtime</artifactId> <version>${atlassian.spring.scanner.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> <scope>provided</scope> </dependency> <!-- WIRED TEST RUNNER DEPENDENCIES --> <dependency> <groupId>com.atlassian.plugins</groupId> <artifactId>atlassian-plugins-osgi-testrunner</artifactId> <version>${plugin.testrunner.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.ws.rs</groupId> <artifactId>jsr311-api</artifactId> <version>1.1.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.2.2-atlassian-1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>com.atlassian.maven.plugins</groupId> <artifactId>maven-confluence-plugin</artifactId> <version>${amps.version}</version> <extensions>true</extensions> <configuration> <productVersion>${confluence.version}</productVersion> <productDataVersion>${confluence.data.version}</productDataVersion> <enableQuickReload>true</enableQuickReload> <enableFastdev>false</enableFastdev> <!-- See here for an explanation of default instructions: --> <!-- https://developer.atlassian.com/docs/advanced-topics/configuration-of-instructions-in-atlassian-plugins --> <instructions> <Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key> <!-- Add package to export here --> <Export-Package> com.example.plugins.tutorial.confluence.simplebp.api, </Export-Package> <!-- Add package import here --> <Import-Package> org.springframework.osgi.*;resolution:="optional", org.eclipse.gemini.blueprint.*;resolution:="optional", * </Import-Package> <!-- Ensure plugin is spring powered --> <Spring-Context>*</Spring-Context> </instructions> </configuration> </plugin> <plugin> <groupId>com.atlassian.plugin</groupId> <artifactId>atlassian-spring-scanner-maven-plugin</artifactId> <version>${atlassian.spring.scanner.version}</version> <executions> <execution> <goals> <goal>atlassian-spring-scanner</goal> </goals> <phase>process-classes</phase> </execution> </executions> <configuration> <scannedDependencies> <dependency> <groupId>com.atlassian.plugin</groupId> <artifactId>atlassian-spring-scanner-external-jar</artifactId> </dependency> </scannedDependencies> <verbose>false</verbose> </configuration> </plugin> </plugins> </build> <properties> <confluence.version>6.7.1</confluence.version> <confluence.data.version>6.7.1</confluence.data.version> <create.content.version>7.0.0</create.content.version> <amps.version>6.3.15</amps.version> <plugin.testrunner.version>1.2.3</plugin.testrunner.version> <atlassian.spring.scanner.version>1.2.13</atlassian.spring.scanner.version> <!-- This key is used to keep the consistency between the key in atlassian-plugin.xml and the key to generate bundle. --> <atlassian.plugin.key>${project.groupId}.${project.artifactId}</atlassian.plugin.key> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> </project>
Save and close the pom.xml
file.
In this step, you create a simple implementation of a com.atlassian.plugin.web.ContextProvider
interface by extending the com.atlassian.confluence.plugins.createcontent.api.contextproviders.AbstractBlueprintContextProvider
. You can inject any components available to the Confluence plugin system into your ContextProvider
. After you add the provider Java code, you need to specify where it is located for the plugin system. You do this in the content-template
module of your atlassian-plugin.xml
file.
Create a MyContextProvider.java
file in the src/main/java/com/example/plugins/tutorial/confluence/simplebp
directory.
Your class should extend com.atlassian.confluence.plugins.createcontent.api.contextproviders.AbstractBlueprintContextProvider
. The AbstractBlueprintContextProvider
implements ContextProvider
and provides additional Blueprint-specific behaviour.
1 2package com.example.plugins.tutorial.confluence.simplebp; import com.atlassian.confluence.plugins.createcontent.api.contextproviders.AbstractBlueprintContextProvider; import com.atlassian.confluence.plugins.createcontent.api.contextproviders.BlueprintContext; public class MyContextProvider extends AbstractBlueprintContextProvider { }
Override the updateBlueprintContext()
method:
1 2package com.example.plugins.tutorial.confluence.simplebp; import com.atlassian.confluence.plugins.createcontent.api.contextproviders.AbstractBlueprintContextProvider; import com.atlassian.confluence.plugins.createcontent.api.contextproviders.BlueprintContext; public class MyContextProvider extends AbstractBlueprintContextProvider { @Override protected BlueprintContext updateBlueprintContext(BlueprintContext blueprintContext) { blueprintContext.put("vName", "Sherlock"); return blueprintContext; } }
Save and close the file.
Open the atlassian-plugin.xml
file and find the content-template
module. Add a context-provider
sub-element.
1 2<content-template key="simplebp-template" i18n-name-key="my.blueprint.title"> <resource name="template" type="download" location="/templates/mytemplate.xml" /> <context-provider class="com.example.plugins.tutorial.confluence.simplebp.MyContextProvider"/> </content-template>
The following code sample shows the full file, including previous sub-element.
1 2<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2"> <plugin-info> <description>${project.description}</description> <version>${project.version}</version> <vendor name="${project.organization.name}" url="${project.organization.url}" /> <param name="plugin-icon">images/pluginIcon.png</param> <param name="plugin-logo">images/pluginLogo.png</param> </plugin-info> <!-- add our i18n resource --> <resource type="i18n" name="i18n" location="simplebp"/> <!-- add our web resources --> <web-resource key="simplebp-resources" name="simplebp Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <resource type="download" name="simplebp.css" location="/css/simplebp.css"/> <resource type="download" name="simplebp.js" location="/js/simplebp.js"/> <resource type="download" name="images/" location="/images"/> <context>simplebp</context> </web-resource> <!-- Blueprint --> <blueprint key="my-blueprint" content-template-key="simplebp-template" index-key="my-index" i18n-name-key="my.blueprint.name"/> <!-- Add to the Create Menu --> <web-item key="create-by-sample-template" i18n-name-key="my.create-link.title" section="system.create.dialog/content"> <description key="my.create-link.description" /> <resource name="icon" type="download" location="/images/myblueprint.png" /> <param name="blueprintKey" value="my-blueprint" /> </web-item> <!-- Template for Blueprint --> <content-template key="simplebp-template" i18n-name-key="my.blueprint.title"> <resource name="template" type="download" location="/templates/mytemplate.xml" /> <context-provider class="com.example.plugins.tutorial.confluence.simplebp.MyContextProvider"/> </content-template> </atlassian-plugin>
Save and close the file.
At this point, you've added some instructional text, a form field, and a way to fill out the field. Now is a good point to test your code.
To start a local Confluence instance, run the following command.
1 2atlas-run
This command takes a minute or so to run. It builds your plugin code, starts a Confluence instance, and installs your plugin. Once the process completes, you'll see a something similar to the following message:
1 2[INFO] confluence started successfully in 132s at http://localhost:1990/confluence [INFO] Type Ctrl-D to shutdown gracefully [INFO] Type Ctrl-C to exit
You'll see the output includes a URL for the Confluence instance.
Log into the instance as user admin
with a password admin
.
The Confluence Dashboard appears.
Click Spaces > Demonstration Space.
The system places you in the space home.
Click Create. The system displays the Create dialog.
Select your blueprint and create a new page.
You'll see the new page filled with the context and instructional text you defined.
To rebuild your plugin, run the atlas-package
command that triggers QuickReload.
In this example, you write a basic Soy (Closure) template file. A Soy template file is a way of dynamically building an HTML file and UI.
Atlassian Soy is Atlassian's implementation of Google Closure. You'll use the Soy template to write a wizard for your blueprint.
This tutorial doesn't really describe in depth on Soy or Closure; you can learn more about them here. After you describe your wizard in Soy, you should update atlassian-plugin.xml
to reference it. Also we add some code to simplebp.js
to react on wizard's events.
Add a soy
subdirectory to the src/main/resources
directory.
Create a file called simplebp.soy
in this new directory.
Add the following content to the simplebp.soy
file and save it.
1 2{namespace MyPlugin.Blueprints.Simple} /** * A form that accepts a person's name */ {template .page1Form} <form action="#" method="post" class="aui"> <fieldset> <div class="field-group"> <label for="vname">{getText('my.blueprint.form.label.title.vName')}</label> <input id="vname" class="text" type="text" name="vName"> </div> </fieldset> </form> {/template}
The template defines a single page, page1Form
, that contains a one-field form. You'll reference the namespace
value
from your JavaScript and later from the atlassian-plugin.xml
file. You'll also specify a label value that you need to add to your i18n strings.
In the simplebp.properties
file, add the my.blueprint.form.label.title
.
1 2my.blueprint.form.label.title.vName=Name my.blueprint.wizard.page1.title=Simplebp WIZARD
The following code sample shows the entire file, including previous title.
1 2my.blueprint.title=Sample Template my.create-link.title=My Sample Template my.create-link.description=Create a new SimpleBB template my.blueprint.form.label.title.vName=Name my.blueprint.wizard.page1.title=Simplebp WIZARD
Save and close the simplebp.properties
file.
In the top of the src/main/resources/js/simplebp.js
file in your project, call the setWizard(()
JavaScript API:
1 2Confluence.Blueprint.setWizard('com.example.plugins.tutorial.confluence.simplebp.simplebp:create-by-sample-template', function(wizard) {});
The setWizard(()
method takes the fully qualified pathname to the web-item that creates the blueprint link.
The fully qualified pathname uses the following this format:
${project.groupId}.${artifactId}:web-item-name
.
Your atlassian-plugin.xml
file defines the project's groupId
and artifactId
values.
Save and close the simplebp.js
file.
In the previous steps, you added some dependencies to your pom.xml
file. You also used a Soy template to specify a Wizard page
and wrote a JavaScript file to listen its events.
In this step, you'll update the web-resource
component in your atlassian-plugin.xml
file
to access the dependencies. Then you'll modify the content-template
module to remove the context-provider
element.
The user fills in the Name
in the wizard.
You'll also add a dialog-wizard
element to the <blueprint/> module.
The dialog-wizard
element describes each dialog-page
in your wizard. A dialog-page
has three required attributes:
Field | Description |
---|---|
`id` | A unique identifier. |
`template-key` | A fully-qualified Soy template reference. The template defines the page content. |
`i18n-name-key` | The i18n key for the page's title. |
When you are done updating your atlassian-plugin.xml
file, you'll run your plugin and see the Wizard in action.
In the src/main/resources/atlassian-plugin.xml
file, find the <web-resource/>
module.
The SDK atlas-create-confluence-plugin
command generates this module for you.
Add a <dependency>
element for the confluence-create-content-plugin.
1 2<!-- add our web resources --> <web-resource key="simplebp-resources" name="simplebp Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <dependency>com.atlassian.confluence.plugins.confluence-create-content-plugin:resources</dependency> <resource type="download" name="simplebp.css" location="/css/simplebp.css" /> <resource type="download" name="simplebp.js" location="/js/simplebp.js" /> <resource type="download" name="images/" location="/images" /> <context>simplebp</context> </web-resource>
Add a <transformation/>
module for soy.
1 2<!-- add our web resources --> <web-resource key="simplebp-resources" name="simplebp Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <dependency>com.atlassian.confluence.plugins.confluence-create-content-plugin:resources</dependency> <transformation extension="soy"> <transformer key="soyTransformer"> <functions>com.atlassian.confluence.plugins.soy:soy-core-functions </functions> </transformer> </transformation> <resource type="download" name="simplebp.css" location="/css/simplebp.css" /> <resource type="download" name="simplebp.js" location="/js/simplebp.js" /> <resource type="download" name="images/" location="/images" /> <context>simplebp</context> </web-resource>
Right before the /images
entry, inside web-resource
, add an entry for Soy template.
When, specifying the name
value, add .js
to the template name. This notifies the plugin system that your Soy resource
transforms to Javascript on the client. Make sure to append .js
to the template name. For example, our simplebp.soy
should look like simplebp.soy.js
.
1 2<!-- add our web resources --> <web-resource key="simplebp-resources" name="simplebp Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <dependency>com.atlassian.confluence.plugins.confluence-create-content-plugin:resources</dependency> <transformation extension="soy"> <transformer key="soyTransformer"> <functions>com.atlassian.confluence.plugins.soy:soy-core-functions </functions> </transformer> </transformation> <resource type="download" name="simplebp.css" location="/css/simplebp.css" /> <resource type="download" name="simplebp.js" location="/js/simplebp.js" /> <resource type="download" name="simplebp.soy.js" location="/soy/simplebp.soy" /> <resource type="download" name="images/" location="/images" /> <context>simplebp</context> </web-resource>
To include your resources in create content dialog, inside context
tag replace auto generated value simplebp
with create-content
.
1 2<!-- add our web resources --> <web-resource key="simplebp-resources" name="simplebp Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <dependency>com.atlassian.confluence.plugins.confluence-create-content-plugin:resources</dependency> <transformation extension="soy"> <transformer key="soyTransformer"> <functions>com.atlassian.confluence.plugins.soy:soy-core-functions </functions> </transformer> </transformation> <resource type="download" name="simplebp.css" location="/css/simplebp.css" /> <resource type="download" name="simplebp.js" location="/js/simplebp.js" /> <resource type="download" name="simplebp.soy.js" location="/soy/simplebp.soy" /> <resource type="download" name="images/" location="/images" /> <context>create-content</context> </web-resource>
In the <blueprint/>
module, define the dialog-wizard
. Make sure that template-key
points to your Soy template.
1 2<!-- Blueprint --> <blueprint key="my-blueprint" content-template-key="simplebp-template" index-key="my-index" i18n-name-key="my.blueprint.name"> <dialog-wizard key="simplebp-wizard"> <dialog-page id="page1Id" template-key="MyPlugin.Blueprints.Simple.page1Form" title-key="my.blueprint.wizard.page1.title" last="true"/> </dialog-wizard> </blueprint>
Comment out the context-provider
where it appears in the content-template
module.
1 2<!-- Template for Blueprint --> <content-template key="simplebp-template" i18n-name-key="my.blueprint.title"> <resource name="template" type="download" location="/templates/mytemplate.xml" /> <!-- <context-provider class="com.example.plugins.tutorial.confluence.simplebp.MyContextProvider" /> --> </content-template>
The following code snippet shows the full file, including the previous module.
1 2<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}" plugins-version="2"> <plugin-info> <description>${project.description}</description> <version>${project.version}</version> <vendor name="${project.organization.name}" url="${project.organization.url}" /> <param name="plugin-icon">images/pluginIcon.png</param> <param name="plugin-logo">images/pluginLogo.png</param> </plugin-info> <!-- add our i18n resource --> <resource type="i18n" name="i18n" location="simplebp" /> <!-- add our web resources --> <web-resource key="simplebp-resources" name="simplebp Web Resources"> <dependency>com.atlassian.auiplugin:ajs</dependency> <dependency>com.atlassian.confluence.plugins.confluence-create-content-plugin:resources</dependency> <transformation extension="soy"> <transformer key="soyTransformer"> <functions>com.atlassian.confluence.plugins.soy:soy-core-functions </functions> </transformer> </transformation> <resource type="download" name="simplebp.css" location="/css/simplebp.css" /> <resource type="download" name="simplebp.js" location="/js/simplebp.js" /> <resource type="download" name="simplebp.soy.js" location="/soy/simplebp.soy" /> <resource type="download" name="images/" location="/images" /> <context>create-content</context> </web-resource> <!-- Blueprint --> <blueprint key="my-blueprint" content-template-key="simplebp-template" index-key="my-index" i18n-name-key="my.blueprint.name"> <dialog-wizard key="simplebp-wizard"> <dialog-page id="page1Id" template-key="MyPlugin.Blueprints.Simple.page1Form" title-key="my.blueprint.wizard.page1.title" last="true"/> </dialog-wizard> </blueprint> <!-- Add to the Create Menu --> <web-item key="create-by-sample-template" i18n-name-key="my.create-link.title" section="system.create.dialog/content"> <description key="my.create-link.description" /> <resource name="icon" type="download" location="/images/myblueprint.png" /> <param name="blueprintKey" value="my-blueprint" /> </web-item> <!-- Template for Blueprint --> <content-template key="simplebp-template" i18n-name-key="my.blueprint.title"> <resource name="template" type="download" location="/templates/mytemplate.xml" /> <!-- <context-provider --> <!-- class="com.example.plugins.tutorial.confluence.simplebp.MyContextProvider" /> --> </content-template> </atlassian-plugin>
Save and close the atlassian-plugin.xml
file.
To rebuild the plugin, run the following command.
1 2atlas-package
Test your blueprint in Confluence to view your wizard.
Although the Wizard form is quite simple, a complex or multi-page Wizard can benefit from some explanatory text. In this step, you add description-header-key
and a description-content-key
to display this explanatory text.
Edit the atlassian-plugin.xml
file.
Add two attributes to the dialog-page
element.
1 2<blueprint key="my-blueprint" content-template-key="simplebp-template" index-key="my-index" i18n-name-key="my.blueprint.name"> ... <dialog-wizard key="simplebp-wizard"> <dialog-page id="page1Id" template-key="MyPlugin.Blueprints.Simple.page1Form" title-key="my.blueprint.wizard.page1.title" description-header-key="my.blueprint.wizard.page1.desc.header" description-content-key="my.blueprint.wizard.page1.desc.content" last="true" /> </dialog-wizard> </blueprint>
Save and close the atlassian-plugin.xml file.
In the simplebp.properties
file, add an entry for the description title and content.
1 2my.blueprint.title=Sample Template my.create-link.title=My Sample Template my.create-link.description=Create a new SimpleBB template my.blueprint.form.label.title.vName=Name my.blueprint.wizard.page1.title=Simplebp WIZARD my.blueprint.wizard.page1.desc.header=About this blueprint my.blueprint.wizard.page1.desc.content=Use this blueprint to create a table with your name and email.
Save and close the simplebp.properties
file.
To rebuild your plugin, run the following command, and QuickReload will automatically reinstall it for you.
1 2atlas-package
Using the setWizard()
JavaScript API you can add fuller interaction to your wizard. For example, you can use this API to validate values entered in a wizard's forms. For validation, you use the submit
hook to check the fields on a page. The submit hook takes the id
value of the dialog-page
and a state
object. Values from the wizard's form are properties of the state.pageData
object. Do the following to validate the entry of the Name field in your wizard:
In the resources/js/simplebp.js
file, add the hook to the setWizard(){}
body.
1 2Confluence.Blueprint.setWizard('com.example.plugins.tutorial.confluence.simplebp.simplebp:create-by-sample-template', function(wizard) { wizard.on('submit.page1Id', function(e, state) { var vName = state.pageData.vName; if (!vName){ alert('Please provide a name value.'); return false; } }); });
This hook checks to make sure the field is not empty.
Save and close the simplebp.js
file.
Rebuild your plugin using the following command:
1 2atlas-package ``` 1. Test your work. ## Next steps You can continue onto the next tutorial, [Write an advanced blueprint plugin](/server/confluence/write-an-advanced-blueprint-plugin) to learn how to construct multiple page tutorials and add event listeners.
Rate this page: