├── .DS_Store ├── .gitignore ├── README.md ├── climbing-harness-icon.svg ├── example_config.json ├── gradientGen.py ├── package-lock.json ├── package.json ├── public ├── actionables-sidebar.css ├── actionables-sidebar.html ├── actionables-sidebar.js ├── actionables.html ├── actionables.js ├── climbing-harness-sidebar.css ├── climbing-harness-sidebar.html ├── climbing-harness-sidebar.js ├── climbing-harness.html ├── climbing-harness.js ├── create-issue-modal.html ├── create-issue.html ├── create-issue.js ├── set-complete.html ├── set-complete.js ├── set-incomplete.html ├── set-incomplete.js ├── set-uncertain.html ├── set-uncertain.js ├── tree-logic.html └── tree-logic.js └── simpleserver.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holochain/soa-toolbox/4ae270c6e794dd8b872edfb484b6ba52ff2e9f62/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | config.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # soa-toolbox 2 | 3 | > Tools for using an Acorn State of Affairs on Miro 4 | 5 | ## Background 6 | 7 | The architecture here is that we serve an HTML file which has the realtime board SDK in it, plus our own custom JS that uses that. 8 | 9 | This HTML gets loaded as an iframe into any realtime board for the Holo team. 10 | 11 | The plugins currently present these functionalities: 12 | * Calculate subtree size — updates selected widget and children with calculations. 13 | * Set node status — change nodes to uncertain, incomplete, or complete 14 | * Create GitHub issue — create a GitHub issue out of the selected node in a repository of choice 15 | 16 | This is meant for the purposes of being able to better track our work and progress. 17 | 18 | The data must be structured in a particular way for this to work. 19 | 20 | There are certain colors that must be used: 21 | - This red #f24726 as a background as "Uncertain" 22 | - This orange #fac710 as a background as "Incomplete" 23 | - This light green #8fd14f as a background as "Complete" 24 | - This darker green as #0ca789 as a border for a "Small" 25 | 26 | An uncertain is something which hasn't been broken down into smalls yet, thus it is unknown how long it will take. 27 | A small is a thing which is discrete and in itself attainable, and shouldn't represent more than 1 days worth of work (otherwise it's not a small). 28 | 29 | In theory, nodes higher in the tree should be able to have their color set automatically, while only the leaves/smalls should have to be updated manually. 30 | 31 | Additionally, **and this is important**, edges must be drawn FROM the child, TO the parent, as this is how RTB stores the data. The direction of the arrow of a line is not available as data, and definitely not available to indicate the directionality of the relationship. If we are to be able to accurately measure our work, and have this tool be useful, all edges must be drawn in the correct direction, that is, you must literally click and drag the line from the child node to the parent node. 32 | 33 | ## Install 34 | 35 | ### Dependencies 36 | 37 | * [ngrok](https://ngrok.com/) (for development) 38 | * [nodejs](https://nodejs.org) 39 | * You will need admin access to your Miro team. 40 | 41 | 1. #### **Set up ngrok** (for development) 42 | 1. Start ngrok on port 8088 to open a tunnel from the web to your local server: `ngrok http 8088` or `./ngrok http 8088` 43 | 2. Copy the public https address, like `https://958648dd.ngrok.io`, to your clipboard. 44 | 2. #### **Set up Miro** 45 | 1. Open Miro settings: Miro > [Your organization] > Settings > Profile settings > Your apps [Beta]. 46 | 2. For each web plugin, create a new app. 47 | 3. Give it the following scopes: 48 | * `boards:read` 49 | * `boards:write` 50 | * `boards_content:read` 51 | * `board_content:write` 52 | 4. Turn on the Enable web-plugin toggle 53 | 5. Select `client:plugins_for_account` 54 | 5. In the Iframe url box, combine the ngrok url with the path to the html file for the plugin. For example, the url for the the set-complete plugin would look like `https://958648dd.ngrok.io/set-complete.html`. 55 | 6. Click Install app and get OAuth Token. Select your Miro team. You won't need the OAuth token you receive. 56 | 3. #### **Create config file** 57 | 1. *You'll need a config file so that the create-issue web-plugin to know about and have access to your GitHub repos. An example config file to copy and edit is provided.* 58 | 2. Create a file called `config.json` 59 | 3. For each repo you'd like to add issues to, create an entry in the config file containing: 60 | * The unique path to your GitHub repo. This is usually a username or organization followed by a slash and then the repo name. For example: `holochain/soa-toolbox` 61 | * A [GitHub Personal Access Token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with repo permissions. 62 | 63 | [comment]: # (Watch out! There are non-breaking zero-width space characters in some URLs above) 64 | 65 | ## Usage 66 | 67 | In a new terminal, start the nodejs server, which has API endpoints, and also serves static files: 68 | `npm run start` 69 | 70 | You should see plugins on your Miro board. 71 | 72 | Make changes to the code and live test your work in Miro. You don't have to restart the nodejs server as long as you don't make changes to `simpleserver.js`, just run the command `reloadSandbox()` in the developer console in your browser to reload the plugins. 73 | 74 | ## Documentation for RTB API 75 | 76 | * [Miro SDK Reference](https://developers.miro.com/docs/sdk-doc) 77 | * [Miro Web-plugin Examples](https://developers.miro.com/docs/web-plugin-examples) 78 | -------------------------------------------------------------------------------- /climbing-harness-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "repos": { 3 | "user1/repo1": { 4 | "accessToken": "15bb0f2e881307af7ea0a9451bbf39767229422e" 5 | }, 6 | "user2/repo2": { 7 | "accessToken": "15bb0f2f081307af7e88a9451bbf92775982643d" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gradientGen.py: -------------------------------------------------------------------------------- 1 | # The code used to generate the tree-logic SVG icon 2 | 3 | def gradient(): 4 | center = 12 5 | 6 | x = 4 7 | yLengths = { 8 | 0:8, 9 | 1:8, 10 | 2:9, 11 | 3:13, 12 | 4:14, 13 | 5:14, 14 | 6:14, 15 | 7:15, 16 | 8:15, 17 | 9:15, 18 | 10:16, 19 | 11:16.5, 20 | 12:16.5, 21 | 13:16.5, 22 | 14:16.5, 23 | 15:16.5, 24 | 16:16.5, 25 | 17:16.5, 26 | 18:16.5, 27 | 19:16, 28 | 20:15, 29 | 21:15, 30 | 22:15, 31 | 23:15, 32 | 24:15, 33 | 25:14, 34 | 26:14, 35 | 27:14, 36 | 28:10, 37 | 29:9, 38 | 30:8, 39 | 31:8} 40 | 41 | colors = { 42 | 0:9, 43 | 1:9, 44 | 2:10, 45 | 3:10, 46 | 4:10, 47 | 5:10, 48 | 6:10+(4.2*1), 49 | 7:10+(4.2*2), 50 | 8:10+(4.2*3), 51 | 9:10+(4.2*4), 52 | 10:10+(4.2*5), 53 | 11:10+(4.2*6), 54 | 12:10+(4.2*7), 55 | 13:10+(4.2*8), # 43.6 56 | 14:47, 57 | 15:47, 58 | 16:47, 59 | 17:47, 60 | 18:47+(4.7*1), 61 | 19:47+(4.7*2), 62 | 20:47+(4.7*3), 63 | 21:47+(4.7*4), 64 | 22:47+(4.7*5), 65 | 23:47+(4.7*6), 66 | 24:47+(4.7*7), 67 | 25:47+(4.7*8), 68 | 26:90, 69 | 27:90, 70 | 28:90, 71 | 29:90, 72 | 30:90, 73 | 31:90, 74 | 32:90} 75 | 76 | red = 10 77 | orange = 47 78 | green = 90 79 | 80 | yMin = 8 81 | yMax = 16 82 | 83 | color = 10 84 | 85 | for i in range(32): 86 | print(f'') 87 | x += .5 88 | yLength = yLengths[i] 89 | yMin = 12 - (yLength/2) 90 | yMax = 12 + (yLength/2) 91 | 92 | color = colors[i] 93 | 94 | def gear(): 95 | print('') 96 | print('') 97 | 98 | def icon(): 99 | print('') 100 | gradient() 101 | gear() 102 | print('') 103 | 104 | icon() 105 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soa-scraper", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.7", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 10 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 11 | "requires": { 12 | "mime-types": "~2.1.24", 13 | "negotiator": "0.6.2" 14 | } 15 | }, 16 | "ajv": { 17 | "version": "6.10.2", 18 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", 19 | "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", 20 | "requires": { 21 | "fast-deep-equal": "^2.0.1", 22 | "fast-json-stable-stringify": "^2.0.0", 23 | "json-schema-traverse": "^0.4.1", 24 | "uri-js": "^4.2.2" 25 | } 26 | }, 27 | "array-flatten": { 28 | "version": "1.1.1", 29 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 30 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 31 | }, 32 | "asn1": { 33 | "version": "0.2.4", 34 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 35 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 36 | "requires": { 37 | "safer-buffer": "~2.1.0" 38 | } 39 | }, 40 | "assert-plus": { 41 | "version": "1.0.0", 42 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 43 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 44 | }, 45 | "asynckit": { 46 | "version": "0.4.0", 47 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 48 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 49 | }, 50 | "aws-sign2": { 51 | "version": "0.7.0", 52 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 53 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 54 | }, 55 | "aws4": { 56 | "version": "1.8.0", 57 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 58 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 59 | }, 60 | "bcrypt-pbkdf": { 61 | "version": "1.0.2", 62 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 63 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 64 | "requires": { 65 | "tweetnacl": "^0.14.3" 66 | } 67 | }, 68 | "body-parser": { 69 | "version": "1.19.0", 70 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 71 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 72 | "requires": { 73 | "bytes": "3.1.0", 74 | "content-type": "~1.0.4", 75 | "debug": "2.6.9", 76 | "depd": "~1.1.2", 77 | "http-errors": "1.7.2", 78 | "iconv-lite": "0.4.24", 79 | "on-finished": "~2.3.0", 80 | "qs": "6.7.0", 81 | "raw-body": "2.4.0", 82 | "type-is": "~1.6.17" 83 | } 84 | }, 85 | "bytes": { 86 | "version": "3.1.0", 87 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 88 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 89 | }, 90 | "caseless": { 91 | "version": "0.12.0", 92 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 93 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 94 | }, 95 | "combined-stream": { 96 | "version": "1.0.8", 97 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 98 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 99 | "requires": { 100 | "delayed-stream": "~1.0.0" 101 | } 102 | }, 103 | "content-disposition": { 104 | "version": "0.5.3", 105 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 106 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 107 | "requires": { 108 | "safe-buffer": "5.1.2" 109 | } 110 | }, 111 | "content-type": { 112 | "version": "1.0.4", 113 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 114 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 115 | }, 116 | "cookie": { 117 | "version": "0.4.0", 118 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 119 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 120 | }, 121 | "cookie-signature": { 122 | "version": "1.0.6", 123 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 124 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 125 | }, 126 | "core-util-is": { 127 | "version": "1.0.2", 128 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 129 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 130 | }, 131 | "cors": { 132 | "version": "2.8.5", 133 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 134 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 135 | "requires": { 136 | "object-assign": "^4", 137 | "vary": "^1" 138 | } 139 | }, 140 | "dashdash": { 141 | "version": "1.14.1", 142 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 143 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 144 | "requires": { 145 | "assert-plus": "^1.0.0" 146 | } 147 | }, 148 | "debug": { 149 | "version": "2.6.9", 150 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 151 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 152 | "requires": { 153 | "ms": "2.0.0" 154 | } 155 | }, 156 | "delayed-stream": { 157 | "version": "1.0.0", 158 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 159 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 160 | }, 161 | "depd": { 162 | "version": "1.1.2", 163 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 164 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 165 | }, 166 | "destroy": { 167 | "version": "1.0.4", 168 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 169 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 170 | }, 171 | "dotenv": { 172 | "version": "8.0.0", 173 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", 174 | "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==" 175 | }, 176 | "ecc-jsbn": { 177 | "version": "0.1.2", 178 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 179 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 180 | "requires": { 181 | "jsbn": "~0.1.0", 182 | "safer-buffer": "^2.1.0" 183 | } 184 | }, 185 | "ee-first": { 186 | "version": "1.1.1", 187 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 188 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 189 | }, 190 | "encodeurl": { 191 | "version": "1.0.2", 192 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 193 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 194 | }, 195 | "escape-html": { 196 | "version": "1.0.3", 197 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 198 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 199 | }, 200 | "etag": { 201 | "version": "1.8.1", 202 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 203 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 204 | }, 205 | "express": { 206 | "version": "4.17.1", 207 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 208 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 209 | "requires": { 210 | "accepts": "~1.3.7", 211 | "array-flatten": "1.1.1", 212 | "body-parser": "1.19.0", 213 | "content-disposition": "0.5.3", 214 | "content-type": "~1.0.4", 215 | "cookie": "0.4.0", 216 | "cookie-signature": "1.0.6", 217 | "debug": "2.6.9", 218 | "depd": "~1.1.2", 219 | "encodeurl": "~1.0.2", 220 | "escape-html": "~1.0.3", 221 | "etag": "~1.8.1", 222 | "finalhandler": "~1.1.2", 223 | "fresh": "0.5.2", 224 | "merge-descriptors": "1.0.1", 225 | "methods": "~1.1.2", 226 | "on-finished": "~2.3.0", 227 | "parseurl": "~1.3.3", 228 | "path-to-regexp": "0.1.7", 229 | "proxy-addr": "~2.0.5", 230 | "qs": "6.7.0", 231 | "range-parser": "~1.2.1", 232 | "safe-buffer": "5.1.2", 233 | "send": "0.17.1", 234 | "serve-static": "1.14.1", 235 | "setprototypeof": "1.1.1", 236 | "statuses": "~1.5.0", 237 | "type-is": "~1.6.18", 238 | "utils-merge": "1.0.1", 239 | "vary": "~1.1.2" 240 | } 241 | }, 242 | "extend": { 243 | "version": "3.0.2", 244 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 245 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 246 | }, 247 | "extsprintf": { 248 | "version": "1.3.0", 249 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 250 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 251 | }, 252 | "fast-deep-equal": { 253 | "version": "2.0.1", 254 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 255 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 256 | }, 257 | "fast-json-stable-stringify": { 258 | "version": "2.0.0", 259 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 260 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 261 | }, 262 | "finalhandler": { 263 | "version": "1.1.2", 264 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 265 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 266 | "requires": { 267 | "debug": "2.6.9", 268 | "encodeurl": "~1.0.2", 269 | "escape-html": "~1.0.3", 270 | "on-finished": "~2.3.0", 271 | "parseurl": "~1.3.3", 272 | "statuses": "~1.5.0", 273 | "unpipe": "~1.0.0" 274 | } 275 | }, 276 | "forever-agent": { 277 | "version": "0.6.1", 278 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 279 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 280 | }, 281 | "form-data": { 282 | "version": "2.3.3", 283 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 284 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 285 | "requires": { 286 | "asynckit": "^0.4.0", 287 | "combined-stream": "^1.0.6", 288 | "mime-types": "^2.1.12" 289 | } 290 | }, 291 | "forwarded": { 292 | "version": "0.1.2", 293 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 294 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 295 | }, 296 | "fresh": { 297 | "version": "0.5.2", 298 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 299 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 300 | }, 301 | "getpass": { 302 | "version": "0.1.7", 303 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 304 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 305 | "requires": { 306 | "assert-plus": "^1.0.0" 307 | } 308 | }, 309 | "har-schema": { 310 | "version": "2.0.0", 311 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 312 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 313 | }, 314 | "har-validator": { 315 | "version": "5.1.3", 316 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 317 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 318 | "requires": { 319 | "ajv": "^6.5.5", 320 | "har-schema": "^2.0.0" 321 | } 322 | }, 323 | "http-errors": { 324 | "version": "1.7.2", 325 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 326 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 327 | "requires": { 328 | "depd": "~1.1.2", 329 | "inherits": "2.0.3", 330 | "setprototypeof": "1.1.1", 331 | "statuses": ">= 1.5.0 < 2", 332 | "toidentifier": "1.0.0" 333 | } 334 | }, 335 | "http-signature": { 336 | "version": "1.2.0", 337 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 338 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 339 | "requires": { 340 | "assert-plus": "^1.0.0", 341 | "jsprim": "^1.2.2", 342 | "sshpk": "^1.7.0" 343 | } 344 | }, 345 | "iconv-lite": { 346 | "version": "0.4.24", 347 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 348 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 349 | "requires": { 350 | "safer-buffer": ">= 2.1.2 < 3" 351 | } 352 | }, 353 | "inherits": { 354 | "version": "2.0.3", 355 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 356 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 357 | }, 358 | "ipaddr.js": { 359 | "version": "1.9.0", 360 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", 361 | "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" 362 | }, 363 | "is-typedarray": { 364 | "version": "1.0.0", 365 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 366 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 367 | }, 368 | "isstream": { 369 | "version": "0.1.2", 370 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 371 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 372 | }, 373 | "jsbn": { 374 | "version": "0.1.1", 375 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 376 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 377 | }, 378 | "json-schema": { 379 | "version": "0.2.3", 380 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 381 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 382 | }, 383 | "json-schema-traverse": { 384 | "version": "0.4.1", 385 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 386 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 387 | }, 388 | "json-stringify-safe": { 389 | "version": "5.0.1", 390 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 391 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 392 | }, 393 | "jsprim": { 394 | "version": "1.4.1", 395 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 396 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 397 | "requires": { 398 | "assert-plus": "1.0.0", 399 | "extsprintf": "1.3.0", 400 | "json-schema": "0.2.3", 401 | "verror": "1.10.0" 402 | } 403 | }, 404 | "media-typer": { 405 | "version": "0.3.0", 406 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 407 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 408 | }, 409 | "merge-descriptors": { 410 | "version": "1.0.1", 411 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 412 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 413 | }, 414 | "methods": { 415 | "version": "1.1.2", 416 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 417 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 418 | }, 419 | "mime": { 420 | "version": "1.6.0", 421 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 422 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 423 | }, 424 | "mime-db": { 425 | "version": "1.40.0", 426 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 427 | "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" 428 | }, 429 | "mime-types": { 430 | "version": "2.1.24", 431 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 432 | "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", 433 | "requires": { 434 | "mime-db": "1.40.0" 435 | } 436 | }, 437 | "ms": { 438 | "version": "2.0.0", 439 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 440 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 441 | }, 442 | "negotiator": { 443 | "version": "0.6.2", 444 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 445 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 446 | }, 447 | "oauth-sign": { 448 | "version": "0.9.0", 449 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 450 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 451 | }, 452 | "object-assign": { 453 | "version": "4.1.1", 454 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 455 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 456 | }, 457 | "on-finished": { 458 | "version": "2.3.0", 459 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 460 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 461 | "requires": { 462 | "ee-first": "1.1.1" 463 | } 464 | }, 465 | "parseurl": { 466 | "version": "1.3.3", 467 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 468 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 469 | }, 470 | "path-to-regexp": { 471 | "version": "0.1.7", 472 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 473 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 474 | }, 475 | "performance-now": { 476 | "version": "2.1.0", 477 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 478 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 479 | }, 480 | "proxy-addr": { 481 | "version": "2.0.5", 482 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", 483 | "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", 484 | "requires": { 485 | "forwarded": "~0.1.2", 486 | "ipaddr.js": "1.9.0" 487 | } 488 | }, 489 | "psl": { 490 | "version": "1.3.0", 491 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", 492 | "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==" 493 | }, 494 | "punycode": { 495 | "version": "2.1.1", 496 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 497 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 498 | }, 499 | "qs": { 500 | "version": "6.7.0", 501 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 502 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 503 | }, 504 | "range-parser": { 505 | "version": "1.2.1", 506 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 507 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 508 | }, 509 | "raw-body": { 510 | "version": "2.4.0", 511 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 512 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 513 | "requires": { 514 | "bytes": "3.1.0", 515 | "http-errors": "1.7.2", 516 | "iconv-lite": "0.4.24", 517 | "unpipe": "1.0.0" 518 | } 519 | }, 520 | "request": { 521 | "version": "2.88.0", 522 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 523 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 524 | "requires": { 525 | "aws-sign2": "~0.7.0", 526 | "aws4": "^1.8.0", 527 | "caseless": "~0.12.0", 528 | "combined-stream": "~1.0.6", 529 | "extend": "~3.0.2", 530 | "forever-agent": "~0.6.1", 531 | "form-data": "~2.3.2", 532 | "har-validator": "~5.1.0", 533 | "http-signature": "~1.2.0", 534 | "is-typedarray": "~1.0.0", 535 | "isstream": "~0.1.2", 536 | "json-stringify-safe": "~5.0.1", 537 | "mime-types": "~2.1.19", 538 | "oauth-sign": "~0.9.0", 539 | "performance-now": "^2.1.0", 540 | "qs": "~6.5.2", 541 | "safe-buffer": "^5.1.2", 542 | "tough-cookie": "~2.4.3", 543 | "tunnel-agent": "^0.6.0", 544 | "uuid": "^3.3.2" 545 | }, 546 | "dependencies": { 547 | "qs": { 548 | "version": "6.5.2", 549 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 550 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 551 | } 552 | } 553 | }, 554 | "safe-buffer": { 555 | "version": "5.1.2", 556 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 557 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 558 | }, 559 | "safer-buffer": { 560 | "version": "2.1.2", 561 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 562 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 563 | }, 564 | "send": { 565 | "version": "0.17.1", 566 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 567 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 568 | "requires": { 569 | "debug": "2.6.9", 570 | "depd": "~1.1.2", 571 | "destroy": "~1.0.4", 572 | "encodeurl": "~1.0.2", 573 | "escape-html": "~1.0.3", 574 | "etag": "~1.8.1", 575 | "fresh": "0.5.2", 576 | "http-errors": "~1.7.2", 577 | "mime": "1.6.0", 578 | "ms": "2.1.1", 579 | "on-finished": "~2.3.0", 580 | "range-parser": "~1.2.1", 581 | "statuses": "~1.5.0" 582 | }, 583 | "dependencies": { 584 | "ms": { 585 | "version": "2.1.1", 586 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 587 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 588 | } 589 | } 590 | }, 591 | "serve-static": { 592 | "version": "1.14.1", 593 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 594 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 595 | "requires": { 596 | "encodeurl": "~1.0.2", 597 | "escape-html": "~1.0.3", 598 | "parseurl": "~1.3.3", 599 | "send": "0.17.1" 600 | } 601 | }, 602 | "setprototypeof": { 603 | "version": "1.1.1", 604 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 605 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 606 | }, 607 | "sshpk": { 608 | "version": "1.16.1", 609 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 610 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 611 | "requires": { 612 | "asn1": "~0.2.3", 613 | "assert-plus": "^1.0.0", 614 | "bcrypt-pbkdf": "^1.0.0", 615 | "dashdash": "^1.12.0", 616 | "ecc-jsbn": "~0.1.1", 617 | "getpass": "^0.1.1", 618 | "jsbn": "~0.1.0", 619 | "safer-buffer": "^2.0.2", 620 | "tweetnacl": "~0.14.0" 621 | } 622 | }, 623 | "statuses": { 624 | "version": "1.5.0", 625 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 626 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 627 | }, 628 | "toidentifier": { 629 | "version": "1.0.0", 630 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 631 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 632 | }, 633 | "tough-cookie": { 634 | "version": "2.4.3", 635 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 636 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 637 | "requires": { 638 | "psl": "^1.1.24", 639 | "punycode": "^1.4.1" 640 | }, 641 | "dependencies": { 642 | "punycode": { 643 | "version": "1.4.1", 644 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 645 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 646 | } 647 | } 648 | }, 649 | "tunnel-agent": { 650 | "version": "0.6.0", 651 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 652 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 653 | "requires": { 654 | "safe-buffer": "^5.0.1" 655 | } 656 | }, 657 | "tweetnacl": { 658 | "version": "0.14.5", 659 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 660 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 661 | }, 662 | "type-is": { 663 | "version": "1.6.18", 664 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 665 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 666 | "requires": { 667 | "media-typer": "0.3.0", 668 | "mime-types": "~2.1.24" 669 | } 670 | }, 671 | "unpipe": { 672 | "version": "1.0.0", 673 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 674 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 675 | }, 676 | "uri-js": { 677 | "version": "4.2.2", 678 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 679 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 680 | "requires": { 681 | "punycode": "^2.1.0" 682 | } 683 | }, 684 | "utils-merge": { 685 | "version": "1.0.1", 686 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 687 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 688 | }, 689 | "uuid": { 690 | "version": "3.3.2", 691 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 692 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 693 | }, 694 | "vary": { 695 | "version": "1.1.2", 696 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 697 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 698 | }, 699 | "verror": { 700 | "version": "1.10.0", 701 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 702 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 703 | "requires": { 704 | "assert-plus": "^1.0.0", 705 | "core-util-is": "1.0.2", 706 | "extsprintf": "^1.2.0" 707 | } 708 | } 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "soa-scraper", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "simpleserver.js", 6 | "scripts": { 7 | "start": "node simpleserver.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "cors": "^2.8.5", 14 | "dotenv": "^8.0.0", 15 | "express": "^4.17.1", 16 | "request": "^2.88.0", 17 | "serve-static": "^1.14.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/actionables-sidebar.css: -------------------------------------------------------------------------------- 1 | .scrollable-container { 2 | overflow-y: auto; 3 | position: absolute; 4 | bottom: 0; 5 | top: 63.5px; 6 | padding-right: 5%; 7 | } 8 | .rtb-sidebar-caption { 9 | font-size: 14px; 10 | font-weight: bold; 11 | color: rgba(0, 0, 0, 0.7); 12 | padding: 24px 0 20px 24px; 13 | } 14 | textarea { 15 | display: block; 16 | height: 25%; 17 | margin: 20px 0 0 20px; 18 | background-color: white; 19 | border-radius: 4px; 20 | border: .5; 21 | width: calc(100% - 40px); 22 | text-align: left; 23 | font-size: 14px; 24 | } 25 | .tip { 26 | color: #CCCCCC; 27 | margin: 20px 0 0 26px; 28 | } 29 | .node { 30 | margin: 20px 0 20px 10px; 31 | } 32 | .node-title { 33 | display: block; 34 | margin: 0 auto; 35 | width: 100%; 36 | text-align: center; 37 | } 38 | ul { 39 | padding-left: 5%; 40 | } 41 | li { 42 | list-style: none; 43 | margin: 2px 0 2px 0; 44 | padding: 2px 5px; 45 | } 46 | .nav-link { 47 | cursor: pointer; 48 | font-weight: 500; 49 | border: 1px solid black; 50 | } 51 | header { 52 | margin-left: 10px; 53 | font-weight: bold; 54 | font-size: larger; 55 | } 56 | -------------------------------------------------------------------------------- /public/actionables-sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
Actionables
14 |
15 | 16 |
17 | Select a node to view its actionable items! 18 |
19 | 20 |
21 | 22 |
23 | 24 |
Actionable items:
25 |
    26 | 27 | 28 |
    29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/actionables-sidebar.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | // subscribe on user selected widgets 3 | miro.addListener(miro.enums.event.SELECTION_UPDATED, updateSidebar) 4 | // update the sidebar once when it first opens 5 | updateSidebar() 6 | }) 7 | 8 | const MAX_NODES = 9999999 9 | 10 | // get the entire text of a node without any of the characters phloem adds 11 | function getFullTextFromNode(node) { 12 | html = node.text 13 | html = html.replace(//gi, '') 14 | html = html.replace(//gi, '') 15 | html = html.replace(/<\/div>/ig, '\n') 16 | html = html.replace(/<\/li>/ig, '\n') 17 | html = html.replace(/
  • /ig, ' * ') 18 | html = html.replace(/<\/ul>/ig, '\n') 19 | html = html.replace(/<\/p>/ig, '\n') 20 | html = html.replace(//gi, "\n") 21 | html = html.replace(/<[^>]+>/ig, '') 22 | text = html.replace(/⋅/g,'') // remove newline markers if there are any 23 | return text 24 | } 25 | 26 | // calculate the issue title from a node's text 27 | function getTitleFromNode(node) { 28 | const titleLength = 13 29 | 30 | var fullText = getFullTextFromNode(node) 31 | 32 | // remove metadata (stuff after ##) 33 | var textWithoutMetadata 34 | if (fullText.includes('##')) { 35 | textWithoutMetadata = fullText.split('##').slice(0, -1).join(' ') 36 | } else { 37 | textWithoutMetadata = fullText 38 | } 39 | 40 | // get the first x words to use as the title 41 | if (textWithoutMetadata.split(' ').length <= titleLength) { 42 | return textWithoutMetadata 43 | } else { 44 | // add ... if the text is longer than 10 words 45 | return textWithoutMetadata.split(' ').slice(0,titleLength).join(' ') + "..." 46 | } 47 | } 48 | 49 | // returns a hash of the ID and short and long text of each node in the given array. 50 | // Example: { 84759034802: ['This is...', 'This is a test node.'], 9879: ['Another...', 'Another one.'] } 51 | // later we use this hash to build the list of the parent and children nodes. 52 | // The title is required to display in the box, and the id is required so that 53 | // we can zoom the viewport to that node onclick. 54 | function makeNodeList(nodeArray) { 55 | nodeList = {} 56 | // console.log("nodearray:") 57 | // console.log(nodeArray) 58 | // trim list of nodes to max length 59 | nodeArrayTrimmed = nodeArray.slice(0, MAX_NODES) 60 | nodeArrayTrimmed.forEach(node => { 61 | if (node == undefined) { 62 | return 63 | } 64 | nodeList[node.id] = [getTitleFromNode(node), getFullTextFromNode(node)] 65 | }) 66 | return nodeList 67 | } 68 | 69 | // remove all items from the lists 70 | function clearList() { 71 | while (listElement.firstChild) { 72 | listElement.removeChild(listElement.firstChild) 73 | } 74 | } 75 | 76 | // zoom out a bit and select the node when a list item is clicked on 77 | async function doOnclick(id) { 78 | // clear both lists so the sidebar is empty when the viewport is animating 79 | clearList() 80 | let zoomLevel = await miro.board.viewport.getZoom() // store current zoom level 81 | await miro.board.viewport.setZoom(zoomLevel * 1.10) // zoom in just a bit 82 | await miro.board.selection.selectWidgets(id) // then select the current widget 83 | } 84 | // when a list item is moused over, zoom to that widget after a quick pause. 85 | // once looking at it, zoom out a bunch to show the context (where in the tree) 86 | // the node is. 87 | function doOnMouseover(id) { 88 | // delay between mousing over a list item and zooming over to preview it (ms) 89 | const delayBeforePreviewZoom = 250 90 | // delay between starting to zoom to a widget and zooming out for context (ms) 91 | const delayBeforeShowContextZoom = 600 92 | // how much to zoom out to show context 93 | const showContextZoomFactor = .30 94 | 95 | function previewZoom() { 96 | async function showContextZoom() { 97 | let zoomLevel = await miro.board.viewport.getZoom() 98 | await miro.board.viewport.setZoom(zoomLevel * showContextZoomFactor) 99 | } 100 | miro.board.viewport.zoomToObject(id) 101 | showContextZoomTimer = window.setTimeout(showContextZoom, delayBeforeShowContextZoom) 102 | } 103 | 104 | previewZoomTimer = window.setTimeout(previewZoom, delayBeforePreviewZoom) 105 | } 106 | // when the mouse stops hovering over a list item, return to the viewport we 107 | // saved when updateSidebar was called (usually when the node was selected) 108 | function doOnMouseout(id) { 109 | miro.board.viewport.setViewportWithAnimation(viewport) 110 | window.clearTimeout(showContextZoomTimer) 111 | window.clearTimeout(previewZoomTimer) 112 | } 113 | 114 | // returns the style object of the element with the given ID so the list item in 115 | // the sidebar can match its background color, font weight, border color, and 116 | // border width. 117 | async function getNodeStyle(id) { 118 | let nodes = await miro.board.widgets.get({id: id}) 119 | let node = nodes[0] // there can only be one node with a given ID 120 | return node.style 121 | } 122 | 123 | async function getNodeX(id) { 124 | let nodes = await miro.board.widgets.get({id: id}) 125 | let node = nodes[0] // there can only be one node with a given ID 126 | return node.x 127 | } 128 | 129 | // Get html elements for tip and lists 130 | const tipElement = document.getElementById('tip') 131 | const listElement = document.getElementById('list') 132 | const nodeTitleElement = document.getElementById('node-title') 133 | 134 | // called each time the selection changes. 135 | // Takes 1 param: the triggering event which is sneakily passed in by the 136 | // listener. We check this event to make sure it has data because the listener 137 | // can be triggered by unresolved promises, which we want to ignore. trigger 138 | // defaults to a fake event with data so that if the function is called with no 139 | // parameters, it runs. 140 | async function updateSidebar(trigger = {data: ["go"]}) { 141 | 142 | // clear sidebar every time selection is updates so we don't build up more and 143 | // more list items 144 | clearSidebar() 145 | 146 | // don't do anything if the listener was triggered by an unresolved promise 147 | // this happens when deselecting 148 | if (trigger.data[0] == undefined) { 149 | console.log("Sidebar update without data!") 150 | return 151 | } 152 | 153 | ///// //// /// // / FUNCTIONS / // /// //// ///// 154 | // hide tip and show current node in sidebar 155 | // requires title to be defined 156 | function hideTipShowText() { 157 | tipElement.style.opacity = '0' 158 | nodeTitleElement.style.opacity = '1' 159 | nodeTitleElement.textContent = title 160 | } 161 | // show tip, clear node title, and clear parents/children 162 | function clearSidebar() { 163 | tipElement.style.opacity = '1' 164 | nodeTitleElement.style.opacity = '0' 165 | nodeTitleElement.textContent = '-' 166 | clearList() 167 | } 168 | 169 | async function updateList(list) { 170 | // update the list of each relation (parents/children) 171 | 172 | // generate a list of html li elements from the list of relations 173 | var elementList = [] 174 | for (var key in list) { 175 | // for each key, get the title and id 176 | let title = list[key][0] 177 | let id = key 178 | 179 | var o = document.createElement('li') // create the new html element 'o' 180 | o.classList.add("nav-link") 181 | o.onclick = function() { doOnclick(id) } 182 | o.onmouseover = function() { doOnMouseover(id) } 183 | o.onmouseout = function() { doOnMouseout(id) } 184 | o.appendChild(document.createTextNode(title)) // label with node title 185 | 186 | // give the element style to match the node it corresponds to 187 | let nodeStyle = await getNodeStyle(id) 188 | o.style.backgroundColor = nodeStyle.backgroundColor 189 | o.style.borderColor = nodeStyle.borderColor 190 | o.style.borderWidth = nodeStyle.borderWidth / 2 + "px" 191 | o.style.color = nodeStyle.textColor 192 | o.style.fontWeight = nodeStyle.bold == 1 ? "bold" : "inherit" 193 | 194 | // store the node's x coordinate as data-* attribute on the html element 195 | let nodeX = await getNodeX(id) 196 | o["data-x"] = nodeX 197 | 198 | elementList.push(o) // append the li element to the list 199 | } 200 | 201 | // sort the list of li elements by x coordinate 202 | elementList.sort((a, b) => a["data-x"] - b["data-x"]) 203 | 204 | // append all the elements to the correct parent element (a ul) in order 205 | elementList.forEach(o => listElement.appendChild(o)) 206 | } 207 | 208 | // walk the tree recursively to generate a list of all leaf nodes 209 | // then filter the list by background color to delete all complete nodes 210 | function getAllLeafNodes(node) { 211 | // get children of given node: 212 | // filter for the edges where the node of interest is the 'endWidgetId', 213 | // meaning it is the parent, and the 'startWidgetId' is the child 214 | var edges = allObjects.filter(i => i.type === 'LINE') 215 | let childNodes = [] 216 | let childrenEdges = edges.filter(l => l.endWidgetId === node.id) 217 | childrenEdges.forEach(edge => { 218 | let childNode = allObjects.find(o => o.id === edge.startWidgetId) 219 | // only add valid nodes to the list 220 | if (typeof childNode.text !== 'string' || childNode.type != "SHAPE") { 221 | return 222 | } 223 | childNodes.push(childNode) 224 | }) 225 | 226 | // if the node has no children, it's a leaf so return it 227 | if (childNodes.length == 0) { 228 | return [node] 229 | } else { 230 | // otherwise the node has children, so recurse: 231 | // return an array of the result of this function run on all the children 232 | // nodes 233 | let out = [] 234 | childNodes.forEach(c => { 235 | // RECURSIVE STEP! 236 | // for each child node, recurse 237 | subChildren = getActionableNodes(c) 238 | // and combine the results into one array 239 | out = out.concat(subChildren) 240 | }) 241 | return out 242 | } 243 | } 244 | 245 | // filter for nodes that aren't complete or purple 246 | function getActionableNodes(node) { 247 | const unactionableColors = ['#8fd14f', '#9510ac'] 248 | 249 | leaves = getAllLeafNodes(node) 250 | return leaves.filter(l => { 251 | b = l.style.backgroundColor 252 | return !unactionableColors.includes(b) 253 | }) 254 | } 255 | ///// //// /// // / END FUNCTIONS / // /// //// ///// 256 | 257 | // Get selected widgets 258 | let widgets = await miro.board.selection.get() 259 | // If there's no selection, don't do anything 260 | if (widgets[0] == undefined) { 261 | return 262 | } 263 | // Get first widget from selected widgets 264 | var widget = widgets[0] 265 | 266 | var text = widget.text 267 | var type = widget.type 268 | 269 | // Check that widget is a valid node with a title. If it's not, stop. 270 | if (typeof text !== 'string' || type != "SHAPE") { 271 | console.log("Valid node must be selected!") 272 | return 273 | } 274 | 275 | // set up variables that updateList implicitly needs 276 | var title = getTitleFromNode(widgets[0]) 277 | var allObjects = await miro.board.getAllObjects() 278 | viewport = await miro.board.viewport.getViewport() 279 | var showContextZoomTimer 280 | var previewZoomTimer 281 | 282 | // set up updateList parameters 283 | var nodeList = makeNodeList(getActionableNodes(widget)) 284 | 285 | hideTipShowText() // show current node text 286 | updateList(nodeList) 287 | } 288 | -------------------------------------------------------------------------------- /public/actionables.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/actionables.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const actionablesIcon = ` 3 | 4 | A 5 | ` 6 | miro.initialize({ 7 | extensionPoints: { 8 | bottomBar: { 9 | title: 'ᴀᴄᴏʀɴ: Explore actionable smalls for selected node', 10 | svgIcon: actionablesIcon, 11 | positionPriority: 993, 12 | onClick: () => { 13 | miro.board.ui.openLeftSidebar('actionables-sidebar.html') 14 | } 15 | } 16 | } 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /public/climbing-harness-sidebar.css: -------------------------------------------------------------------------------- 1 | .scrollable-container { 2 | overflow-y: auto; 3 | position: absolute; 4 | bottom: 0; 5 | top: 63.5px; 6 | padding-right: 5%; 7 | } 8 | .rtb-sidebar-caption { 9 | font-size: 14px; 10 | font-weight: bold; 11 | color: rgba(0, 0, 0, 0.7); 12 | padding: 24px 0 20px 24px; 13 | } 14 | textarea { 15 | display: block; 16 | height: 25%; 17 | margin: 20px 0 0 20px; 18 | background-color: white; 19 | border-radius: 4px; 20 | border: .5; 21 | width: calc(100% - 40px); 22 | text-align: left; 23 | font-size: 14px; 24 | } 25 | .tip { 26 | color: #CCCCCC; 27 | margin: 20px 0 0 26px; 28 | } 29 | .node { 30 | margin: 20px 0 20px 10px; 31 | } 32 | .node-title { 33 | display: block; 34 | margin: 0 auto; 35 | width: 100%; 36 | text-align: center; 37 | font-weight: bold; 38 | } 39 | ul { 40 | padding-left: 5%; 41 | } 42 | li { 43 | list-style: none; 44 | margin: 2px 0 2px 0; 45 | padding: 2px 5px; 46 | } 47 | .nav-link { 48 | cursor: pointer; 49 | font-weight: 500; 50 | border: 1px solid black; 51 | } 52 | header { 53 | margin-left: 10px; 54 | font-weight: bold; 55 | font-size: larger; 56 | } 57 | -------------------------------------------------------------------------------- /public/climbing-harness-sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    Climbing Harness
    14 |
    15 | 16 |
    Parents:
    17 |
      18 | 19 |
      20 | Select a node to view its connections. 21 |
      22 |
      23 | 24 |
      25 | 26 |
      Children:
      27 |
        28 | 29 | 30 |
        31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/climbing-harness-sidebar.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | // subscribe on user selected widgets 3 | miro.addListener(miro.enums.event.SELECTION_UPDATED, updateSidebar) 4 | // update the sidebar once when it first opens 5 | updateSidebar() 6 | }) 7 | 8 | // get the entire text of a node without any of the characters phloem adds 9 | function getFullTextFromNode(node) { 10 | html = node.text 11 | html = html.replace(//gi, '') 12 | html = html.replace(//gi, '') 13 | html = html.replace(/<\/div>/ig, '\n') 14 | html = html.replace(/<\/li>/ig, '\n') 15 | html = html.replace(/
      • /ig, ' * ') 16 | html = html.replace(/<\/ul>/ig, '\n') 17 | html = html.replace(/<\/p>/ig, '\n') 18 | html = html.replace(//gi, "\n") 19 | html = html.replace(/<[^>]+>/ig, '') 20 | text = html.replace(/⋅/g,'') // remove newline markers if there are any 21 | return text 22 | } 23 | 24 | // calculate the issue title from a node's text 25 | function getTitleFromNode(node) { 26 | let titleLength = 13 27 | 28 | var fullText = getFullTextFromNode(node) 29 | 30 | // remove metadata (stuff after ##) 31 | var textWithoutMetadata 32 | if (fullText.includes('##')) { 33 | textWithoutMetadata = fullText.split('##').slice(0, -1).join(' ') 34 | } else { 35 | textWithoutMetadata = fullText 36 | } 37 | 38 | // get the first x words to use as the title 39 | if (textWithoutMetadata.split(' ').length <= titleLength) { 40 | return textWithoutMetadata 41 | } else { 42 | // add ... if the text is longer than 10 words 43 | return textWithoutMetadata.split(' ').slice(0,titleLength).join(' ') + "..." 44 | } 45 | } 46 | 47 | // returns a hash of the ID and short and long text of each node in the given array. 48 | // Example: { 84759034802: ['This is...', 'This is a test node.'], 9879: ['Another...', 'Another one.'] } 49 | // later we use this hash to build the list of the parent and children nodes. 50 | // The title is required to display in the box, and the id is required so that 51 | // we can zoom the viewport to that node onclick. 52 | function makeNodeList(nodeArray) { 53 | nodeList = {} 54 | // console.log("nodearray:") 55 | // console.log(nodeArray) 56 | nodeArray.forEach(node => { 57 | if (node == undefined) { 58 | return 59 | } 60 | nodeList[node.id] = [getTitleFromNode(node), getFullTextFromNode(node)] 61 | }) 62 | return nodeList 63 | } 64 | 65 | // remove all items from both lists 66 | function clearLists() { 67 | while (parentListElement.firstChild) { 68 | parentListElement.removeChild(parentListElement.firstChild) 69 | } 70 | while (childListElement.firstChild) { // refactor? 71 | childListElement.removeChild(childListElement.firstChild) 72 | } 73 | } 74 | 75 | // zoom out a bit and select the node when a list item is clicked on 76 | async function doOnclick(id) { 77 | // clear both lists so the sidebar is empty when the viewport is animating 78 | clearLists() 79 | let zoomLevel = await miro.board.viewport.getZoom() // store current zoom level 80 | await miro.board.viewport.setZoom(zoomLevel * 1.10) // zoom in just a bit 81 | await miro.board.selection.selectWidgets(id) // then select the current widget 82 | } 83 | // when a list item is moused over, zoom to that widget after a quick pause. 84 | // once looking at it, zoom out a bunch to show the context (where in the tree) 85 | // the node is. 86 | function doOnMouseover(id) { 87 | // delay between mousing over a list item and zooming over to preview it (ms) 88 | let delayBeforePreviewZoom = 250 89 | // delay between starting to zoom to a widget and zooming out for context (ms) 90 | let delayBeforeShowContextZoom = 600 91 | // how much to zoom out to show context 92 | let showContextZoomFactor = .30 93 | 94 | function previewZoom() { 95 | async function showContextZoom() { 96 | let zoomLevel = await miro.board.viewport.getZoom() 97 | await miro.board.viewport.setZoom(zoomLevel * showContextZoomFactor) 98 | } 99 | miro.board.viewport.zoomToObject(id) 100 | showContextZoomTimer = window.setTimeout(showContextZoom, delayBeforeShowContextZoom) 101 | } 102 | 103 | previewZoomTimer = window.setTimeout(previewZoom, delayBeforePreviewZoom) 104 | } 105 | // when the mouse stops hovering over a list item, return to the viewport we 106 | // saved when updateSidebar was called (usually when the node was selected) 107 | function doOnMouseout(id) { 108 | miro.board.viewport.setViewportWithAnimation(viewport) 109 | window.clearTimeout(showContextZoomTimer) 110 | window.clearTimeout(previewZoomTimer) 111 | } 112 | 113 | // returns the style object of the element with the given ID so the list item in 114 | // the sidebar can match its background color, font weight, border color, and 115 | // border width. 116 | async function getNodeStyle(id) { 117 | let nodes = await miro.board.widgets.get({id: id}) 118 | let node = nodes[0] // there can only be one node with a given ID 119 | return node.style 120 | } 121 | 122 | async function getNodeX(id) { 123 | let nodes = await miro.board.widgets.get({id: id}) 124 | let node = nodes[0] // there can only be one node with a given ID 125 | return node.x 126 | } 127 | 128 | // Get html elements for tip and lists 129 | const tipElement = document.getElementById('tip') 130 | const nodeElement = document.getElementById('node') 131 | const nodeTitleElement = nodeElement.children['node-title'] 132 | const parentListElement = document.getElementById('parent-list') 133 | const childListElement = document.getElementById('child-list') 134 | 135 | // called each time the selection changes. 136 | // Takes 1 param: the triggering event which is sneakily passed in by the 137 | // listener. We check this event to make sure it has data because the listener 138 | // can be triggered by unresolved promises, which we want to ignore. trigger 139 | // defaults to a fake event with data so that if the function is called with no 140 | // parameters, it runs. 141 | async function updateSidebar(trigger = {data: ["go"]}) { 142 | 143 | // clear sidebar every time selection is updates so we don't build up more and 144 | // more list items 145 | clearSidebar() 146 | 147 | // don't do anything if the listener was triggered by an unresolved promise 148 | // this happens when deselecting 149 | if (trigger.data[0] == undefined) { 150 | console.log("Sidebar update without data!") 151 | return 152 | } 153 | 154 | ///// //// /// // / FUNCTIONS / // /// //// ///// 155 | // hide tip and show current node in sidebar 156 | // requires title to be defined 157 | function hideTipShowText() { 158 | tipElement.style.opacity = '0' 159 | nodeElement.style.opacity = '1' 160 | nodeTitleElement.textContent = title 161 | } 162 | // show tip, clear node title, and clear parents/children 163 | function clearSidebar() { 164 | tipElement.style.opacity = '1' 165 | nodeElement.style.opacity = '0' 166 | nodeTitleElement.textContent = ' ' 167 | clearLists() 168 | } 169 | 170 | async function updateLists(parentList, childList) { 171 | // update the list of each relation (parents/children) 172 | [[parentList,parentListElement], [childList,childListElement]].forEach(async relation => { 173 | var list = relation[0] 174 | var element = relation[1] 175 | 176 | // generate a list of html li elements from the list of relations 177 | var elementList = [] 178 | for (var key in list) { 179 | // for each key, get the title and id 180 | let title = list[key][0] 181 | let id = key 182 | 183 | var o = document.createElement('li') // create the new html element 'o' 184 | o.classList.add("nav-link") 185 | o.onclick = function() { doOnclick(id) } 186 | o.onmouseover = function() { doOnMouseover(id) } 187 | o.onmouseout = function() { doOnMouseout(id) } 188 | o.appendChild(document.createTextNode(title)) // label with node title 189 | 190 | // give the element style to match the node it corresponds to 191 | let nodeStyle = await getNodeStyle(id) 192 | o.style.backgroundColor = nodeStyle.backgroundColor 193 | o.style.borderColor = nodeStyle.borderColor 194 | o.style.borderWidth = nodeStyle.borderWidth / 2 + "px" 195 | o.style.color = nodeStyle.textColor 196 | o.style.fontWeight = nodeStyle.bold == 1 ? "bold" : "inherit" 197 | 198 | // store the node's x coordinate as data-* attribute on the html element 199 | let nodeX = await getNodeX(id) 200 | o["data-x"] = nodeX 201 | 202 | elementList.push(o) // append the li element to the list 203 | } 204 | 205 | // sort the list of li elements by x coordinate 206 | elementList.sort((a, b) => a["data-x"] - b["data-x"]) 207 | 208 | // append all the elements to the correct parent element (a ul) in order 209 | elementList.forEach(o => element.appendChild(o)) 210 | }) 211 | } 212 | 213 | function getChildNodes(node) { 214 | // filter for the edges where the node of interest 215 | // is the 'endWidgetId', meaning it is the parent, and the 'startWidgetId' is the child 216 | var edges = allObjects.filter(i => i.type === 'LINE') 217 | let childNodes = [] 218 | let childrenEdges = edges.filter(l => l.endWidgetId === node.id) 219 | childrenEdges.forEach(edge => { 220 | let childNode = allObjects.find(o => o.id === edge.startWidgetId) 221 | // only add valid nodes to the list 222 | if (typeof childNode.text !== 'string' || childNode.type != "SHAPE") { 223 | return 224 | } 225 | childNodes.push(childNode) 226 | }) 227 | return childNodes 228 | } 229 | function getParentNodes(node) { // FIXME refactor 230 | var edges = allObjects.filter(i => i.type === 'LINE') 231 | let parentNodes = [] 232 | let parentEdges = edges.filter(l => l.startWidgetId === node.id) 233 | parentEdges.forEach(edge => { 234 | let parentNode = allObjects.find(o => o.id === edge.endWidgetId) 235 | if (typeof parentNode.text !== 'string' || parentNode.type != "SHAPE") { 236 | return 237 | } 238 | parentNodes.push(parentNode) 239 | }) 240 | return parentNodes 241 | } 242 | ///// //// /// // / END FUNCTIONS / // /// //// ///// 243 | 244 | // Get selected widgets 245 | let widgets = await miro.board.selection.get() 246 | // If there's no selection, don't do anything 247 | if (widgets[0] == undefined) { 248 | return 249 | } 250 | // Get first widget from selected widgets 251 | var widget = widgets[0] 252 | 253 | var text = widget.text 254 | var type = widget.type 255 | 256 | // Check that widget is a valid node with a title. If it's not, stop. 257 | if (typeof text !== 'string' || type != "SHAPE") { 258 | console.log("Valid node must be selected!") 259 | return 260 | } 261 | 262 | // set up variables that updateLists implicitly needs 263 | var title = getTitleFromNode(widgets[0]) 264 | var allObjects = await miro.board.getAllObjects() 265 | viewport = await miro.board.viewport.getViewport() 266 | var showContextZoomTimer 267 | var previewZoomTimer 268 | 269 | // set up updateList parameters 270 | var parentNodeList = makeNodeList(getParentNodes(widget)) 271 | var childNodeList = makeNodeList(getChildNodes(widget)) 272 | 273 | hideTipShowText() // show current node text 274 | updateLists(parentNodeList, childNodeList) 275 | } 276 | -------------------------------------------------------------------------------- /public/climbing-harness.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/climbing-harness.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const climbingHarnessIcon = ` 3 | 4 | 9 | 10 | 11 | 12 | ` 13 | miro.initialize({ 14 | extensionPoints: { 15 | bottomBar: { 16 | title: 'ᴀᴄᴏʀɴ: Explore parents and children of selected node', 17 | svgIcon: climbingHarnessIcon, 18 | positionPriority: 994, 19 | onClick: () => { 20 | miro.board.ui.openLeftSidebar('climbing-harness-sidebar.html') 21 | } 22 | } 23 | } 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /public/create-issue-modal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 31 | 48 | 49 | 50 | 51 | 52 |
        In which repo would you like to create this issue?
        53 |
        54 | 55 | 57 | 58 |    63 | 64 |


        65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/create-issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/create-issue.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const createIssueIcon = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ` 42 | 43 | miro.initialize({ 44 | extensionPoints: { 45 | bottomBar: { 46 | title: 'ᴀᴄᴏʀɴ: Create GitHub issue for selected node', // note: gets turned to lowercase by Miro 47 | svgIcon: createIssueIcon, 48 | positionPriority: 995, 49 | onClick: activateModal 50 | } 51 | } 52 | }) 53 | }) 54 | 55 | // get the entire text of a node without any of the characters phloem adds 56 | function getFullTextFromNode(root) { 57 | return root.text.replace(/⋅/g,'') // remove newline markers if there are any 58 | } 59 | 60 | // calculate the issue title from a node's text 61 | function getTitleFromNode(root) { 62 | let titleLength = 10 63 | 64 | var fullText = getFullTextFromNode(root) 65 | // get the first x words to use as the title 66 | if (fullText.split(' ').length <= titleLength) { 67 | return fullText 68 | } else { 69 | // add ... if the text is longer than 10 words 70 | return fullText.split(' ').slice(0,titleLength).join(' ') + "..." 71 | } 72 | } 73 | 74 | // calculate the issue body text from a node. This is the entire text plus a 75 | // link back to the node. Takes board info object because this has to be gotten 76 | // inside an async function. 77 | function getBodyFromNode(root, boardInfo) { 78 | var fullText = getFullTextFromNode(root) 79 | // append link to node in Miro 80 | return fullText + '

        See card in SoA Tree' 81 | } 82 | 83 | // return the URL that is typed in a config node in Miro 84 | async function getServerURL() { 85 | let configBackgroundColor = "#CA59E2" 86 | // find nodes with the right background color. Miro turns hex codes lowercase 87 | var configNodes = await miro.board.widgets.get({type: 'shape', style:{backgroundColor: configBackgroundColor.toLowerCase()}}) 88 | var configNode = configNodes[0] 89 | var serverURL = configNode.text 90 | // console.log(configNode) 91 | // console.log(serverURL) 92 | return serverURL 93 | } 94 | 95 | // check that the selection is valid and then activate the modal popup 96 | async function activateModal() { 97 | // gets the selected widgets on the board, returns an array 98 | let selection = await miro.board.selection.get() 99 | // validate that we can proceed with the selected item 100 | if (!validateSelection(selection)) return 101 | 102 | // prompt the user to choose the repo in which to create the issue 103 | miro.board.ui.openModal('create-issue-modal.html') 104 | } 105 | 106 | // create and send an issue of the selected node to the specified repo 107 | async function createAndSendIssue(repoPath) { 108 | // gets the selected widgets on the board, returns an array 109 | let selection = await miro.board.selection.get() 110 | // gets the board info 111 | let boardInfo = await miro.board.info.get() 112 | 113 | miro.showNotification('Creating issue...') 114 | console.log("creating issue to send to repo: " + repoPath) 115 | 116 | let root = selection[0] 117 | 118 | var title = getTitleFromNode(root) 119 | var text = getBodyFromNode(root, boardInfo) 120 | 121 | // send the issue to simpleserver.js to be sent to GitHub 122 | sendIssue(title, text, repoPath) 123 | 124 | miro.showNotification(`Successfully created issue in repo: ${repoPath}`) 125 | } 126 | 127 | const uncertainRed = "#f24726" 128 | const incompleteYellow = "#fac710" 129 | const completeGreen = "#8fd14f" 130 | const smallGreen = "#0ca789" 131 | const timeTeal = "#12cdd4" 132 | 133 | // var variables = {} 134 | // function setVariable(attribute, value) { 135 | // variables[attribute] = value 136 | // } 137 | // 138 | // function getVariable(attribute) { 139 | // return variables[attribute] 140 | // } 141 | // variables["test"] = 104 142 | 143 | async function sendIssue(title, body, repoPath, assignee = null, labels = null) { 144 | 145 | var serverURL = await getServerURL() 146 | var url = serverURL + "/create-issue" 147 | 148 | var xhr = new XMLHttpRequest() 149 | xhr.open('POST', url, true) 150 | xhr.setRequestHeader("Content-type", "application/json") 151 | 152 | // set up the issue JSON with the desired information 153 | var json = { 154 | "issueRepoPath": repoPath, 155 | "issueTitle": title, 156 | "issueBody": body, 157 | "issueLabels": [], 158 | "issueAssignee": null 159 | } 160 | 161 | // convert issue JSON to string 162 | var issue = JSON.stringify(json) 163 | // send issue 164 | xhr.send(issue) 165 | 166 | } 167 | 168 | function validateSelection(selection) { 169 | // validate only one selection 170 | if (selection.length > 1) { 171 | miro.showErrorNotification('You must only have one node selected') 172 | return false 173 | } else if (selection.length === 0) { 174 | miro.showErrorNotification('You must have at least one node selected') 175 | return false 176 | } else if (selection[0].type !== "SHAPE") { 177 | miro.showErrorNotification('The selected element must be a node') 178 | return false 179 | } else { 180 | return true 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /public/set-complete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/set-complete.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const completeIcon = ` 3 | ` 4 | 5 | miro.initialize({ 6 | extensionPoints: { 7 | bottomBar: { 8 | title: 'ᴀᴄᴏʀɴ: Set selected as complete', 9 | svgIcon: completeIcon, 10 | positionPriority: 997, 11 | onClick: completeSelected 12 | } 13 | } 14 | }) 15 | }) 16 | 17 | 18 | async function completeSelected() { 19 | // gets the selected widgets on the board, returns an array 20 | let selection = await miro.board.selection.get() 21 | 22 | // validate that we can proceed with the selected item 23 | if (!validateSelection(selection)) return 24 | 25 | let root = selection[0] 26 | 27 | miro.board.widgets.shapes.update(root.id, { style: { 28 | backgroundColor: completeGreen, 29 | }}) 30 | } 31 | 32 | const uncertainRed = "#f24726" 33 | const incompleteYellow = "#fac710" 34 | const completeGreen = "#8fd14f" 35 | const smallGreen = "#0ca789" 36 | const timeTeal = "#12cdd4" 37 | 38 | function validateSelection(selection) { 39 | // validate only one selection 40 | if (selection.length > 1) { 41 | miro.showErrorNotification('You must only have one node selected') 42 | return false 43 | } else if (selection.length === 0) { 44 | miro.showErrorNotification('You must have at least one node selected') 45 | return false 46 | } else if (selection[0].type !== "SHAPE") { 47 | miro.showErrorNotification('The selected element must be a node') 48 | return false 49 | } else { 50 | return true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/set-incomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/set-incomplete.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const incompleteIcon =` 3 | 4 | ` 5 | 6 | miro.initialize({ 7 | extensionPoints: { 8 | bottomBar: { 9 | title: 'ᴀᴄᴏʀɴ: Set selected as incomplete', 10 | svgIcon: incompleteIcon, 11 | positionPriority: 998, 12 | onClick: incompleteSelected 13 | } 14 | } 15 | }) 16 | }) 17 | 18 | 19 | async function incompleteSelected() { 20 | // gets the selected widgets on the board, returns an array 21 | let selection = await miro.board.selection.get() 22 | 23 | // validate that we can proceed with the selected item 24 | if (!validateSelection(selection)) return 25 | 26 | let root = selection[0] 27 | 28 | miro.board.widgets.shapes.update(root.id, { style: { 29 | backgroundColor: incompleteYellow, 30 | borderColor: smallGreen, 31 | borderWidth: 16 32 | }}) 33 | } 34 | 35 | const uncertainRed = "#f24726" 36 | const incompleteYellow = "#fac710" 37 | const completeGreen = "#8fd14f" 38 | const smallGreen = "#0ca789" 39 | const timeTeal = "#12cdd4" 40 | 41 | function validateSelection(selection) { 42 | // validate only one selection 43 | if (selection.length > 1) { 44 | miro.showErrorNotification('You must only have one node selected') 45 | return false 46 | } else if (selection.length === 0) { 47 | miro.showErrorNotification('You must have at least one node selected') 48 | return false 49 | } else if (selection[0].type !== "SHAPE") { 50 | miro.showErrorNotification('The selected element must be a node') 51 | return false 52 | } else { 53 | return true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/set-uncertain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/set-uncertain.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const uncertainIcon =` 3 | 4 | ` 5 | 6 | miro.initialize({ 7 | extensionPoints: { 8 | bottomBar: { 9 | title: 'ᴀᴄᴏʀɴ: Set selected as uncertain', 10 | svgIcon: uncertainIcon, 11 | positionPriority: 996, 12 | onClick: uncertainSelected 13 | } 14 | } 15 | }) 16 | }) 17 | 18 | 19 | async function uncertainSelected() { 20 | // gets the selected widgets on the board, returns an array 21 | let selection = await miro.board.selection.get() 22 | 23 | // validate that we can proceed with the selected item 24 | if (!validateSelection(selection)) return 25 | 26 | let root = selection[0] 27 | 28 | miro.board.widgets.shapes.update(root.id, { style: { 29 | backgroundColor: uncertainRed, 30 | borderColor: "#000000", 31 | borderWidth: 2 32 | }}) 33 | } 34 | 35 | const uncertainRed = "#f24726" 36 | const incompleteYellow = "#fac710" 37 | const completeGreen = "#8fd14f" 38 | const smallGreen = "#0ca789" 39 | const timeTeal = "#12cdd4" 40 | 41 | function validateSelection(selection) { 42 | // validate only one selection 43 | if (selection.length > 1) { 44 | miro.showErrorNotification('You must only have one node selected') 45 | return false 46 | } else if (selection.length === 0) { 47 | miro.showErrorNotification('You must have at least one node selected') 48 | return false 49 | } else if (selection[0].type !== "SHAPE") { 50 | miro.showErrorNotification('The selected element must be a node') 51 | return false 52 | } else { 53 | return true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/tree-logic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/tree-logic.js: -------------------------------------------------------------------------------- 1 | miro.onReady(() => { 2 | const treeLogicIcon = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ` 39 | 40 | miro.initialize({ 41 | extensionPoints: { 42 | bottomBar: { 43 | title: 'ᴀᴄᴏʀɴ: Calculate subtree sizes for selected', // note: gets turned to lowercase by Miro 44 | svgIcon: treeLogicIcon, 45 | positionPriority: 996, 46 | onClick: updateDataForSelected 47 | } 48 | } 49 | }) 50 | }) 51 | 52 | 53 | async function updateDataForSelected() { 54 | // gets the selected widgets on the board, returns an array 55 | let selection = await miro.board.selection.get() 56 | 57 | // validate that we can proceed with the selected item 58 | if (!validateSelection(selection)) return 59 | 60 | miro.showNotification('Calculating subtree sizes') 61 | 62 | // root is the widget that things will be calculated starting from and in relation to 63 | let root = selection[0] 64 | 65 | // get the entire list of objects on the board as an array 66 | let allObjects = await miro.board.getAllObjects() 67 | 68 | // collect the full list of edges 69 | let edges = allObjects.filter(i => i.type === 'LINE') 70 | 71 | // starting with the root and recursing, calculate all the sizes of the subtrees, and update the nodes 72 | getAndSetCountsForNode(root, allObjects, edges) 73 | miro.showNotification('Successfully set subtree sizes') 74 | } 75 | 76 | const uncertainRed = "#f24726" 77 | const incompleteYellow = "#fac710" 78 | const completeGreen = "#8fd14f" 79 | const smallGreen = "#0ca789" 80 | const timeTeal = "#12cdd4" 81 | 82 | const countPrefix = "##" 83 | const historySeparator = "--" 84 | 85 | function getAndSetCountsForNode(node, allObjects, edges) { 86 | // initialize the counts for this node 87 | let counts = { 88 | uncertain: {}, 89 | completedSmall: {}, 90 | uncompletedSmall:{} 91 | } 92 | // increase the counts for this node to include the counts of its children 93 | // filter for the edges where the node of interest 94 | // is the 'endWidgetId', meaning it is the parent, and the 'startWidgetId' is the child 95 | let childrenEdges = edges.filter(l => l.endWidgetId === node.id) 96 | childrenEdges.forEach(edge => { 97 | let childNode = allObjects.find(o => o.id === edge.startWidgetId) 98 | let childCounts = getAndSetCountsForNode(childNode, allObjects, edges) 99 | counts = {uncertain:{...childCounts.uncertain,...counts.uncertain}, 100 | completedSmall:{...childCounts.completedSmall,...counts.completedSmall}, 101 | uncompletedSmall:{...childCounts.uncompletedSmall,...counts.uncompletedSmall} 102 | } 103 | }) 104 | 105 | const completedSmallCount = Object.keys(counts.completedSmall).length 106 | const uncompletedSmallCount = Object.keys(counts.uncompletedSmall).length 107 | const totalSmallCount = completedSmallCount + uncompletedSmallCount 108 | const uncertainCount = Object.keys(counts.uncertain).length 109 | // at this point, the counts only include the totals of its children 110 | // use this moment to update the label for this node 111 | var countText 112 | 113 | // get the main text from the current node 114 | const orig_text = node.text.split(" "+countPrefix+" ") 115 | var main_text = orig_text[0] 116 | // use ⋅ to recognize the end of a line and reinsert a line break 117 | main_text = main_text.replace(/⋅/g,"⋅
        ") 118 | const old_counts = orig_text[1] 119 | 120 | // get the background color and border color, since that's representing completion and node-type 121 | let backgroundColor = node.style && (node.style.backgroundColor || node.style.stickerBackgroundColor) 122 | let borderColor = node.style && node.style.borderColor 123 | 124 | // update the history for time nodes. We only need to do this if there was an old_counts value, 125 | // i.e. we can skip this if this is the very time the tree is being calculated. 126 | if (backgroundColor === timeTeal && old_counts != undefined) { 127 | const count_parts = old_counts.split(" "+historySeparator+" ") // spaces because thats what the brs show up as 128 | const history_str = count_parts[1] === undefined ? "" : count_parts[1] ; 129 | countText = makeCountsWithHistoryStr(uncompletedSmallCount,totalSmallCount,uncertainCount,history_str) 130 | } else { 131 | // This is for non-time nodes, just use the plain counts string 132 | countText = makeCountsStr(uncompletedSmallCount,totalSmallCount,uncertainCount) 133 | } 134 | 135 | // now determine what this node itself is 136 | 137 | // uncertain or small? 138 | if (backgroundColor === uncertainRed) counts.uncertain[node.id] = true 139 | else if (borderColor === smallGreen) { 140 | if (backgroundColor === completeGreen) { 141 | counts.completedSmall[node.id] = true 142 | } else { 143 | counts.uncompletedSmall[node.id] = true 144 | } 145 | } 146 | 147 | // now update the text for the node 148 | const newText = `${main_text}
        ${countPrefix} ${countText}` 149 | miro.board.widgets.shapes.update(node.id, { text: newText }) 150 | 151 | 152 | // return counts so that recursion can happen 153 | return counts 154 | } 155 | 156 | function makeCountsStr(uncompletedSmallCount,totalSmallCount,uncertainCount,prediction) { 157 | const base = `S: ${uncompletedSmallCount}/${totalSmallCount} U: ${uncertainCount}` 158 | return (!prediction) ? base : `${base} P: ${prediction}` 159 | } 160 | 161 | function makeCountsWithHistoryStr(uncompletedSmallCount,totalSmallCount,uncertainCount,history_str) { 162 | const history = history_str === "" ? [] : parseHistory(history_str) 163 | const prediction = predict(history) 164 | var countText = makeCountsStr(uncompletedSmallCount,totalSmallCount,uncertainCount,prediction) 165 | const d = new Date(); 166 | 167 | // if there is no history at all, or if we are recalculating for the first time in a day 168 | // then add todays count to the top of the history. 169 | if (history.length == 0 || !isToday(history[0].date)) { 170 | const todays_date = dateStr(d) 171 | var today = `${todays_date}~ ${countText}` 172 | countText = `${countText}
        ${historySeparator}
        ${today}` 173 | if (history_str) { 174 | countText = `${countText};
        ${historyToString(history)}` 175 | } 176 | } else { 177 | countText = `${countText}
        ${historySeparator}
        ${historyToString(history)}` 178 | } 179 | return countText 180 | } 181 | 182 | 183 | function parseDate(date_str) { 184 | const parts = date_str.split('-'); 185 | return new Date(parts[0], parts[1] - 1, parts[2]); 186 | } 187 | 188 | function dateStr(d) { 189 | return d.getFullYear() + "-" + ("0"+(d.getMonth()+1)).slice(-2)+ "-"+ ("0" + d.getDate()).slice(-2) 190 | } 191 | 192 | function isToday(date_str) { 193 | const first = parseDate(date_str); 194 | const second = new Date(); 195 | return first.getYear() == second.getYear() && first.getDate() == second.getDate() && first.getMonth() == second.getMonth() 196 | } 197 | 198 | function historyToString(history) { 199 | return history.map(x => { 200 | return `${x.date}~ ${makeCountsStr(x.uncomplete,x.total,x.unknown,x.prediction)}` 201 | }).join(";
        ") 202 | } 203 | 204 | function parseHistory(historyStr) { 205 | const h = historyStr.split(/; */).filter(x=> x!=""); 206 | 207 | const re = /([^~]+)~ S: (.*?)\/(.*?) U: (.*)( P: (.*?))*/; 208 | const old_re = /(.*?): (.*)\/(.*)/; 209 | return h.map(x => { 210 | let m = re.exec(x) 211 | if (m) { 212 | return { 213 | date: m[1], 214 | uncomplete: m[2], 215 | total: m[3], 216 | unknown: !m[4]?"?":m[4], 217 | prediction: m[6], 218 | } 219 | } 220 | m = old_re.exec(x) 221 | if (m) { 222 | return { 223 | date: m[1], 224 | uncomplete: m[2], 225 | total: m[3], 226 | unknown: "?" 227 | } 228 | } 229 | }) 230 | } 231 | 232 | function predict(history) { 233 | const points = history.map(x=>{const date = Date.parse(x.date); return [parseInt(x.uncomplete),date]}) 234 | if (points.length <3) {return ""} 235 | const r = new regression("linear",points.reverse()) 236 | 237 | // the slope of the result has to be negative otherwise it means 238 | // we'll never finish 239 | if (r.string.startsWith("y = -")) { 240 | const zeroPoint = r.equation[1] 241 | return dateStr(new Date(zeroPoint)) 242 | } else { 243 | return "NEVER!"; 244 | } 245 | } 246 | 247 | function validateSelection(selection) { 248 | // validate only one selection 249 | if (selection.length > 1) { 250 | miro.showErrorNotification('You must only have one node selected') 251 | return false 252 | } else if (selection.length === 0) { 253 | miro.showErrorNotification('You must have at least one node selected') 254 | return false 255 | } else if (selection[0].type !== "SHAPE") { 256 | miro.showErrorNotification('The selected element must be a node') 257 | return false 258 | } else { 259 | return true 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /simpleserver.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | var express = require('express') 3 | var cors = require('cors') 4 | var serveStatic = require('serve-static') 5 | var app = express() 6 | const config = require('./config.json') 7 | const request = require('request'); 8 | 9 | let threeData = { 10 | nodes: [], 11 | links: [] 12 | } 13 | 14 | // id, name, data, children [] 15 | let spaceTreeData = {} 16 | 17 | // JSON parsing 18 | app.use(express.json({limit: '50mb'})) 19 | 20 | // necessary CORS headers 21 | app.use(cors({ 22 | origin: 'https://miro.com' 23 | })) 24 | 25 | // static file server 26 | app.use(serveStatic('public')) 27 | 28 | // send out issue information to GitHub API 29 | app.post('/create-issue', function (req, res) { 30 | // console.log(`REQ: ${req}`) 31 | // req should contain information about the issue (title, body, labels), and 32 | // the repo it should be created in 33 | var url = 'https://api.github.com/repos/' + req.body.issueRepoPath + '/issues' 34 | // add acorn to the other labels 35 | req.body.issueLabels.push("acorn") 36 | 37 | // set up the issue JSON with the desired information 38 | var issue = { 39 | "title": req.body.issueTitle, 40 | "body": req.body.issueBody, 41 | "labels": req.body.issueLabels 42 | } 43 | 44 | request({ 45 | url:url, 46 | method:"POST", 47 | json: true, 48 | body: issue, 49 | headers: { 50 | 'Authorization': 'Bearer ' + config.repos[req.body.issueRepoPath].accessToken,
 // look up access token 51 | 'Content-Type': 'application/json',
 52 | 'User-Agent': 'soa-toolbox-server' 53 | } 54 | }, function (err, res, body) { 55 | // console.log(`RES: ${res}`) 56 | }) 57 | 58 | }) 59 | 60 | // share the list of repos from config file with Miro so we can display them in the modal pop-up 61 | app.get('/get-config-repos', function (req, res) { 62 | // extract repo names from config file 63 | var repos = Object.keys(config.repos) 64 | // console.log(repos) 65 | res.send(repos) 66 | }) 67 | 68 | 69 | app.get('/get-3d-data', function (req, res) { 70 | res.send(threeData) 71 | }) 72 | 73 | app.get('/get-spacetree-data', function (req, res) { 74 | res.send(spaceTreeData) 75 | }) 76 | 77 | app.post('/update-soa-tree-data', function (req, res) { 78 | 79 | // reset and build the threeData 80 | threeData.nodes = [] 81 | threeData.links = [] 82 | req.body.forEach(widget => { 83 | switch (widget.type) { 84 | case 'LINE': 85 | if (widget.startWidgetId && widget.endWidgetId) { 86 | threeData.links.push({ 87 | source: widget.startWidgetId, 88 | target: widget.endWidgetId, 89 | id: widget.id 90 | }) 91 | } 92 | default: 93 | threeData.nodes.push({ 94 | id: widget.id, 95 | name: widget.text || widget.title || "", 96 | val: 5, 97 | color: widget.style ? widget.style.backgroundColor || widget.style.stickerBackgroundColor : '' 98 | }) 99 | } 100 | }) 101 | 102 | // build the data for the spacetree 103 | // gets the Holochain root node 104 | var rootId = '3074457346387129258' 105 | 106 | var closedAlphaRootNode = '3074457346342611504' 107 | var root = req.body.find(i => i.id === closedAlphaRootNode) 108 | spaceTreeData = { 109 | id: root.id, 110 | name: root.text, 111 | data: { 112 | $color: root.style ? root.style.backgroundColor || root.style.stickerBackgroundColor : 'red' 113 | }, 114 | children: [] 115 | } 116 | let collectedLinks = {} 117 | function addChildren(node) { 118 | threeData.links 119 | // get all the links that haven't been collected yet, and that are either from or to the node of interest 120 | .filter(link => (link.source === node.id || link.target === node.id) && !collectedLinks[link.id] && link.id !== '3074457346469683813') 121 | // add them to collected links, and to the children of the node of interest, and recurse 122 | .forEach(link => { 123 | collectedLinks[link.id] = link 124 | let connectedNodeId = link.source === node.id ? link.target : link.source 125 | let connectedNode = req.body.find(i => i.id === connectedNodeId) 126 | let childNode = { 127 | id: connectedNode.id, 128 | name: connectedNode.text, 129 | data: { 130 | $color: connectedNode.style ? connectedNode.style.backgroundColor || connectedNode.style.stickerBackgroundColor : 'red', 131 | small: connectedNode.style ? connectedNode.style.borderColor === "#0ca789" : false 132 | }, 133 | children: [] 134 | } 135 | addChildren(childNode) 136 | node.children.push(childNode) 137 | }) 138 | } 139 | addChildren(spaceTreeData) 140 | 141 | res.sendStatus(200) 142 | }) 143 | 144 | app.listen(process.env.PORT || 8088, function () { 145 | console.log('CORS-enabled web server listening on port ' + (process.env.PORT || 8088)) 146 | }) 147 | --------------------------------------------------------------------------------