├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── atlassian-connect.json ├── favicon.ico ├── index.html └── manifest.json ├── readme_assets └── jira-addon.gif ├── scripts ├── pre-build.js ├── pre-start.js └── util.js └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Leanconvert Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-react-static-atlassian-connect-addon 2 | 3 | A boilerplate for static Jira / Confluence [Atlassian Connect addons](https://developer.atlassian.com/static/connect/docs/latest/index.html) built on top of [create-react-app](https://github.com/facebookincubator/create-react-app). 4 | 5 | # Quick Start 6 | 7 | Create Atlassian Connect addon just in a few minutes. 8 | 9 | ![Create Atlassian Connect addon](readme_assets/jira-addon.gif) 10 | 11 | ## Get Development Jira Version 12 | 13 | Follow instruction on the [developer.atlassian.com](https://developer.atlassian.com/static/connect/docs/latest/guides/development-setup.html#cloud-dev) 14 | 15 | ## Setup Addon Project 16 | 17 | ```shell 18 | git clone https://github.com/leanconvert/create-react-static-atlassian-connect-addon.git 19 | cd 20 | rm -rf .git 21 | npm i 22 | ``` 23 | 24 | **You’ll need to have Node >= 6 on your machine.** 25 | 26 | ## Configure 27 | 28 | ### package.json 29 | 30 | Specify the following properties: 31 | 32 | + `atlassian-connect-dev-instance-host` - name of the host of your Atlassian **development** instance (e.g. `.atlassian.net`) 33 | + `atlassian-connect-prod-instance-host` - name of the host of your atlassian **production** instance (e.g. `.atlassian.net`) 34 | + `atlassian-connect-addon-home` - URL to your addon on production (e.g. `.firebaseapp.com`) 35 | 36 | ### atlassian-connect.json 37 | 38 | `atlassian-addon.json` is an Atlassian Addon Descriptor - visit [official Atlassian documentation](https://developer.atlassian.com/static/connect/docs/latest/modules/) for more info. 39 | 40 | ## Start Development 41 | 42 | ```shell 43 | npm start 44 | ``` 45 | 46 | ## Upload Addon (Jira / Confluence) 47 | 48 | + Go to `https://.atlassian.net/plugins/servlet/upm` 49 | + Click "Upload add-on" 50 | + Paste the URL that was automatically added to your clipboard after running `npm start` (path to the `atlassian-connect.json` generated by [ngrok](https://ngrok.com/)) 51 | 52 | Now you are all set! Use all the benefits of the `react-create-app` tooling developing an awesome Atlassian add-on. 53 | 54 | ## Atlassian Specific Files 55 | 56 | ### atlassian-addon.json 57 | 58 | `atlassian-addon.json` is an Atlassian Addon Descriptor - visit [official Atlassian documentation](https://developer.atlassian.com/static/connect/docs/latest/modules/) for more info. 59 | 60 | ### scripts/pre-start.js 61 | 62 | + Being executed before `react-scripts start` (see `package.json`) 63 | + Adds `REACT_APP_HOST` environment variable with your Atlassian development instance host URL specified in the `"atlassian-connect-dev-instance-host"` in your `package.json` file 64 | + Initializes [ngrok](https://ngrok.com/) to tunnel the `https://localhost:3000` 65 | + Updates `public/atlassian-connect.json`'s `"baseUrl"` with the by ngrok generated URL 66 | + Updates `public/atlassian-connect.json`'s `"links/config"` with the full public path to the `atlassian-connect.json` file (e.g. `https:/4c413a45.ngrok.io/atlassian-connect.json`) 67 | + Adds atlassian-connect public URL (generated by `ngrok`) to the clipboard so you don't need to open the file to copy it yourself before installing in your Atlassian instance 68 | 69 | ### scripts/pre-build.js 70 | 71 | + Being executed before `react-scripts start` (see `package.json`) 72 | + Adds `REACT_APP_HOST` environment variable with your Atlassian production instance host URL specified in the `atlassian-connect-prod-instance-host` in your `package.json` file 73 | + Updates `public/atlassian-connect.json`'s `"baseUrl"` with one specified in `package.json`'s `"atlassian-connect-addon-home"` property 74 | + Updates `public/atlassian-connect.json`'s `"links/config"` with the full public path to the production `atlassian-connect.json` file (e.g. `https://myaddon.firebaseapp.com/atlassian-connect.json`) 75 | + Adds atlassian-connect **production** public URL to the clipboard so you don't need to open the file to copy it yourself before installing in your Atlassian instance 76 | 77 | ### util.js 78 | 79 | Contains helper functions to be used in `pre-start.js` and `pre-build.js` scripts. 80 | 81 | ## Atlassian Specific Tweaks 82 | 83 | ### package.json 84 | 85 | #### Atlassian specific properties, modules and scripts 86 | 87 | + `atlassian-connect-dev-instance-host` - host URL of your Atlassian development instance 88 | + `atlassian-connect-prod-instance-host` - host URL of your Atlassian production instance 89 | + `atlassian-connect-addon-home` - where your addon will be hosted on production 90 | + `"copy-paste"` - used to add the development `atlassian-connect.json` URL to the clipboard 91 | + `"ngrok"` - used to create a tunnel to the localhost for the development 92 | + `"start": "node scripts/pre-start.js & react-scripts start"` - runs `pre-start.js` in addition to `react-scripts start` 93 | + `"build": "node scripts/pre-build.js & react-scripts build"` - runs `pre-build.js` in addition to `react-scripts build` 94 | 95 | ```json 96 | { 97 | "name": "create-react-app-atlassian-connect-addon", 98 | "version": "0.1.0", 99 | "private": true, 100 | "atlassian-connect-dev-instance-host": "dev-example.atlassian.net", 101 | "atlassian-connect-prod-instance-host": "example.atlassian.net", 102 | "atlassian-connect-addon-home": "https://example.com/", 103 | "dependencies": { 104 | "react": "^15.6.1", 105 | "react-dom": "^15.6.1" 106 | }, 107 | "devDependencies": { 108 | "copy-paste": "^1.3.0", 109 | "ngrok": "^2.2.10", 110 | "react-scripts": "1.0.7" 111 | }, 112 | "scripts": { 113 | "start": "node scripts/pre-start.js & react-scripts start", 114 | "build": "node scripts/pre-build.js & react-scripts build", 115 | "test": "react-scripts test --env=jsdom", 116 | "eject": "react-scripts eject" 117 | } 118 | } 119 | 120 | ``` 121 | 122 | ### public/index.html 123 | 124 | ```html 125 | 126 | 127 | 128 | 129 | 130 | 131 | 135 | 136 | 137 | 146 | React Atlassian Addon 147 | 148 | 149 | 152 |
153 | 156 | 157 | 158 | 168 | 169 | 170 | ``` 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app-atlassian-connect-addon", 3 | "version": "0.2.2", 4 | "private": true, 5 | "keywords": [ 6 | "react-app-boilerplate", 7 | "atlassian-connect-addon", 8 | "atlassian-connect", 9 | "jira-addon", 10 | "confluence-addon" 11 | ], 12 | "atlassian-connect-dev-instance-host": "dev-example.atlassian.net", 13 | "atlassian-connect-prod-instance-host": "example.atlassian.net", 14 | "atlassian-connect-addon-home": "https://example.com/", 15 | "dependencies": { 16 | "react": "^15.6.1", 17 | "react-dom": "^15.6.1" 18 | }, 19 | "devDependencies": { 20 | "copy-paste": "^1.3.0", 21 | "ngrok": "^2.2.10", 22 | "react-scripts": "^1.0.7" 23 | }, 24 | "scripts": { 25 | "start": "node scripts/pre-start.js & react-scripts start", 26 | "build": "node scripts/pre-build.js & react-scripts build", 27 | "test": "react-scripts test --env=jsdom", 28 | "eject": "react-scripts eject" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/atlassian-connect.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "my-addon-unique-key", 3 | "name": "My addon name", 4 | "description": "My awesome addon description", 5 | "vendor": { 6 | "name": "My name", 7 | "url": "www.example.com" 8 | }, 9 | "baseUrl": "", 10 | "links": { 11 | "config": "" 12 | }, 13 | "authentication": { 14 | "type": "none" 15 | }, 16 | "scopes": [ 17 | "READ", 18 | "WRITE" 19 | ], 20 | "modules": { 21 | "generalPages": [ 22 | { 23 | "key": "my-addon-page-name", 24 | "location": "system.top.navigation.bar", 25 | "name": { 26 | "value": "My addon" 27 | }, 28 | "url": "/index", 29 | "conditions": [ 30 | { 31 | "condition": "user_is_logged_in" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leanconvert/create-react-static-atlassian-connect-addon/c8345912cf7842017755b224dc6ec94b93868663/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React Atlassian Addon 23 | 24 | 25 | 28 |
29 | 33 | 34 | 35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Atlassian Addon", 3 | "name": "Create React App Atlassian Addon Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /readme_assets/jira-addon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leanconvert/create-react-static-atlassian-connect-addon/c8345912cf7842017755b224dc6ec94b93868663/readme_assets/jira-addon.gif -------------------------------------------------------------------------------- /scripts/pre-build.js: -------------------------------------------------------------------------------- 1 | const util = require('./util'); 2 | const pkg = require('../package'); 3 | const ncp = require("copy-paste"); 4 | 5 | if (!pkg['atlassian-connect-addon-home']) { 6 | console.error('Please specify your addon home (hosting) URL as a value of the "atlassian-connect-addon-home" property in your package.json (e.g. .firebaseapp.com).'); 7 | process.exit(1); 8 | } 9 | 10 | const hostingUrl = pkg['atlassian-connect-addon-home']; 11 | 12 | util.setHostVariable('prod'); 13 | util.updateConnectConfig(hostingUrl); 14 | ncp.copy(`${hostingUrl}/atlassian-connect.json`); 15 | -------------------------------------------------------------------------------- /scripts/pre-start.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const util = require('./util'); 4 | const ngrok = require('ngrok'); 5 | const ncp = require("copy-paste"); 6 | 7 | util.setHostVariable('dev'); 8 | 9 | ngrok.connect(3000, (err, url) => { 10 | util.updateConnectConfig(url); 11 | ncp.copy(`${url}/atlassian-connect.json`); 12 | }); 13 | -------------------------------------------------------------------------------- /scripts/util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const pkg = require('../package'); 4 | 5 | /** 6 | * Update atlassian-connect.json with the baseUrl generated by `ngrok` 7 | * @param {string} baseUrl 8 | * @return {void} 9 | */ 10 | exports.updateConnectConfig = (baseUrl) => { 11 | const configPath = path.join(__dirname, '..', 'public', 'atlassian-connect.json'); 12 | 13 | fs.readFile(configPath, (err, data) => { 14 | var config = JSON.parse(data); 15 | 16 | config.baseUrl = baseUrl; 17 | config.links = { 18 | config: `${baseUrl}/atlassian-connect.json` 19 | }; 20 | 21 | fs.writeFile(configPath, JSON.stringify(config, null, 2), (err, result) => { 22 | if (err) { 23 | console.error(err); 24 | process.exit(1); 25 | } 26 | }); 27 | }) 28 | }; 29 | 30 | /** 31 | * Overwrite `react-scripts/srcipts/start.js` or `react-scripts/srcipts/start.js` 32 | * to set REACT_APP_HOST env variable to be used in `public/index.html` 33 | * @param {string} env Environment name (dev/prod) 34 | * @return {void} 35 | */ 36 | exports.setHostVariable = (env) => { 37 | if (!pkg['atlassian-connect-dev-instance-host']) { 38 | console.error('Please specify URL of your Atlassian DEVELOPMENT instance as a value of the "atlassian-connect-dev-instance-host" in your package.json (e.g. .atlassian.com).'); 39 | process.exit(1); 40 | } 41 | 42 | if (!pkg['atlassian-connect-prod-instance-host']) { 43 | console.error('Please specify URL of your Atlassian PRODUCTION instance as a value of the "atlassian-connect-dev-instance-host" in your package.json (e.g. .atlassian.com).'); 44 | process.exit(1); 45 | } 46 | 47 | const filePath = getFilePath(env); 48 | 49 | fs.readFile(filePath, 'utf-8', (err, data) => { 50 | var updatedScript = getUpdatedScript(env, data); 51 | 52 | fs.writeFile(filePath, updatedScript, (err, result) => { 53 | if (err) { 54 | console.error(err); 55 | process.exit(1); 56 | } 57 | }); 58 | }); 59 | }; 60 | 61 | /** 62 | * Get file name 63 | * @param {string} env Environment name (dev/prod) 64 | * @return {string} 65 | */ 66 | function getFileName(env) { 67 | return env === 'dev' ? 'start' : 'build'; 68 | } 69 | 70 | /** 71 | * Geet file path 72 | * @param {string} env Environment name (dev/prod) 73 | * @return {string} 74 | */ 75 | function getFilePath(env) { 76 | const fileName = getFileName(env); 77 | return path.join(__dirname, `../node_modules/react-scripts/scripts/${fileName}.js`); 78 | } 79 | 80 | /** 81 | * Get atlassian instance host URL 82 | * @param {string} env Environment name (dev/prod) 83 | * @return {string} host Atlassian instance host URL 84 | */ 85 | function getHost(env) { 86 | let host = pkg[`atlassian-connect-${env}-instance-host`]; 87 | host = !/^https:\/\//.test(host) && (host = `https://${host}`); 88 | return host; 89 | } 90 | 91 | /** 92 | * Get script udapted with `process.env.REACT_APP_HOST` 93 | * @param {string} host Atlassian instance host URL 94 | * @param {string} data `react-scripts` script (`start.js` or `build.js`) 95 | */ 96 | function getUpdatedScript(env, data) { 97 | const host = getHost(env); 98 | var updatedScript; 99 | 100 | if (data.includes('REACT_APP_HOST')) { 101 | updatedScript = data.replace( 102 | /\.REACT_APP_HOST.+?;/, 103 | `.REACT_APP_HOST = '${host}';`); 104 | } else { 105 | updatedScript = `process.env.REACT_APP_HOST = '${host}';\n${data}`; 106 | } 107 | 108 | return updatedScript; 109 | } 110 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | logo 11 |

Welcome to React

12 |
13 |

14 | To get started, edit src/App.js and save to reload. 15 |

16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import registerServiceWorker from './registerServiceWorker'; 5 | import './index.css'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | export default function register() { 12 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 13 | window.addEventListener('load', () => { 14 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 15 | navigator.serviceWorker 16 | .register(swUrl) 17 | .then(registration => { 18 | registration.onupdatefound = () => { 19 | const installingWorker = registration.installing; 20 | installingWorker.onstatechange = () => { 21 | if (installingWorker.state === 'installed') { 22 | if (navigator.serviceWorker.controller) { 23 | // At this point, the old content will have been purged and 24 | // the fresh content will have been added to the cache. 25 | // It's the perfect time to display a "New content is 26 | // available; please refresh." message in your web app. 27 | console.log('New content is available; please refresh.'); 28 | } else { 29 | // At this point, everything has been precached. 30 | // It's the perfect time to display a 31 | // "Content is cached for offline use." message. 32 | console.log('Content is cached for offline use.'); 33 | } 34 | } 35 | }; 36 | }; 37 | }) 38 | .catch(error => { 39 | console.error('Error during service worker registration:', error); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | export function unregister() { 46 | if ('serviceWorker' in navigator) { 47 | navigator.serviceWorker.ready.then(registration => { 48 | registration.unregister(); 49 | }); 50 | } 51 | } 52 | --------------------------------------------------------------------------------