├── README.md └── sketch_user_flows.sketchplugin └── Contents └── Sketch ├── exportAllInterface.js ├── exportSelectedInterface.js ├── importReqfireProject.js └── manifest.json /README.md: -------------------------------------------------------------------------------- 1 | # Sketch User Flows 2 | 3 | Download Sketch User Flows: auto-generate User Flows and keep your designs in sync with your Reqfire project 4 | 5 | 6 | ## Contents 7 | * Overview 8 | * Installation 9 | * How it works 10 | * FAQ 11 | * Get in touch 12 | 13 | ## The quickest way to create User Flows in Sketch 14 | 15 | Communicate your designs with User Flows: auto-generate User Flows and keep your designs in sync with your Reqfire project. 16 | 17 | **Generate** User Flow maps from your Reqfire project. 18 | 19 | **Sync** your designs in Sketch with your User Flows as they develop. 20 | 21 | 22 | 23 | ## Installing 24 | You can download our plugin right here: 25 | 26 | 27 | Download Sketch User Flows: auto-generate User Flows and keep your designs in sync with your Reqfire project 28 | 29 | 30 | Is this your first plugin? Here's the docs to better understand [how Sketch plugins work](https://www.sketchapp.com/docs/plugins/). 31 | 32 | ## Generate and Sync User Flows 33 | Generate a User Flow map in Sketch from a Reqfire project 34 | Sync Artboard designs in Sketch from a Reqfire project 35 | 36 | --- 37 | 38 | ## How it works: 39 | 40 | ### Step 1: Create User Flows in Reqfire 41 | Creating user flows in sketch 42 | 43 | ### Step 2: Use the plugin to import User Flows to Sketch 44 | In Sketch: Plugin > Sketch User Flows > Import Reqfire Project 45 | 46 | Import user flows 47 | 48 | ### Step 3: Sync your designs with Reqfire 49 | In Sketch: Plugin > Sketch User Flows > Sync All/Selected symbols to Reqfire 50 | 51 | Sync designs with Reqfire 52 | 53 | 54 | ## Get in touch 55 | 56 | Feel free to create an Issue or PR. Or drop us an email at [hello@reqfire.com](mailto:hello@reqfire.com) 57 | -------------------------------------------------------------------------------- /sketch_user_flows.sketchplugin/Contents/Sketch/exportAllInterface.js: -------------------------------------------------------------------------------- 1 | var that = this; 2 | function __skpm_run (key, context) { 3 | that.context = context; 4 | 5 | var exports = 6 | /******/ (function(modules) { // webpackBootstrap 7 | /******/ // The module cache 8 | /******/ var installedModules = {}; 9 | /******/ 10 | /******/ // The require function 11 | /******/ function __webpack_require__(moduleId) { 12 | /******/ 13 | /******/ // Check if module is in cache 14 | /******/ if(installedModules[moduleId]) { 15 | /******/ return installedModules[moduleId].exports; 16 | /******/ } 17 | /******/ // Create a new module (and put it into the cache) 18 | /******/ var module = installedModules[moduleId] = { 19 | /******/ i: moduleId, 20 | /******/ l: false, 21 | /******/ exports: {} 22 | /******/ }; 23 | /******/ 24 | /******/ // Execute the module function 25 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 26 | /******/ 27 | /******/ // Flag the module as loaded 28 | /******/ module.l = true; 29 | /******/ 30 | /******/ // Return the exports of the module 31 | /******/ return module.exports; 32 | /******/ } 33 | /******/ 34 | /******/ 35 | /******/ // expose the modules object (__webpack_modules__) 36 | /******/ __webpack_require__.m = modules; 37 | /******/ 38 | /******/ // expose the module cache 39 | /******/ __webpack_require__.c = installedModules; 40 | /******/ 41 | /******/ // define getter function for harmony exports 42 | /******/ __webpack_require__.d = function(exports, name, getter) { 43 | /******/ if(!__webpack_require__.o(exports, name)) { 44 | /******/ Object.defineProperty(exports, name, { 45 | /******/ configurable: false, 46 | /******/ enumerable: true, 47 | /******/ get: getter 48 | /******/ }); 49 | /******/ } 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ // Load entry module and return exports 68 | /******/ return __webpack_require__(__webpack_require__.s = 2); 69 | /******/ }) 70 | /************************************************************************/ 71 | /******/ ([ 72 | /* 0 */ 73 | /***/ (function(module, exports) { 74 | 75 | var IMPORT = "IMPORT"; 76 | var EXPORT_ALL = "EXPORT_ALL"; 77 | var EXPORT_SELECTED = "EXPORT_SELECTED"; 78 | 79 | module.exports = { 80 | IMPORT: IMPORT, 81 | EXPORT_ALL: EXPORT_ALL, 82 | EXPORT_SELECTED: EXPORT_SELECTED 83 | }; 84 | 85 | /***/ }), 86 | /* 1 */ 87 | /***/ (function(module, exports, __webpack_require__) { 88 | 89 | var insertionSort = __webpack_require__(4).insertionSort; 90 | 91 | // make request to get object data from json url 92 | function getJSONFromURL(url, key) { 93 | var task = NSTask.alloc().init(); 94 | task.setLaunchPath("/usr/bin/curl"); 95 | 96 | var args = NSMutableArray.alloc().init(); 97 | args.addObject("-v"); 98 | args.addObject("POST"); 99 | args.addObject("-F"); 100 | args.addObject("apikey=" + key); 101 | args.addObject(url); 102 | task.setArguments(args); 103 | 104 | var outputPipe = NSPipe.pipe(); 105 | task.setStandardOutput(outputPipe); 106 | task.launch(); 107 | 108 | var outputData = outputPipe.fileHandleForReading(); 109 | var data = outputData.readDataToEndOfFile(); 110 | var classNameOfOutput = NSStringFromClass(data["class"]()); 111 | 112 | if (classNameOfOutput != "_NSZeroData") { 113 | var res = NSJSONSerialization.JSONObjectWithData_options_error(data, NSJSONReadingMutableContainers, null); 114 | 115 | if (res != null) { 116 | if (res.status == 1) { 117 | //success 118 | //log(res.content); 119 | return res.content; 120 | } else { 121 | return null; 122 | } 123 | } 124 | } else { 125 | return null; 126 | } 127 | } 128 | 129 | //retrive image data from url 130 | function getImageFromURL(url, ingnoreCache) { 131 | var request = ingnoreCache ? NSURLRequest.requestWithURL_cachePolicy_timeoutInterval(NSURL.URLWithString(url), NSURLRequestReloadIgnoringLocalCacheData, 60) : NSURLRequest.requestWithURL(NSURL.URLWithString(url)); 132 | 133 | var responsePtr = MOPointer.alloc().init(); 134 | var errorPtr = MOPointer.alloc().init(); 135 | var data = NSURLConnection.sendSynchronousRequest_returningResponse_error(request, responsePtr, errorPtr); 136 | 137 | if (errorPtr.value() != null) { 138 | return null; 139 | } 140 | 141 | var response = responsePtr.value(); 142 | 143 | if (response.statusCode() != 200) { 144 | return null; 145 | } 146 | 147 | var mimeType = response.allHeaderFields()["Content-Type"]; 148 | 149 | if (!mimeType || !mimeType.hasPrefix("image/")) { 150 | return null; 151 | } 152 | 153 | return NSImage.alloc().initWithData(data); 154 | } 155 | 156 | function organiseData(object) { 157 | var flattenByKey = function flattenByKey(object, property) { 158 | return Object.keys(object[property]).map(function (key) { 159 | return object[property][key]; 160 | }); 161 | }; 162 | data = {}; 163 | data.ifaces = object.ifaces; 164 | data.groups = flattenByKey(object, "groups"); 165 | 166 | data.groups = insertionSort(data.groups); 167 | data.groups.forEach(function (group) { 168 | if (group.uc) { 169 | var tempUc = flattenByKey(group, "uc"); 170 | group.uc = insertionSort(tempUc); 171 | group.uc.forEach(function (uc) { 172 | uc.flow = flattenByKey(uc, "flow"); 173 | // we don't order flows by number (for now) 174 | uc.flow.forEach(function (flow) { 175 | if (flow.step) { 176 | var tempStep = flattenByKey(flow, "step"); 177 | flow.step = insertionSort(tempStep); 178 | flow.step.forEach(function (step) { 179 | if (step.object) { 180 | step.object = flattenByKey(step, "object"); 181 | step.object = step.object.find(function (obj) { 182 | return obj.object_type == "12"; 183 | }); 184 | } 185 | }); 186 | } 187 | }); 188 | }); 189 | } 190 | }); 191 | 192 | // log(data); 193 | return data; 194 | } 195 | 196 | function logRequest(key, message, isImport) { 197 | var task = NSTask.alloc().init(); 198 | task.setLaunchPath("/usr/bin/curl"); 199 | 200 | var typeNumber = isImport ? 0 : 1; 201 | 202 | var args = NSMutableArray.alloc().init(); 203 | args.addObject("-v"); 204 | args.addObject("POST"); 205 | args.addObject("-F"); 206 | args.addObject("message=" + message); 207 | args.addObject("https://www.reqfire.com/app/project/sketchevent/type/" + typeNumber + "/ext/" + key); 208 | task.setArguments(args); 209 | 210 | var outputPipe = NSPipe.pipe(); 211 | task.setStandardOutput(outputPipe); 212 | task.launch(); 213 | } 214 | 215 | module.exports = { 216 | getJSONFromURL: getJSONFromURL, 217 | getImageFromURL: getImageFromURL, 218 | organiseData: organiseData, 219 | logRequest: logRequest 220 | }; 221 | 222 | /***/ }), 223 | /* 2 */ 224 | /***/ (function(module, exports, __webpack_require__) { 225 | 226 | Object.defineProperty(exports, "__esModule", { 227 | value: true 228 | }); 229 | 230 | exports["default"] = function (context) { 231 | // call the function with allSymbos === true 232 | return exportInterfaces(context, true); 233 | }; 234 | 235 | var exportInterfaces = __webpack_require__(3).exportInterfaces; 236 | 237 | /***/ }), 238 | /* 3 */ 239 | /***/ (function(module, exports, __webpack_require__) { 240 | 241 | var getJSONFromURL = __webpack_require__(1).getJSONFromURL; 242 | var getImageFromURL = __webpack_require__(1).getImageFromURL; 243 | var organiseData = __webpack_require__(1).organiseData; 244 | var logRequest = __webpack_require__(1).logRequest; 245 | var getInputFromUser = __webpack_require__(5).getInputFromUser; 246 | var EXPORT_ALL = __webpack_require__(0).EXPORT_ALL; 247 | var EXPORT_SELECTED = __webpack_require__(0).EXPORT_SELECTED; 248 | 249 | function exportInterfaces(context, exportAllSymbols) { 250 | var sketch = context.api(); 251 | var doc = context.document; 252 | var pages = doc.pages(); 253 | var projectImportURL = "https://www.reqfire.com/app/project/sketchexport/"; 254 | 255 | var app = NSApplication.sharedApplication(); 256 | // Get file path 257 | var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(); 258 | 259 | // Get file content 260 | var manifest = NSJSONSerialization.JSONObjectWithData_options_error(NSData.dataWithContentsOfFile(manifestPath), 0, nil); 261 | var version = manifest.version; 262 | 263 | var layersToExport; 264 | if (exportAllSymbols) { 265 | layersToExport = getLayers(context, pages); 266 | } else { 267 | layersToExport = checkSymbols(context.selection); 268 | if (context.selection.length < 1) { 269 | app.displayDialog_withTitle("Please ensure you have at least one symbol artboard selected.", "Error — No symbol artboard selected for sync"); 270 | return null; 271 | } else { 272 | if (layersToExport.length < 1) { 273 | app.displayDialog_withTitle("Please ensure the symbol artboards selected aren't empty.", "Error — Unable to sync current selection"); 274 | return null; 275 | } 276 | } 277 | } 278 | if (layersToExport.length == 0) { 279 | app.displayDialog_withTitle("No need to export", "No Update Found"); 280 | return null; 281 | } 282 | 283 | // Get the user's project API key. Pass true for export 284 | var type = exportAllSymbols ? EXPORT_ALL : EXPORT_SELECTED; 285 | var apiKey = getInputFromUser(context, type); 286 | if (!apiKey || apiKey.isEqualToString("")) { 287 | // TODO: prompt that they've cancelled. 288 | return null; 289 | } 290 | // Get JSON object 291 | var jsonResponse = getJSONFromURL(projectImportURL, apiKey); 292 | if (!jsonResponse) { 293 | app.displayDialog_withTitle("Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", "Error — No API key was entered"); 294 | logRequest(apiKey, "Invalid API Key", false); 295 | return null; 296 | } 297 | 298 | if (!version.isEqualToString(jsonResponse.version)) { 299 | app.displayDialog_withTitle("You can download the latest version from https://github.com/reqfire/sketch-user-flows/", "Error — Plugin version is not up to date"); 300 | logRequest(apiKey, "Plugin out of date", false); 301 | log("wrong version"); 302 | return null; 303 | } 304 | 305 | var projectName = jsonResponse.project.name; // get current project name 306 | 307 | var organisedData = organiseData(jsonResponse); 308 | 309 | // Temporary image path 310 | var exportPath = NSTemporaryDirectory() + "sketch-reqfire-export/"; 311 | 312 | // Convert all symbols into exportable image 313 | var exportInfoList = layersToPNG(context, layersToExport); 314 | 315 | var flag; 316 | for (var i = 0; i < exportInfoList.length; i++) { 317 | for (var j = 0; j < organisedData.ifaces.length; j++) { 318 | var iName = organisedData.ifaces[j].name; 319 | var iPid = organisedData.ifaces[j].persistent_id; 320 | var X = exportInfoList[i].layerName; 321 | var id = X.split(" id:")[1]; 322 | 323 | if (id == organisedData.ifaces[j].persistent_id) { 324 | flag = 1; // symbol ID exists in object ifaces 325 | break; 326 | } else { 327 | flag = -1; 328 | } 329 | } 330 | 331 | if (flag == -1) { 332 | // symbol does not match ifaces 333 | exportInfoList.splice(i, 1); // remove item out of the export list 334 | print("failed"); 335 | } 336 | } 337 | 338 | //upload image to server 339 | if (getConfirmationFromUser(exportInfoList)) { 340 | var uploadStatus = updateLayer(exportInfoList, apiKey); 341 | 342 | if (uploadStatus == 1) { 343 | app.displayDialog_withTitle("Please refresh your Reqfire project to see the changes.", "Success — Your project has been synced successfully"); 344 | } else if (uploadStatus == 2) { 345 | app.displayDialog_withTitle("Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", "Error — Invalid API key provided"); 346 | } else if (uploadStatus == 3) { 347 | // TODO - delete this 348 | app.displayDialog_withTitle("Unmatched project key and api key!", "Unable to Export"); 349 | } else { 350 | app.displayDialog_withTitle("Error!", "Unable to Export"); 351 | } 352 | } else { 353 | log("User has cancelled Export All."); 354 | // app.displayDialogue_withTitle("Export cancelled"); 355 | } 356 | 357 | // return a list of symbols need to be exported 358 | function getLayers(context) { 359 | var loopSymbolMasters = context.document.documentData().allSymbols().objectEnumerator(); 360 | var symbolMaster; 361 | var layers = []; // a list to save exportable symbol 362 | 363 | while (symbolMaster = loopSymbolMasters.nextObject()) { 364 | // symbol children count equals one when there is no child in the symbol. 365 | // only symbols has children can be added into the list 366 | if (symbolMaster.children().count() != 1) { 367 | layers.push(symbolMaster); 368 | } 369 | } 370 | return layers; 371 | } 372 | 373 | // convert layer into image 374 | function layersToPNG(context, layers) { 375 | var exportInfoList = []; // prepare list with export layer info 376 | for (var i = 0; i < layers.length; i++) { 377 | var mslayer = layers[i]; 378 | var layerID = mslayer.objectID(); 379 | 380 | // store image into a temp path 381 | var filePath = exportPath + layerID + ".png"; 382 | var exportRequest = MSExportRequest.exportRequestsFromExportableLayer(mslayer).firstObject(); 383 | 384 | context.document.saveArtboardOrSlice_toFile(exportRequest, filePath); // save layer to file 385 | var exportInfo = { 386 | layerID: layerID, 387 | layerName: mslayer.name(), 388 | layer: mslayer, 389 | path: filePath 390 | }; 391 | 392 | exportInfoList.push(exportInfo); 393 | } 394 | return exportInfoList; 395 | } 396 | 397 | //upload symbols 398 | function updateLayer(list, apiKey) { 399 | var fullURl = "https://www.reqfire.com/app/component/sketchimport"; 400 | var task = NSTask.alloc().init(); 401 | var uploadStatus; 402 | task.setLaunchPath("/usr/bin/curl"); 403 | var args = NSMutableArray.alloc().init(); 404 | args.addObject("-v"); 405 | args.addObject("POST"); 406 | args.addObject("--header"); 407 | args.addObject("Content-Type: multipart/form-data"); 408 | 409 | for (var i = 0; i < list.length; i++) { 410 | args.addObject("-F"); 411 | args.addObject(list[i]["layerID"] + "=" + list[i]["layerName"]); 412 | args.addObject("-F"); 413 | args.addObject(list[i]["layerName"].split(" id:")[1] + "=@" + list[i]["path"]); 414 | } 415 | 416 | args.addObject("-F"); 417 | args.addObject("apikey=" + apiKey); 418 | args.addObject(fullURl); 419 | task.setArguments(args); 420 | var outputPipe = NSPipe.pipe(); 421 | task.setStandardOutput(outputPipe); 422 | task.launch(); 423 | var outputData = outputPipe.fileHandleForReading(); 424 | var data = outputData.readDataToEndOfFile(); 425 | var classNameOfOutput = NSStringFromClass(data["class"]()); 426 | 427 | if (classNameOfOutput != "_NSZeroData") { 428 | var res = NSJSONSerialization.JSONObjectWithData_options_error(data, NSJSONReadingMutableLeaves, nil); 429 | 430 | if (res != null) { 431 | if (res.error == nil) { 432 | uploadStatus = res.status; 433 | } 434 | } 435 | } else { 436 | uploadStatus = 0; 437 | } 438 | print(res); 439 | return uploadStatus; 440 | } 441 | 442 | function getConfirmationFromUser(exportInfoList) { 443 | var alert = NSAlert.alloc().init(); 444 | var plural = exportInfoList.length == 1 ? "" : "s"; 445 | 446 | alert.setMessageText("Confirming Project Sync"); 447 | alert.setInformativeText(exportInfoList.length + " symbol" + plural + " (containing content) selected to be synced with — " + projectName); 448 | alert.addButtonWithTitle("Ok"); 449 | alert.addButtonWithTitle("Cancel"); 450 | 451 | return alert.runModal() == NSAlertFirstButtonReturn; 452 | } 453 | 454 | function checkSymbols(layers) { 455 | var flag = 0; 456 | var layerList = []; // a list to save exportable symbol 457 | layers.forEach(function (layer) { 458 | // check the selected layer is symbol ot not 459 | if (layer instanceof MSSymbolMaster) { 460 | // symbol children count equals one when there is no child in the symbol. 461 | // only symbols has children can be added into the list 462 | if (layer.children().count() != 1) { 463 | layerList.push(layer); 464 | } 465 | } 466 | }); 467 | return layerList; 468 | } 469 | } 470 | 471 | module.exports = { 472 | exportInterfaces: exportInterfaces 473 | }; 474 | 475 | /***/ }), 476 | /* 4 */ 477 | /***/ (function(module, exports) { 478 | 479 | function insertionSort(items) { 480 | var len = items.length, 481 | value, 482 | i, 483 | j; 484 | 485 | for (i = 0; i < len; i++) { 486 | // store the current value because it may shift later 487 | value = items[i]; 488 | for (j = i - 1; j > -1 && Number(items[j].number) > Number(value.number); j--) { 489 | items[j + 1] = items[j]; 490 | } 491 | items[j + 1] = value; 492 | } 493 | return items; 494 | } 495 | 496 | module.exports = { 497 | insertionSort: insertionSort 498 | }; 499 | 500 | /***/ }), 501 | /* 5 */ 502 | /***/ (function(module, exports, __webpack_require__) { 503 | 504 | var IMPORT = __webpack_require__(0).IMPORT; 505 | var EXPORT_ALL = __webpack_require__(0).EXPORT_ALL; 506 | var EXPORT_SELECTED = __webpack_require__(0).EXPORT_SELECTED; 507 | 508 | function getInputFromUser(context, type) { 509 | var window = type === IMPORT ? createImportWindow(context) : createExportWindow(context, type); 510 | var alert = window[0]; 511 | var response = alert.runModal(); 512 | if (response == "1000") { 513 | //save user input to user defaults to enable auto filled text field 514 | // userDefaults.setObject_forKey(apiKey, "apiKey"); 515 | // userDefaults.synchronize(); 516 | return jsonTextField.stringValue(); 517 | } else { 518 | return false; 519 | } 520 | } 521 | 522 | function createImportWindow(context) { 523 | userDefaults = NSUserDefaults.alloc().initWithSuiteName("com.bohemiancoding.sketch.exportSelectedInterface"); 524 | 525 | var alert = COSAlertWindow["new"](); 526 | alert.setMessageText("Import Reqfire Project"); 527 | alert.addButtonWithTitle("Confirm"); 528 | alert.addButtonWithTitle("Cancel"); 529 | var viewWidth = 400; 530 | var viewHeight = 150; 531 | var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight)); 532 | alert.addAccessoryView(view); 533 | 534 | var linkLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 55, viewWidth - 100, 35)); 535 | linkLabel.setStringValue("Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch."); 536 | linkLabel.setSelectable(false); 537 | linkLabel.setEditable(false); 538 | linkLabel.setBezeled(false); 539 | linkLabel.setDrawsBackground(false); 540 | 541 | var jsonLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 85, viewWidth - 100, 20)); 542 | jsonLabel.setStringValue("Enter your API key"); 543 | jsonLabel.setSelectable(false); 544 | jsonLabel.setEditable(false); 545 | jsonLabel.setBezeled(false); 546 | jsonLabel.setDrawsBackground(false); 547 | 548 | view.addSubview(linkLabel); 549 | view.addSubview(jsonLabel); 550 | 551 | jsonTextField = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 105, 300, 20)); 552 | view.addSubview(jsonTextField); 553 | 554 | return [alert]; 555 | } 556 | 557 | function createExportWindow(context, type) { 558 | // Creates a user defaults object initialized with the defaults for the specified domain name 559 | userDefaults = NSUserDefaults.alloc().initWithSuiteName("com.bohemiancoding.sketch.exportAllInterface"); 560 | 561 | var alert = COSAlertWindow["new"](); 562 | if (type === EXPORT_ALL) { 563 | alert.setMessageText("Sync All Artboards"); 564 | } else if (type === EXPORT_SELECTED) { 565 | alert.setMessageText("Sync Selected Artboards"); 566 | } 567 | alert.addButtonWithTitle("Confirm"); 568 | alert.addButtonWithTitle("Cancel"); 569 | 570 | var viewWidth = 400; 571 | var viewHeight = 160; 572 | var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight)); 573 | alert.addAccessoryView(view); 574 | 575 | var alertLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 55, viewWidth - 100, 50)); 576 | var infoLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 95, viewWidth - 100, 35)); 577 | var jsonLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 120, viewWidth - 100, 20)); 578 | 579 | alertLabel.setStringValue("Note: Any symbols you create that are not sitting inside the symbol artboards generated by this plugin will not be synced up to your Reqfire project."); 580 | alertLabel.setSelectable(false); 581 | alertLabel.setEditable(false); 582 | alertLabel.setBezeled(false); 583 | alertLabel.setDrawsBackground(false); 584 | 585 | infoLabel.setStringValue("Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch."); 586 | infoLabel.setSelectable(false); 587 | infoLabel.setEditable(false); 588 | infoLabel.setBezeled(false); 589 | infoLabel.setDrawsBackground(false); 590 | 591 | jsonLabel.setStringValue("Enter your API key"); 592 | jsonLabel.setSelectable(false); 593 | jsonLabel.setEditable(false); 594 | jsonLabel.setBezeled(false); 595 | jsonLabel.setDrawsBackground(false); 596 | 597 | view.addSubview(alertLabel); 598 | view.addSubview(infoLabel); 599 | view.addSubview(jsonLabel); 600 | 601 | jsonTextField = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 140, 300, 20)); 602 | jsonTextField.setStringValue(""); 603 | view.addSubview(jsonTextField); 604 | 605 | return [alert]; 606 | } 607 | 608 | module.exports = { 609 | getInputFromUser: getInputFromUser, 610 | createImportWindow: createImportWindow, 611 | createExportWindow: createExportWindow 612 | }; 613 | 614 | /***/ }) 615 | /******/ ]); 616 | if (key === 'default' && typeof exports === 'function') { 617 | exports(context); 618 | } else { 619 | exports[key](context); 620 | } 621 | } 622 | that['onRun'] = __skpm_run.bind(this, 'default') 623 | -------------------------------------------------------------------------------- /sketch_user_flows.sketchplugin/Contents/Sketch/exportSelectedInterface.js: -------------------------------------------------------------------------------- 1 | var that = this; 2 | function __skpm_run (key, context) { 3 | that.context = context; 4 | 5 | var exports = 6 | /******/ (function(modules) { // webpackBootstrap 7 | /******/ // The module cache 8 | /******/ var installedModules = {}; 9 | /******/ 10 | /******/ // The require function 11 | /******/ function __webpack_require__(moduleId) { 12 | /******/ 13 | /******/ // Check if module is in cache 14 | /******/ if(installedModules[moduleId]) { 15 | /******/ return installedModules[moduleId].exports; 16 | /******/ } 17 | /******/ // Create a new module (and put it into the cache) 18 | /******/ var module = installedModules[moduleId] = { 19 | /******/ i: moduleId, 20 | /******/ l: false, 21 | /******/ exports: {} 22 | /******/ }; 23 | /******/ 24 | /******/ // Execute the module function 25 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 26 | /******/ 27 | /******/ // Flag the module as loaded 28 | /******/ module.l = true; 29 | /******/ 30 | /******/ // Return the exports of the module 31 | /******/ return module.exports; 32 | /******/ } 33 | /******/ 34 | /******/ 35 | /******/ // expose the modules object (__webpack_modules__) 36 | /******/ __webpack_require__.m = modules; 37 | /******/ 38 | /******/ // expose the module cache 39 | /******/ __webpack_require__.c = installedModules; 40 | /******/ 41 | /******/ // define getter function for harmony exports 42 | /******/ __webpack_require__.d = function(exports, name, getter) { 43 | /******/ if(!__webpack_require__.o(exports, name)) { 44 | /******/ Object.defineProperty(exports, name, { 45 | /******/ configurable: false, 46 | /******/ enumerable: true, 47 | /******/ get: getter 48 | /******/ }); 49 | /******/ } 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ // Load entry module and return exports 68 | /******/ return __webpack_require__(__webpack_require__.s = 2); 69 | /******/ }) 70 | /************************************************************************/ 71 | /******/ ([ 72 | /* 0 */ 73 | /***/ (function(module, exports, __webpack_require__) { 74 | 75 | var insertionSort = __webpack_require__(3).insertionSort; 76 | 77 | // make request to get object data from json url 78 | function getJSONFromURL(url, key) { 79 | var task = NSTask.alloc().init(); 80 | task.setLaunchPath("/usr/bin/curl"); 81 | 82 | var args = NSMutableArray.alloc().init(); 83 | args.addObject("-v"); 84 | args.addObject("POST"); 85 | args.addObject("-F"); 86 | args.addObject("apikey=" + key); 87 | args.addObject(url); 88 | task.setArguments(args); 89 | 90 | var outputPipe = NSPipe.pipe(); 91 | task.setStandardOutput(outputPipe); 92 | task.launch(); 93 | 94 | var outputData = outputPipe.fileHandleForReading(); 95 | var data = outputData.readDataToEndOfFile(); 96 | var classNameOfOutput = NSStringFromClass(data["class"]()); 97 | 98 | if (classNameOfOutput != "_NSZeroData") { 99 | var res = NSJSONSerialization.JSONObjectWithData_options_error(data, NSJSONReadingMutableContainers, null); 100 | 101 | if (res != null) { 102 | if (res.status == 1) { 103 | //success 104 | //log(res.content); 105 | return res.content; 106 | } else { 107 | return null; 108 | } 109 | } 110 | } else { 111 | return null; 112 | } 113 | } 114 | 115 | //retrive image data from url 116 | function getImageFromURL(url, ingnoreCache) { 117 | var request = ingnoreCache ? NSURLRequest.requestWithURL_cachePolicy_timeoutInterval(NSURL.URLWithString(url), NSURLRequestReloadIgnoringLocalCacheData, 60) : NSURLRequest.requestWithURL(NSURL.URLWithString(url)); 118 | 119 | var responsePtr = MOPointer.alloc().init(); 120 | var errorPtr = MOPointer.alloc().init(); 121 | var data = NSURLConnection.sendSynchronousRequest_returningResponse_error(request, responsePtr, errorPtr); 122 | 123 | if (errorPtr.value() != null) { 124 | return null; 125 | } 126 | 127 | var response = responsePtr.value(); 128 | 129 | if (response.statusCode() != 200) { 130 | return null; 131 | } 132 | 133 | var mimeType = response.allHeaderFields()["Content-Type"]; 134 | 135 | if (!mimeType || !mimeType.hasPrefix("image/")) { 136 | return null; 137 | } 138 | 139 | return NSImage.alloc().initWithData(data); 140 | } 141 | 142 | function organiseData(object) { 143 | var flattenByKey = function flattenByKey(object, property) { 144 | return Object.keys(object[property]).map(function (key) { 145 | return object[property][key]; 146 | }); 147 | }; 148 | data = {}; 149 | data.ifaces = object.ifaces; 150 | data.groups = flattenByKey(object, "groups"); 151 | 152 | data.groups = insertionSort(data.groups); 153 | data.groups.forEach(function (group) { 154 | if (group.uc) { 155 | var tempUc = flattenByKey(group, "uc"); 156 | group.uc = insertionSort(tempUc); 157 | group.uc.forEach(function (uc) { 158 | uc.flow = flattenByKey(uc, "flow"); 159 | // we don't order flows by number (for now) 160 | uc.flow.forEach(function (flow) { 161 | if (flow.step) { 162 | var tempStep = flattenByKey(flow, "step"); 163 | flow.step = insertionSort(tempStep); 164 | flow.step.forEach(function (step) { 165 | if (step.object) { 166 | step.object = flattenByKey(step, "object"); 167 | step.object = step.object.find(function (obj) { 168 | return obj.object_type == "12"; 169 | }); 170 | } 171 | }); 172 | } 173 | }); 174 | }); 175 | } 176 | }); 177 | 178 | // log(data); 179 | return data; 180 | } 181 | 182 | function logRequest(key, message, isImport) { 183 | var task = NSTask.alloc().init(); 184 | task.setLaunchPath("/usr/bin/curl"); 185 | 186 | var typeNumber = isImport ? 0 : 1; 187 | 188 | var args = NSMutableArray.alloc().init(); 189 | args.addObject("-v"); 190 | args.addObject("POST"); 191 | args.addObject("-F"); 192 | args.addObject("message=" + message); 193 | args.addObject("https://www.reqfire.com/app/project/sketchevent/type/" + typeNumber + "/ext/" + key); 194 | task.setArguments(args); 195 | 196 | var outputPipe = NSPipe.pipe(); 197 | task.setStandardOutput(outputPipe); 198 | task.launch(); 199 | } 200 | 201 | module.exports = { 202 | getJSONFromURL: getJSONFromURL, 203 | getImageFromURL: getImageFromURL, 204 | organiseData: organiseData, 205 | logRequest: logRequest 206 | }; 207 | 208 | /***/ }), 209 | /* 1 */ 210 | /***/ (function(module, exports) { 211 | 212 | var IMPORT = "IMPORT"; 213 | var EXPORT_ALL = "EXPORT_ALL"; 214 | var EXPORT_SELECTED = "EXPORT_SELECTED"; 215 | 216 | module.exports = { 217 | IMPORT: IMPORT, 218 | EXPORT_ALL: EXPORT_ALL, 219 | EXPORT_SELECTED: EXPORT_SELECTED 220 | }; 221 | 222 | /***/ }), 223 | /* 2 */ 224 | /***/ (function(module, exports, __webpack_require__) { 225 | 226 | Object.defineProperty(exports, "__esModule", { 227 | value: true 228 | }); 229 | 230 | exports["default"] = function (context) { 231 | return exportInterfaces(context, false); 232 | }; 233 | 234 | var getJSONFromURL = __webpack_require__(0).getJSONFromURL; 235 | 236 | 237 | // var sketch = context.api(); 238 | // var doc = context.document; 239 | // var pages = doc.pages(); 240 | // var uploadStatus; 241 | // var jsonInput; 242 | // const jsonUrl = "https://www.reqfire.com/app/project/sketchexport/"; 243 | // var app = NSApplication.sharedApplication(); 244 | // var userDefaults; 245 | // var layers = context.selection; 246 | 247 | // if (layers.length < 1) { 248 | // app.displayDialog_withTitle( 249 | // "Please ensure you have at least one symbol artboard selected.", 250 | // "Error — No symbol artboard selected for sync" 251 | // ); 252 | // } else { 253 | // if (checkSymbols(layers).length < 1) { 254 | // app.displayDialog_withTitle( 255 | // "Please ensure the symbol artboards selected aren't empty.", 256 | // "Error — Unable to sync current selection" 257 | // ); 258 | // } else { 259 | // if (getInputFromUser(context)) { 260 | // logRequest(jsonInput); 261 | // var obj = getJSONFromURL(jsonUrl, jsonInput); 262 | 263 | // //get file path 264 | // var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(); 265 | // //get file content 266 | // var manifest = NSJSONSerialization.JSONObjectWithData_options_error( 267 | // NSData.dataWithContentsOfFile(manifestPath), 268 | // 0, 269 | // nil 270 | // ); 271 | // var version = manifest.version; 272 | 273 | // var exportPath = NSTemporaryDirectory() + "sketch-reqfire-export/"; // set a temporary path 274 | // var exportInfoList = layersToPNG(context, checkSymbols(layers)); 275 | 276 | // if (obj) { 277 | // projectName = obj.project.name; 278 | // if (version == obj.version) { 279 | // obj = convertData(obj); 280 | // var flag; 281 | // for (var i = 0; i < exportInfoList.length; i++) { 282 | // for (var j = 0; j < obj.ifaces.length; j++) { 283 | // var iPid = obj.ifaces[j].persistent_id; 284 | // var X = exportInfoList[i].layerName; 285 | // var id = X.split(" id:")[1]; 286 | // if (id == obj.ifaces[j].persistent_id) { 287 | // flag = 1; 288 | // break; 289 | // } else { 290 | // flag = -1; 291 | // } 292 | // } 293 | // if (flag == -1) { 294 | // exportInfoList.splice(i, 1); 295 | // print("failed"); 296 | // } 297 | // } 298 | 299 | // if (getConfirmationFromUser(exportInfoList)) { 300 | // updateLayer(exportInfoList); 301 | 302 | // if (uploadStatus == 1) { 303 | // app.displayDialog_withTitle( 304 | // "Please refresh your Reqfire project to see the changes.", 305 | // "Success — Your project has been synced successfully" 306 | // ); 307 | // } else if (uploadStatus == 2) { 308 | // app.displayDialog_withTitle( 309 | // "Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", 310 | // "Error — Invalid API key provided" 311 | // ); 312 | // } else if (uploadStatus == 3) { 313 | // // TODO - delete this 314 | // app.displayDialog_withTitle( 315 | // "Unmatched project key and api key!", 316 | // "Unable to Export" 317 | // ); 318 | // } else { 319 | // app.displayDialog_withTitle("Error!", "Unable to Export"); 320 | // } 321 | // print(layers.length); 322 | // } else { 323 | // log("User has cancelled Export Selected.") 324 | // // app.displayDialogue_withTitle("Export cancelled"); 325 | // } 326 | // } else { 327 | // app.displayDialog_withTitle( 328 | // "You can download the latest version from https://github.com/reqfire/sketch-user-flows/", 329 | // "Error — Plugin version is not up to date" 330 | // ); 331 | // } 332 | // } else { 333 | // app.displayDialog_withTitle( 334 | // "Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", 335 | // "Error — No project found" 336 | // ); 337 | // } 338 | // } else { 339 | // log("cancelled"); 340 | // } 341 | // } 342 | // } 343 | 344 | // function layersToPNG(context, layers) { 345 | // var exportInfoList = []; // prepare list with export layer info 346 | 347 | // for (var i = 0; i < layers.length; i++) { 348 | // var mslayer = layers[i]; 349 | // var layerID = mslayer.objectID(); 350 | // // store image into a temp path 351 | // var filePath = exportPath + layerID + ".png"; 352 | // var exportRequest = MSExportRequest.exportRequestsFromExportableLayer( 353 | // mslayer 354 | // ).firstObject(); 355 | 356 | // context.document.saveArtboardOrSlice_toFile(exportRequest, filePath); // save layer to file 357 | 358 | // var exportInfo = { 359 | // layerID: layerID, 360 | // layerName: mslayer.name(), 361 | // layer: mslayer, 362 | // path: filePath 363 | // }; 364 | 365 | // exportInfoList.push(exportInfo); 366 | // } 367 | 368 | // return exportInfoList; 369 | // } 370 | 371 | // function updateLayer(list) { 372 | // var fullURl = "https://www.reqfire.com/app/component/sketchimport"; 373 | // var task = NSTask.alloc().init(); 374 | // task.setLaunchPath("/usr/bin/curl"); 375 | 376 | // var args = NSMutableArray.alloc().init(); 377 | // args.addObject("-v"); 378 | // args.addObject("POST"); 379 | // args.addObject("--header"); 380 | // args.addObject("Content-Type: multipart/form-data"); 381 | 382 | // for (var i = 0; i < list.length; i++) { 383 | // args.addObject("-F"); 384 | // args.addObject(list[i]["layerID"] + "=" + list[i]["layerName"]); 385 | // args.addObject("-F"); 386 | // args.addObject( 387 | // list[i]["layerName"].split(" id:")[1] + "=@" + list[i]["path"] 388 | // ); 389 | // } 390 | 391 | // args.addObject("-F"); 392 | // args.addObject("apikey=" + jsonInput); 393 | // args.addObject(fullURl); 394 | // task.setArguments(args); 395 | // var outputPipe = NSPipe.pipe(); 396 | // task.setStandardOutput(outputPipe); 397 | // task.launch(); 398 | 399 | // var outputData = outputPipe.fileHandleForReading(); 400 | // var data = outputData.readDataToEndOfFile(); 401 | // var classNameOfOutput = NSStringFromClass(data.class()); 402 | 403 | // if (classNameOfOutput != "_NSZeroData") { 404 | // var res = NSJSONSerialization.JSONObjectWithData_options_error( 405 | // data, 406 | // NSJSONReadingMutableLeaves, 407 | // nil 408 | // ); 409 | 410 | // if (res != null) { 411 | // uploadStatus = res.status; 412 | // } 413 | // } else { 414 | // uploadStatus = 0; 415 | // } 416 | // print(res); 417 | // } 418 | 419 | // // read data from json url 420 | // function getJSONFromURL(url, key) { 421 | // var task = NSTask.alloc().init(); 422 | // task.setLaunchPath("/usr/bin/curl"); 423 | 424 | // var args = NSMutableArray.alloc().init(); 425 | // args.addObject("-v"); 426 | // args.addObject("POST"); 427 | // args.addObject("-F"); 428 | // args.addObject("apikey=" + key); 429 | // args.addObject(url); 430 | // task.setArguments(args); 431 | // var outputPipe = NSPipe.pipe(); 432 | // task.setStandardOutput(outputPipe); 433 | // task.launch(); 434 | 435 | // var outputData = outputPipe.fileHandleForReading(); 436 | // var data = outputData.readDataToEndOfFile(); 437 | // var classNameOfOutput = NSStringFromClass(data.class()); 438 | 439 | // if (classNameOfOutput != "_NSZeroData") { 440 | // var res = NSJSONSerialization.JSONObjectWithData_options_error( 441 | // data, 442 | // NSJSONReadingMutableContainers, 443 | // null 444 | // ); 445 | 446 | // if (res != null) { 447 | // log(res.status); 448 | // if (res.status == 1) { 449 | // return res.content; 450 | // } else { 451 | // return null; 452 | // } 453 | // // TODO error handling 454 | // } 455 | // } else { 456 | // return null; 457 | // } 458 | // // var res =NSString.alloc().initWithData_encoding(data,NSUTF8StringEncoding) 459 | // } 460 | 461 | // function convertData(object) { 462 | // const flattenByKey = function(object, property) { 463 | // return Object.keys(object[property]).map(function(key) { 464 | // return object[property][key]; 465 | // }); 466 | // }; 467 | 468 | // data = {}; 469 | // data.ifaces = object.ifaces; 470 | // data.groups = flattenByKey(object, "groups"); 471 | 472 | // data.groups.forEach(function(group) { 473 | // if (group.uc) { 474 | // group.uc = flattenByKey(group, "uc"); 475 | // group.uc.forEach(function(uc) { 476 | // uc.flow = flattenByKey(uc, "flow"); 477 | // uc.flow.forEach(function(flow) { 478 | // if (flow.step) { 479 | // flow.step = flattenByKey(flow, "step"); 480 | // flow.step.forEach(function(step) { 481 | // if (step.object) { 482 | // step.object = flattenByKey(step, "object"); 483 | // step.object = step.object.find(function(obj) { 484 | // return obj.object_type == "12"; 485 | // }); 486 | // } 487 | // }); 488 | // } 489 | // }); 490 | // }); 491 | // } 492 | // }); 493 | // return data; 494 | // } 495 | 496 | // //check selection is symbol or not, and check if it is symbol with no child 497 | // function checkSymbols(layers) { 498 | // var flag = 0; 499 | // var layerList = []; // a list to save exportable symbol 500 | // layers.forEach(function(layer) { 501 | // // check the selected layer is symbol ot not 502 | // if (layer instanceof MSSymbolMaster) { 503 | // // symbol children count equals one when there is no child in the symbol. 504 | // // only symbols has children can be added into the list 505 | // if (layer.children().count() != 1) { 506 | // layerList.push(layer); 507 | // } 508 | // } 509 | // }); 510 | // return layerList; 511 | // } 512 | 513 | // function getInputFromUser(context) { 514 | // var window = createInputWindow(context); 515 | // var alert = window[0]; 516 | // var response = alert.runModal(); 517 | // if (response == "1000") { 518 | // jsonInput = jsonTextField.stringValue(); 519 | // //save user input to user defaults to enable auto filled text field 520 | // userDefaults.setObject_forKey(jsonInput, "jsonInput"); 521 | // userDefaults.synchronize(); 522 | // return true; 523 | // } else { 524 | // return false; 525 | // } 526 | // } 527 | 528 | // function getConfirmationFromUser(exportInfoList) { 529 | // var alert = NSAlert.alloc().init(); 530 | // var plural = exportInfoList.length == 1 ? "" : "s"; 531 | // alert.setMessageText("Confirming project export"); 532 | // alert.setInformativeText( 533 | // exportInfoList.length + 534 | // " Symbol" + 535 | // plural + 536 | // " selected to be synced with your Reqfire project — " + projectName 537 | // ); 538 | // alert.addButtonWithTitle("Ok"); 539 | // alert.addButtonWithTitle("Cancel"); 540 | 541 | // return alert.runModal() == NSAlertFirstButtonReturn; 542 | // } 543 | 544 | // function createInputWindow(context) { 545 | // // Creates a user defaults object initialized with the defaults for the specified domain name 546 | // userDefaults = NSUserDefaults.alloc().initWithSuiteName( 547 | // "com.bohemiancoding.sketch.exportSelectedInterface" 548 | // ); 549 | 550 | // var alert = COSAlertWindow.new(); 551 | // alert.setMessageText("Enter your Reqfire project's API Key"); 552 | // alert.addButtonWithTitle("Ok"); 553 | // alert.addButtonWithTitle("Cancel"); 554 | // var viewWidth = 400; 555 | // var viewHeight = 160; 556 | // var view = NSView.alloc().initWithFrame( 557 | // NSMakeRect(0, 0, viewWidth, viewHeight) 558 | // ); 559 | // alert.addAccessoryView(view); 560 | 561 | // var alertLabel = NSTextField.alloc().initWithFrame( 562 | // NSMakeRect(0, viewHeight - 40, viewWidth - 100, 35) 563 | // ); 564 | // var infoLabel = NSTextField.alloc().initWithFrame( 565 | // NSMakeRect(0, viewHeight - 80, viewWidth - 100, 35) 566 | // ); 567 | // var jsonLabel = NSTextField.alloc().initWithFrame( 568 | // NSMakeRect(0, viewHeight - 105, viewWidth - 100, 20) 569 | // ); 570 | 571 | // alertLabel.setStringValue( 572 | // "Note: Any symbols you create that is not sitting inside the symbol artboards generated by Sketch User Flows will not be synced up to your Reqfire project." 573 | // ); 574 | // alertLabel.setSelectable(false); 575 | // alertLabel.setEditable(false); 576 | // alertLabel.setBezeled(false); 577 | // alertLabel.setDrawsBackground(false); 578 | 579 | // infoLabel.setStringValue( 580 | // "Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch." 581 | // ); 582 | // infoLabel.setSelectable(false); 583 | // infoLabel.setEditable(false); 584 | // infoLabel.setBezeled(false); 585 | // infoLabel.setDrawsBackground(false); 586 | 587 | // jsonLabel.setStringValue("Enter your API key"); 588 | // jsonLabel.setSelectable(false); 589 | // jsonLabel.setEditable(false); 590 | // jsonLabel.setBezeled(false); 591 | // jsonLabel.setDrawsBackground(false); 592 | 593 | // view.addSubview(alertLabel); 594 | // view.addSubview(infoLabel); 595 | // view.addSubview(jsonLabel); 596 | 597 | // jsonTextField = NSTextField.alloc().initWithFrame( 598 | // NSMakeRect(0, viewHeight - 125, 300, 20) 599 | // ); 600 | // jsonTextField.setStringValue(getJsonValue(context)); 601 | // view.addSubview(jsonTextField); 602 | 603 | // return [alert]; 604 | // } 605 | 606 | // function getJsonValue(context) { //get value by the key 607 | // var jsonValue = userDefaults.objectForKey("jsonInput"); 608 | // if (jsonValue != undefined) { 609 | // return jsonValue; 610 | // } else { 611 | // return ""; // Default value 612 | // } 613 | // } 614 | 615 | // function logRequest(key, message) { 616 | // var task = NSTask.alloc().init(); 617 | // task.setLaunchPath("/usr/bin/curl"); 618 | 619 | // var args = NSMutableArray.alloc().init(); 620 | // args.addObject("-v"); 621 | // args.addObject("POST"); 622 | // args.addObject("-F"); 623 | // args.addObject("message=" + message); 624 | // args.addObject("https://www.reqfire.com/app/project/sketchevent/type/0/ext/" + key); 625 | // task.setArguments(args); 626 | 627 | // var outputPipe = NSPipe.pipe(); 628 | // task.setStandardOutput(outputPipe); 629 | // task.launch(); 630 | // } 631 | var exportInterfaces = __webpack_require__(4).exportInterfaces; 632 | 633 | /***/ }), 634 | /* 3 */ 635 | /***/ (function(module, exports) { 636 | 637 | function insertionSort(items) { 638 | var len = items.length, 639 | value, 640 | i, 641 | j; 642 | 643 | for (i = 0; i < len; i++) { 644 | // store the current value because it may shift later 645 | value = items[i]; 646 | for (j = i - 1; j > -1 && Number(items[j].number) > Number(value.number); j--) { 647 | items[j + 1] = items[j]; 648 | } 649 | items[j + 1] = value; 650 | } 651 | return items; 652 | } 653 | 654 | module.exports = { 655 | insertionSort: insertionSort 656 | }; 657 | 658 | /***/ }), 659 | /* 4 */ 660 | /***/ (function(module, exports, __webpack_require__) { 661 | 662 | var getJSONFromURL = __webpack_require__(0).getJSONFromURL; 663 | var getImageFromURL = __webpack_require__(0).getImageFromURL; 664 | var organiseData = __webpack_require__(0).organiseData; 665 | var logRequest = __webpack_require__(0).logRequest; 666 | var getInputFromUser = __webpack_require__(5).getInputFromUser; 667 | var EXPORT_ALL = __webpack_require__(1).EXPORT_ALL; 668 | var EXPORT_SELECTED = __webpack_require__(1).EXPORT_SELECTED; 669 | 670 | function exportInterfaces(context, exportAllSymbols) { 671 | var sketch = context.api(); 672 | var doc = context.document; 673 | var pages = doc.pages(); 674 | var projectImportURL = "https://www.reqfire.com/app/project/sketchexport/"; 675 | 676 | var app = NSApplication.sharedApplication(); 677 | // Get file path 678 | var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(); 679 | 680 | // Get file content 681 | var manifest = NSJSONSerialization.JSONObjectWithData_options_error(NSData.dataWithContentsOfFile(manifestPath), 0, nil); 682 | var version = manifest.version; 683 | 684 | var layersToExport; 685 | if (exportAllSymbols) { 686 | layersToExport = getLayers(context, pages); 687 | } else { 688 | layersToExport = checkSymbols(context.selection); 689 | if (context.selection.length < 1) { 690 | app.displayDialog_withTitle("Please ensure you have at least one symbol artboard selected.", "Error — No symbol artboard selected for sync"); 691 | return null; 692 | } else { 693 | if (layersToExport.length < 1) { 694 | app.displayDialog_withTitle("Please ensure the symbol artboards selected aren't empty.", "Error — Unable to sync current selection"); 695 | return null; 696 | } 697 | } 698 | } 699 | if (layersToExport.length == 0) { 700 | app.displayDialog_withTitle("No need to export", "No Update Found"); 701 | return null; 702 | } 703 | 704 | // Get the user's project API key. Pass true for export 705 | var type = exportAllSymbols ? EXPORT_ALL : EXPORT_SELECTED; 706 | var apiKey = getInputFromUser(context, type); 707 | if (!apiKey || apiKey.isEqualToString("")) { 708 | // TODO: prompt that they've cancelled. 709 | return null; 710 | } 711 | // Get JSON object 712 | var jsonResponse = getJSONFromURL(projectImportURL, apiKey); 713 | if (!jsonResponse) { 714 | app.displayDialog_withTitle("Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", "Error — No API key was entered"); 715 | logRequest(apiKey, "Invalid API Key", false); 716 | return null; 717 | } 718 | 719 | if (!version.isEqualToString(jsonResponse.version)) { 720 | app.displayDialog_withTitle("You can download the latest version from https://github.com/reqfire/sketch-user-flows/", "Error — Plugin version is not up to date"); 721 | logRequest(apiKey, "Plugin out of date", false); 722 | log("wrong version"); 723 | return null; 724 | } 725 | 726 | var projectName = jsonResponse.project.name; // get current project name 727 | 728 | var organisedData = organiseData(jsonResponse); 729 | 730 | // Temporary image path 731 | var exportPath = NSTemporaryDirectory() + "sketch-reqfire-export/"; 732 | 733 | // Convert all symbols into exportable image 734 | var exportInfoList = layersToPNG(context, layersToExport); 735 | 736 | var flag; 737 | for (var i = 0; i < exportInfoList.length; i++) { 738 | for (var j = 0; j < organisedData.ifaces.length; j++) { 739 | var iName = organisedData.ifaces[j].name; 740 | var iPid = organisedData.ifaces[j].persistent_id; 741 | var X = exportInfoList[i].layerName; 742 | var id = X.split(" id:")[1]; 743 | 744 | if (id == organisedData.ifaces[j].persistent_id) { 745 | flag = 1; // symbol ID exists in object ifaces 746 | break; 747 | } else { 748 | flag = -1; 749 | } 750 | } 751 | 752 | if (flag == -1) { 753 | // symbol does not match ifaces 754 | exportInfoList.splice(i, 1); // remove item out of the export list 755 | print("failed"); 756 | } 757 | } 758 | 759 | //upload image to server 760 | if (getConfirmationFromUser(exportInfoList)) { 761 | var uploadStatus = updateLayer(exportInfoList, apiKey); 762 | 763 | if (uploadStatus == 1) { 764 | app.displayDialog_withTitle("Please refresh your Reqfire project to see the changes.", "Success — Your project has been synced successfully"); 765 | } else if (uploadStatus == 2) { 766 | app.displayDialog_withTitle("Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", "Error — Invalid API key provided"); 767 | } else if (uploadStatus == 3) { 768 | // TODO - delete this 769 | app.displayDialog_withTitle("Unmatched project key and api key!", "Unable to Export"); 770 | } else { 771 | app.displayDialog_withTitle("Error!", "Unable to Export"); 772 | } 773 | } else { 774 | log("User has cancelled Export All."); 775 | // app.displayDialogue_withTitle("Export cancelled"); 776 | } 777 | 778 | // return a list of symbols need to be exported 779 | function getLayers(context) { 780 | var loopSymbolMasters = context.document.documentData().allSymbols().objectEnumerator(); 781 | var symbolMaster; 782 | var layers = []; // a list to save exportable symbol 783 | 784 | while (symbolMaster = loopSymbolMasters.nextObject()) { 785 | // symbol children count equals one when there is no child in the symbol. 786 | // only symbols has children can be added into the list 787 | if (symbolMaster.children().count() != 1) { 788 | layers.push(symbolMaster); 789 | } 790 | } 791 | return layers; 792 | } 793 | 794 | // convert layer into image 795 | function layersToPNG(context, layers) { 796 | var exportInfoList = []; // prepare list with export layer info 797 | for (var i = 0; i < layers.length; i++) { 798 | var mslayer = layers[i]; 799 | var layerID = mslayer.objectID(); 800 | 801 | // store image into a temp path 802 | var filePath = exportPath + layerID + ".png"; 803 | var exportRequest = MSExportRequest.exportRequestsFromExportableLayer(mslayer).firstObject(); 804 | 805 | context.document.saveArtboardOrSlice_toFile(exportRequest, filePath); // save layer to file 806 | var exportInfo = { 807 | layerID: layerID, 808 | layerName: mslayer.name(), 809 | layer: mslayer, 810 | path: filePath 811 | }; 812 | 813 | exportInfoList.push(exportInfo); 814 | } 815 | return exportInfoList; 816 | } 817 | 818 | //upload symbols 819 | function updateLayer(list, apiKey) { 820 | var fullURl = "https://www.reqfire.com/app/component/sketchimport"; 821 | var task = NSTask.alloc().init(); 822 | var uploadStatus; 823 | task.setLaunchPath("/usr/bin/curl"); 824 | var args = NSMutableArray.alloc().init(); 825 | args.addObject("-v"); 826 | args.addObject("POST"); 827 | args.addObject("--header"); 828 | args.addObject("Content-Type: multipart/form-data"); 829 | 830 | for (var i = 0; i < list.length; i++) { 831 | args.addObject("-F"); 832 | args.addObject(list[i]["layerID"] + "=" + list[i]["layerName"]); 833 | args.addObject("-F"); 834 | args.addObject(list[i]["layerName"].split(" id:")[1] + "=@" + list[i]["path"]); 835 | } 836 | 837 | args.addObject("-F"); 838 | args.addObject("apikey=" + apiKey); 839 | args.addObject(fullURl); 840 | task.setArguments(args); 841 | var outputPipe = NSPipe.pipe(); 842 | task.setStandardOutput(outputPipe); 843 | task.launch(); 844 | var outputData = outputPipe.fileHandleForReading(); 845 | var data = outputData.readDataToEndOfFile(); 846 | var classNameOfOutput = NSStringFromClass(data["class"]()); 847 | 848 | if (classNameOfOutput != "_NSZeroData") { 849 | var res = NSJSONSerialization.JSONObjectWithData_options_error(data, NSJSONReadingMutableLeaves, nil); 850 | 851 | if (res != null) { 852 | if (res.error == nil) { 853 | uploadStatus = res.status; 854 | } 855 | } 856 | } else { 857 | uploadStatus = 0; 858 | } 859 | print(res); 860 | return uploadStatus; 861 | } 862 | 863 | function getConfirmationFromUser(exportInfoList) { 864 | var alert = NSAlert.alloc().init(); 865 | var plural = exportInfoList.length == 1 ? "" : "s"; 866 | 867 | alert.setMessageText("Confirming Project Sync"); 868 | alert.setInformativeText(exportInfoList.length + " symbol" + plural + " (containing content) selected to be synced with — " + projectName); 869 | alert.addButtonWithTitle("Ok"); 870 | alert.addButtonWithTitle("Cancel"); 871 | 872 | return alert.runModal() == NSAlertFirstButtonReturn; 873 | } 874 | 875 | function checkSymbols(layers) { 876 | var flag = 0; 877 | var layerList = []; // a list to save exportable symbol 878 | layers.forEach(function (layer) { 879 | // check the selected layer is symbol ot not 880 | if (layer instanceof MSSymbolMaster) { 881 | // symbol children count equals one when there is no child in the symbol. 882 | // only symbols has children can be added into the list 883 | if (layer.children().count() != 1) { 884 | layerList.push(layer); 885 | } 886 | } 887 | }); 888 | return layerList; 889 | } 890 | } 891 | 892 | module.exports = { 893 | exportInterfaces: exportInterfaces 894 | }; 895 | 896 | /***/ }), 897 | /* 5 */ 898 | /***/ (function(module, exports, __webpack_require__) { 899 | 900 | var IMPORT = __webpack_require__(1).IMPORT; 901 | var EXPORT_ALL = __webpack_require__(1).EXPORT_ALL; 902 | var EXPORT_SELECTED = __webpack_require__(1).EXPORT_SELECTED; 903 | 904 | function getInputFromUser(context, type) { 905 | var window = type === IMPORT ? createImportWindow(context) : createExportWindow(context, type); 906 | var alert = window[0]; 907 | var response = alert.runModal(); 908 | if (response == "1000") { 909 | //save user input to user defaults to enable auto filled text field 910 | // userDefaults.setObject_forKey(apiKey, "apiKey"); 911 | // userDefaults.synchronize(); 912 | return jsonTextField.stringValue(); 913 | } else { 914 | return false; 915 | } 916 | } 917 | 918 | function createImportWindow(context) { 919 | userDefaults = NSUserDefaults.alloc().initWithSuiteName("com.bohemiancoding.sketch.exportSelectedInterface"); 920 | 921 | var alert = COSAlertWindow["new"](); 922 | alert.setMessageText("Import Reqfire Project"); 923 | alert.addButtonWithTitle("Confirm"); 924 | alert.addButtonWithTitle("Cancel"); 925 | var viewWidth = 400; 926 | var viewHeight = 150; 927 | var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight)); 928 | alert.addAccessoryView(view); 929 | 930 | var linkLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 55, viewWidth - 100, 35)); 931 | linkLabel.setStringValue("Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch."); 932 | linkLabel.setSelectable(false); 933 | linkLabel.setEditable(false); 934 | linkLabel.setBezeled(false); 935 | linkLabel.setDrawsBackground(false); 936 | 937 | var jsonLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 85, viewWidth - 100, 20)); 938 | jsonLabel.setStringValue("Enter your API key"); 939 | jsonLabel.setSelectable(false); 940 | jsonLabel.setEditable(false); 941 | jsonLabel.setBezeled(false); 942 | jsonLabel.setDrawsBackground(false); 943 | 944 | view.addSubview(linkLabel); 945 | view.addSubview(jsonLabel); 946 | 947 | jsonTextField = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 105, 300, 20)); 948 | view.addSubview(jsonTextField); 949 | 950 | return [alert]; 951 | } 952 | 953 | function createExportWindow(context, type) { 954 | // Creates a user defaults object initialized with the defaults for the specified domain name 955 | userDefaults = NSUserDefaults.alloc().initWithSuiteName("com.bohemiancoding.sketch.exportAllInterface"); 956 | 957 | var alert = COSAlertWindow["new"](); 958 | if (type === EXPORT_ALL) { 959 | alert.setMessageText("Sync All Artboards"); 960 | } else if (type === EXPORT_SELECTED) { 961 | alert.setMessageText("Sync Selected Artboards"); 962 | } 963 | alert.addButtonWithTitle("Confirm"); 964 | alert.addButtonWithTitle("Cancel"); 965 | 966 | var viewWidth = 400; 967 | var viewHeight = 160; 968 | var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight)); 969 | alert.addAccessoryView(view); 970 | 971 | var alertLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 55, viewWidth - 100, 50)); 972 | var infoLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 95, viewWidth - 100, 35)); 973 | var jsonLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 120, viewWidth - 100, 20)); 974 | 975 | alertLabel.setStringValue("Note: Any symbols you create that are not sitting inside the symbol artboards generated by this plugin will not be synced up to your Reqfire project."); 976 | alertLabel.setSelectable(false); 977 | alertLabel.setEditable(false); 978 | alertLabel.setBezeled(false); 979 | alertLabel.setDrawsBackground(false); 980 | 981 | infoLabel.setStringValue("Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch."); 982 | infoLabel.setSelectable(false); 983 | infoLabel.setEditable(false); 984 | infoLabel.setBezeled(false); 985 | infoLabel.setDrawsBackground(false); 986 | 987 | jsonLabel.setStringValue("Enter your API key"); 988 | jsonLabel.setSelectable(false); 989 | jsonLabel.setEditable(false); 990 | jsonLabel.setBezeled(false); 991 | jsonLabel.setDrawsBackground(false); 992 | 993 | view.addSubview(alertLabel); 994 | view.addSubview(infoLabel); 995 | view.addSubview(jsonLabel); 996 | 997 | jsonTextField = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 140, 300, 20)); 998 | jsonTextField.setStringValue(""); 999 | view.addSubview(jsonTextField); 1000 | 1001 | return [alert]; 1002 | } 1003 | 1004 | module.exports = { 1005 | getInputFromUser: getInputFromUser, 1006 | createImportWindow: createImportWindow, 1007 | createExportWindow: createExportWindow 1008 | }; 1009 | 1010 | /***/ }) 1011 | /******/ ]); 1012 | if (key === 'default' && typeof exports === 'function') { 1013 | exports(context); 1014 | } else { 1015 | exports[key](context); 1016 | } 1017 | } 1018 | that['onRun'] = __skpm_run.bind(this, 'default') 1019 | -------------------------------------------------------------------------------- /sketch_user_flows.sketchplugin/Contents/Sketch/importReqfireProject.js: -------------------------------------------------------------------------------- 1 | var that = this; 2 | function __skpm_run (key, context) { 3 | that.context = context; 4 | 5 | var exports = 6 | /******/ (function(modules) { // webpackBootstrap 7 | /******/ // The module cache 8 | /******/ var installedModules = {}; 9 | /******/ 10 | /******/ // The require function 11 | /******/ function __webpack_require__(moduleId) { 12 | /******/ 13 | /******/ // Check if module is in cache 14 | /******/ if(installedModules[moduleId]) { 15 | /******/ return installedModules[moduleId].exports; 16 | /******/ } 17 | /******/ // Create a new module (and put it into the cache) 18 | /******/ var module = installedModules[moduleId] = { 19 | /******/ i: moduleId, 20 | /******/ l: false, 21 | /******/ exports: {} 22 | /******/ }; 23 | /******/ 24 | /******/ // Execute the module function 25 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 26 | /******/ 27 | /******/ // Flag the module as loaded 28 | /******/ module.l = true; 29 | /******/ 30 | /******/ // Return the exports of the module 31 | /******/ return module.exports; 32 | /******/ } 33 | /******/ 34 | /******/ 35 | /******/ // expose the modules object (__webpack_modules__) 36 | /******/ __webpack_require__.m = modules; 37 | /******/ 38 | /******/ // expose the module cache 39 | /******/ __webpack_require__.c = installedModules; 40 | /******/ 41 | /******/ // define getter function for harmony exports 42 | /******/ __webpack_require__.d = function(exports, name, getter) { 43 | /******/ if(!__webpack_require__.o(exports, name)) { 44 | /******/ Object.defineProperty(exports, name, { 45 | /******/ configurable: false, 46 | /******/ enumerable: true, 47 | /******/ get: getter 48 | /******/ }); 49 | /******/ } 50 | /******/ }; 51 | /******/ 52 | /******/ // getDefaultExport function for compatibility with non-harmony modules 53 | /******/ __webpack_require__.n = function(module) { 54 | /******/ var getter = module && module.__esModule ? 55 | /******/ function getDefault() { return module['default']; } : 56 | /******/ function getModuleExports() { return module; }; 57 | /******/ __webpack_require__.d(getter, 'a', getter); 58 | /******/ return getter; 59 | /******/ }; 60 | /******/ 61 | /******/ // Object.prototype.hasOwnProperty.call 62 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 63 | /******/ 64 | /******/ // __webpack_public_path__ 65 | /******/ __webpack_require__.p = ""; 66 | /******/ 67 | /******/ // Load entry module and return exports 68 | /******/ return __webpack_require__(__webpack_require__.s = 3); 69 | /******/ }) 70 | /************************************************************************/ 71 | /******/ ([ 72 | /* 0 */ 73 | /***/ (function(module, exports) { 74 | 75 | //text colors 76 | var subtitleColor = MSColor.colorWithRed_green_blue_alpha(0.8, 0.8, 0.8, 1); 77 | var groupColor = MSColor.colorWithRed_green_blue_alpha(0.6078431373, 0.6078431373, 0.6078431373, 1); 78 | var flowColor = MSColor.colorWithRed_green_blue_alpha(0.2588235294, 0.2588235294, 0.2588235294, 1); 79 | var versionColor = MSColor.colorWithRed_green_blue_alpha(0, 0, 0, 0.34); 80 | var hintColor = MSColor.colorWithRed_green_blue_alpha(0.8156862745, 0.007843137255, 0.1058823529, 1); 81 | 82 | module.exports = { 83 | subtitleColor: subtitleColor, 84 | groupColor: groupColor, 85 | flowColor: flowColor, 86 | versionColor: versionColor, 87 | hintColor: hintColor 88 | }; 89 | 90 | /***/ }), 91 | /* 1 */ 92 | /***/ (function(module, exports, __webpack_require__) { 93 | 94 | var insertionSort = __webpack_require__(4).insertionSort; 95 | 96 | // make request to get object data from json url 97 | function getJSONFromURL(url, key) { 98 | var task = NSTask.alloc().init(); 99 | task.setLaunchPath("/usr/bin/curl"); 100 | 101 | var args = NSMutableArray.alloc().init(); 102 | args.addObject("-v"); 103 | args.addObject("POST"); 104 | args.addObject("-F"); 105 | args.addObject("apikey=" + key); 106 | args.addObject(url); 107 | task.setArguments(args); 108 | 109 | var outputPipe = NSPipe.pipe(); 110 | task.setStandardOutput(outputPipe); 111 | task.launch(); 112 | 113 | var outputData = outputPipe.fileHandleForReading(); 114 | var data = outputData.readDataToEndOfFile(); 115 | var classNameOfOutput = NSStringFromClass(data["class"]()); 116 | 117 | if (classNameOfOutput != "_NSZeroData") { 118 | var res = NSJSONSerialization.JSONObjectWithData_options_error(data, NSJSONReadingMutableContainers, null); 119 | 120 | if (res != null) { 121 | if (res.status == 1) { 122 | //success 123 | //log(res.content); 124 | return res.content; 125 | } else { 126 | return null; 127 | } 128 | } 129 | } else { 130 | return null; 131 | } 132 | } 133 | 134 | //retrive image data from url 135 | function getImageFromURL(url, ingnoreCache) { 136 | var request = ingnoreCache ? NSURLRequest.requestWithURL_cachePolicy_timeoutInterval(NSURL.URLWithString(url), NSURLRequestReloadIgnoringLocalCacheData, 60) : NSURLRequest.requestWithURL(NSURL.URLWithString(url)); 137 | 138 | var responsePtr = MOPointer.alloc().init(); 139 | var errorPtr = MOPointer.alloc().init(); 140 | var data = NSURLConnection.sendSynchronousRequest_returningResponse_error(request, responsePtr, errorPtr); 141 | 142 | if (errorPtr.value() != null) { 143 | return null; 144 | } 145 | 146 | var response = responsePtr.value(); 147 | 148 | if (response.statusCode() != 200) { 149 | return null; 150 | } 151 | 152 | var mimeType = response.allHeaderFields()["Content-Type"]; 153 | 154 | if (!mimeType || !mimeType.hasPrefix("image/")) { 155 | return null; 156 | } 157 | 158 | return NSImage.alloc().initWithData(data); 159 | } 160 | 161 | function organiseData(object) { 162 | var flattenByKey = function flattenByKey(object, property) { 163 | return Object.keys(object[property]).map(function (key) { 164 | return object[property][key]; 165 | }); 166 | }; 167 | data = {}; 168 | data.ifaces = object.ifaces; 169 | data.groups = flattenByKey(object, "groups"); 170 | 171 | data.groups = insertionSort(data.groups); 172 | data.groups.forEach(function (group) { 173 | if (group.uc) { 174 | var tempUc = flattenByKey(group, "uc"); 175 | group.uc = insertionSort(tempUc); 176 | group.uc.forEach(function (uc) { 177 | uc.flow = flattenByKey(uc, "flow"); 178 | // we don't order flows by number (for now) 179 | uc.flow.forEach(function (flow) { 180 | if (flow.step) { 181 | var tempStep = flattenByKey(flow, "step"); 182 | flow.step = insertionSort(tempStep); 183 | flow.step.forEach(function (step) { 184 | if (step.object) { 185 | step.object = flattenByKey(step, "object"); 186 | step.object = step.object.find(function (obj) { 187 | return obj.object_type == "12"; 188 | }); 189 | } 190 | }); 191 | } 192 | }); 193 | }); 194 | } 195 | }); 196 | 197 | // log(data); 198 | return data; 199 | } 200 | 201 | function logRequest(key, message, isImport) { 202 | var task = NSTask.alloc().init(); 203 | task.setLaunchPath("/usr/bin/curl"); 204 | 205 | var typeNumber = isImport ? 0 : 1; 206 | 207 | var args = NSMutableArray.alloc().init(); 208 | args.addObject("-v"); 209 | args.addObject("POST"); 210 | args.addObject("-F"); 211 | args.addObject("message=" + message); 212 | args.addObject("https://www.reqfire.com/app/project/sketchevent/type/" + typeNumber + "/ext/" + key); 213 | task.setArguments(args); 214 | 215 | var outputPipe = NSPipe.pipe(); 216 | task.setStandardOutput(outputPipe); 217 | task.launch(); 218 | } 219 | 220 | module.exports = { 221 | getJSONFromURL: getJSONFromURL, 222 | getImageFromURL: getImageFromURL, 223 | organiseData: organiseData, 224 | logRequest: logRequest 225 | }; 226 | 227 | /***/ }), 228 | /* 2 */ 229 | /***/ (function(module, exports) { 230 | 231 | var IMPORT = "IMPORT"; 232 | var EXPORT_ALL = "EXPORT_ALL"; 233 | var EXPORT_SELECTED = "EXPORT_SELECTED"; 234 | 235 | module.exports = { 236 | IMPORT: IMPORT, 237 | EXPORT_ALL: EXPORT_ALL, 238 | EXPORT_SELECTED: EXPORT_SELECTED 239 | }; 240 | 241 | /***/ }), 242 | /* 3 */ 243 | /***/ (function(module, exports, __webpack_require__) { 244 | 245 | Object.defineProperty(exports, "__esModule", { 246 | value: true 247 | }); 248 | 249 | exports["default"] = function (context) { 250 | coscript.setShouldKeepAround(false); 251 | var sketch = context.api(); 252 | 253 | var app = NSApplication.sharedApplication(); 254 | var document = sketch.selectedDocument; 255 | var page = document.selectedPage; 256 | var pages = context.document.pages(); 257 | var imageUrl = "https://www.reqfire.com/app/images/"; 258 | var projectImportURL = "https://www.reqfire.com/app/project/sketchexport/"; 259 | 260 | // get file path 261 | var manifestPath = context.plugin.url().URLByAppendingPathComponent("Contents").URLByAppendingPathComponent("Sketch").URLByAppendingPathComponent("manifest.json").path(); 262 | // read the .json file content 263 | var manifest = NSJSONSerialization.JSONObjectWithData_options_error(NSData.dataWithContentsOfFile(manifestPath), 0, nil); 264 | var version = manifest.version; 265 | 266 | // default page name when open a new file 267 | var defaultPage = "Page 1"; 268 | 269 | // Get the user's project API key. Pass import 270 | var apiKey = getInputFromUser(context, IMPORT); 271 | 272 | if (!apiKey || apiKey.isEqualToString("")) { 273 | // TODO: prompt that they've cancelled. 274 | return null; 275 | } 276 | // Get JSON object 277 | var jsonResponse = getJSONFromURL(projectImportURL, apiKey); 278 | 279 | if (!jsonResponse) { 280 | app.displayDialog_withTitle("Please ensure you are using a valid API key.\nThis can be found within your project at Reqfire > Export > Sync to Sketch.", "Error — No API key was entered"); 281 | logRequest(apiKey, "Invalid API Key", true); 282 | return null; 283 | } 284 | 285 | if (!version.isEqualToString(jsonResponse.version)) { 286 | app.displayDialog_withTitle("You can download the latest version from https://github.com/reqfire/sketch-user-flows/", "Error — Plugin version is not up to date"); 287 | logRequest(apiKey, "Plugin out of date", true); 288 | log("wrong version"); 289 | return null; 290 | } 291 | 292 | // Convert json object 293 | var organisedData = organiseData(jsonResponse); 294 | // Get all symbols (interfaces) in current project 295 | var symbolList = getSymbols(context, pages); 296 | // Current version is the latest 297 | 298 | // Opened file has no symbol page or no symbol 299 | if (!symbolList) { 300 | // Create groups for ifaces 301 | for (var i = 0; i < organisedData.ifaces.length; i++) { 302 | var iName = organisedData.ifaces[i].name; 303 | var iPid = organisedData.ifaces[i].persistent_id; 304 | var iUrl = imageUrl + organisedData.ifaces[i].image; 305 | // create layers for all ifaces in the project 306 | createGroup(iName + " id:" + iPid, iUrl); 307 | } 308 | // get current page after create all the iface layers 309 | var currentPage = context.document.currentPage(); 310 | var number = currentPage.layers().length; 311 | var layer; 312 | // create symbol for each layer (group) 313 | for (var i = 0; i < number; i++) { 314 | layer = currentPage.layers()[i]; 315 | var layerArray = MSLayerArray.arrayWithLayers([layer]); 316 | var symbolName = currentPage.layers()[i].name(); 317 | // create symbol for each layer in the document and send symbols to "Symbol" Page 318 | var newSymbol = MSSymbolCreator.createSymbolFromLayers_withName_onSymbolsPage(layerArray, symbolName, true); 319 | } 320 | } else { 321 | // there are symbols exist already 322 | // create a temporary page to save the layers created for ifaces exist in reqfire project but not on current sketch file 323 | var tempPage = context.document.documentData().addBlankPage(); 324 | tempPage.setName("reqfire_temp"); 325 | var flag; 326 | 327 | // compare exist symbol's ID with ifaces persistent_id in json object 328 | for (var i = 0; i < organisedData.ifaces.length; i++) { 329 | for (var j = 0; j < symbolList.length; j++) { 330 | var X = symbolList[j].name(); 331 | var ID = X.split(" id:")[1]; 332 | 333 | if (organisedData.ifaces[i].persistent_id == ID) { 334 | flag = 1; // iface exist in sketch file 335 | 336 | break; 337 | } else { 338 | flag = -1; 339 | } 340 | } 341 | 342 | if (flag == 1) { 343 | // iface exist in sketch file 344 | // update the symbol's name only 345 | updateSymbol(organisedData.ifaces[i].persistent_id, organisedData.ifaces[i].name + " id:" + organisedData.ifaces[i].persistent_id); 346 | } else { 347 | // create new layers for ifaces do not exist in sketch file 348 | var iName = organisedData.ifaces[i].name; 349 | var iUrl = imageUrl + organisedData.ifaces[i].image; 350 | page = document.selectedPage; // update the value of current selected page (reqfire_temp) 351 | createGroup(iName + " id:" + organisedData.ifaces[i].persistent_id, iUrl); 352 | } 353 | } 354 | 355 | var number = tempPage.layers().length; 356 | var layer; 357 | 358 | // create symbol for each layer 359 | for (var i = 0; i < number; i++) { 360 | layer = tempPage.layers()[i]; 361 | var layerArray = MSLayerArray.arrayWithLayers([layer]); 362 | var symbolName = tempPage.layers()[i].name(); 363 | MSSymbolCreator.createSymbolFromLayers_withName_onSymbolsPage(layerArray, symbolName, true); 364 | } 365 | } 366 | removePage("reqfire_temp"); 367 | removePage("User Flows"); 368 | removePage("Welcome"); 369 | removePage(defaultPage); 370 | 371 | // create a new page to save user flows of the project 372 | var goalPage = context.document.documentData().addBlankPage(); 373 | goalPage.setName("User Flows"); 374 | organizeSymbol(organisedData); // clean the Symbol page by deleting blank shape 375 | createArtboard(goalPage, organisedData); // create texts and artboards to illustrate all the user flows 376 | 377 | createInfoPage(context, document, version); 378 | 379 | logRequest(apiKey, "Import successful", true); 380 | // complete 381 | return null; 382 | 383 | // create a rectangle shape with image fill 384 | function createGroup(name, url) { 385 | // var FillType = { Solid: 0, Gradient: 1, Pattern: 4, Noise: 5 }; 386 | // var PatternFillType = { Tile: 0, Fill: 1, Stretch: 2, Fit: 3 }; 387 | var groupShape = page.newShape({ 388 | frame: new sketch.Rectangle(200, 200, 1440, 1024), 389 | name: name 390 | }); 391 | 392 | // convert shape into a sketch object to enable setting its style 393 | var group = groupShape.sketchObject; 394 | //set shape fills 395 | // var fill = group.style().addStylePartOfType(0); 396 | var imgData = getImageFromURL(url); 397 | 398 | if (imgData) { 399 | var fill = group.style().fills().firstObject(); 400 | fill.setFillType(4); 401 | fill.setImage(MSImageData.alloc().initWithImage(imgData)); 402 | } else { 403 | log("No image loaded from server for interface"); 404 | // bad solution for now, append ! to namd id to check if empty. 405 | group.setName(name + "!"); 406 | } 407 | } 408 | 409 | //delete the shape with no fill image on symbol page 410 | function organizeSymbol(obj) { 411 | var symbolMasters = context.document.documentData().allSymbols(); // get all symbols in document 412 | 413 | // further poor implementations. need to find symbols that have been marked with a "!" and 414 | // remove the internals of that symbol 415 | symbolMasters.forEach(function (symbolMaster) { 416 | // search for the corresponding symbol 417 | var nameStr = String(symbolMaster.name().toString()); 418 | if (nameStr[nameStr.length - 1] == "!") { 419 | symbolMaster.children()[1].removeFromParent(); // remove the symbol's child element (a shape with no image fill) 420 | symbolMaster.setName(nameStr.substring(0, nameStr.length - 1)); 421 | } 422 | }); 423 | } 424 | 425 | // create artboard based on groups and use cases 426 | function createArtboard(page, object) { 427 | var setY = 0; 428 | 429 | for (var i = 0; i < object.groups.length; i++) { 430 | var groupName = object.groups[i].name; 431 | 432 | // Group Name Label 433 | var subtitle = MSTextLayer["new"](); 434 | subtitle.setName("Group Name"); 435 | subtitle.setStringValue("Group Name"); 436 | subtitle.setFontPostscriptName("Helvetica"); 437 | subtitle.setFontSize(24); 438 | subtitle.setTextAlignment(0); 439 | subtitle.setTextColor(subtitleColor); 440 | var tFrame = subtitle.frame(); 441 | tFrame.setX(0); 442 | tFrame.setY(setY); 443 | page.addLayer(subtitle); 444 | 445 | // Group Name Heading 446 | var text = MSTextLayer["new"](); 447 | text.setName(groupName); 448 | text.setStringValue(groupName); 449 | text.setFontPostscriptName("Helvetica-Bold"); 450 | text.setFontSize(64); 451 | text.setTextAlignment(0); 452 | text.setTextColor(groupColor); 453 | var tFrame = text.frame(); 454 | tFrame.setX(-3); 455 | tFrame.setY(setY + 29); 456 | page.addLayer(text); 457 | 458 | if (object.groups[i].uc) { 459 | for (var n = 0; n < object.groups[i].uc.length; n++) { 460 | var useCaseName = object.groups[i].uc[n].name; 461 | var flowNo = object.groups[i].uc[n].flow.length; 462 | for (var j = 0; j < flowNo; j++) { 463 | if (object.groups[i].uc[n].flow[j].step) { 464 | var interfaceNo = object.groups[i].uc[n].flow[j].step.length; 465 | 466 | // Flow Name Label 467 | 468 | // Flow Name Heading 469 | var flowHeading = MSTextLayer["new"](); 470 | flowHeading.setName(useCaseName); 471 | flowHeading.setStringValue(useCaseName); 472 | flowHeading.setFontPostscriptName("Helvetica-Bold"); 473 | flowHeading.setFontSize(36); 474 | flowHeading.setTextAlignment(0); 475 | flowHeading.setTextColor(flowColor); 476 | var tFrame = flowHeading.frame(); 477 | tFrame.setX(70); 478 | tFrame.setY(setY + 29 + 77 + 48); // setY + gap to group heading + group heading height + gap to flow heading 479 | page.addLayer(flowHeading); 480 | 481 | for (var k = 0; k < interfaceNo; k++) { 482 | if (object.groups[i].uc[n].flow[j].step[k].object) { 483 | var interfaceName = object.groups[i].uc[n].flow[j].step[k].object.name; 484 | var interfacePid = object.groups[i].uc[n].flow[j].step[k].object.persistent_id; 485 | 486 | var artboardName = interfaceName; 487 | var artboard = MSArtboardGroup["new"](); 488 | artboard.setName(String.fromCharCode("a".charCodeAt() + k) + ". " + artboardName); 489 | var frame = artboard.frame(); 490 | frame.setX(70 + 1540 * k); // offset + (artboard with + gap) * k 491 | frame.setY(setY + 29 + 77 + 48 + 43 + 50); // setY + gap to group heading + group heading height + gap to flow heading + flow heading height + gap to board 492 | frame.setWidth(1440); 493 | frame.setHeight(1024); 494 | var insertSymbol = symbolMasterWithName( 495 | // find the symbol by name that needs to create an instance 496 | context, interfaceName + " id:" + interfacePid); 497 | var symbolInstance = insertSymbol.newSymbolInstance(); // create an symbol instance 498 | page.addLayer(artboard); 499 | artboard.addLayer(symbolInstance); 500 | } else { 501 | var blankText = MSTextLayer["new"](); 502 | blankText.setName("Text"); 503 | blankText.setStringValue("No interface linked"); 504 | blankText.setFontPostscriptName("Helvetica-Bold"); 505 | blankText.setFontSize(64); 506 | blankText.setTextAlignment(1); 507 | var tFrame = blankText.frame(); 508 | tFrame.setX(70 + 1540 * k + 1440 / 2 - 573 / 2); 509 | tFrame.setY(setY + 29 + 77 + 48 + 43 + 50 + 1024 / 2 - 77 / 2); 510 | page.addLayer(blankText); 511 | } 512 | } 513 | } else { 514 | // user goal has no step 515 | var flowHeading = MSTextLayer["new"](); 516 | flowHeading.setName(useCaseName + " has no steps"); 517 | flowHeading.setStringValue(useCaseName + " has no steps"); 518 | flowHeading.setFontPostscriptName("Helvetica-Bold"); 519 | flowHeading.setFontSize(36); 520 | flowHeading.setTextAlignment(0); 521 | flowHeading.setTextColor(flowColor); 522 | var tFrame = flowHeading.frame(); 523 | tFrame.setX(70); 524 | tFrame.setY(setY + 29 + 77 + 48); // setY + gap to group heading + group heading height + gap to flow heading 525 | page.addLayer(flowHeading); 526 | } 527 | } 528 | setY = setY + 29 + 77 + 48 + 43 + 50 + 1024 + 100; 529 | } 530 | } else { 531 | // group has no use flows 532 | var message = MSTextLayer["new"](); 533 | message.setName(groupName + "has no Flows"); 534 | message.setStringValue(groupName + " has no Flows"); 535 | message.setFontPostscriptName("Helvetica-Bold"); 536 | message.setFontSize(36); 537 | message.setTextAlignment(0); 538 | message.setTextColor(flowColor); 539 | var tFrame = message.frame(); 540 | tFrame.setX(70); 541 | tFrame.setY(setY + 29 + 77 + 48); 542 | page.addLayer(message); 543 | 544 | setY = setY + 29 + 77 + 48 + 43 + 50 + 1024 + 100; 545 | } 546 | } 547 | } 548 | 549 | //return symbol by name 550 | function symbolMasterWithName(context, name) { 551 | var loopSymbolMasters = context.document.documentData().allSymbols().objectEnumerator(); 552 | var symbolMaster; 553 | 554 | while (symbolMaster = loopSymbolMasters.nextObject()) { 555 | // loop all the symbols 556 | if (symbolMaster.name().isEqualToString(name)) { 557 | return symbolMaster; 558 | } 559 | } 560 | } 561 | 562 | //update symbol 563 | function updateSymbol(id, name) { 564 | var loopSymbolMasters = context.document.documentData().allSymbols().objectEnumerator(); 565 | var symbolMaster; 566 | 567 | while (symbolMaster = loopSymbolMasters.nextObject()) { 568 | if (symbolMaster.name().split(" id:")[1] == id) { 569 | symbolMaster.setName(name); // only update symbol name 570 | } 571 | } 572 | } 573 | 574 | // remove unused page by name 575 | function removePage(pageName) { 576 | var pages = context.document.pages(); 577 | for (var i = 0; i < pages.count(); i++) { 578 | var page = pages[i]; 579 | var name = page.name(); 580 | if (name == pageName) { 581 | context.document.documentData().removePage(page); 582 | context.document.pageTreeLayoutDidChange(); //update the page tree 583 | } 584 | } 585 | } 586 | 587 | // get all the symbols on Symbols page 588 | function getSymbols(context, pages) { 589 | for (i = 0; i < pages.count(); i++) { 590 | if (pages[i].name().isEqualToString("Symbols")) { 591 | var layers = pages[i].layers(); 592 | return layers; 593 | } 594 | } 595 | } 596 | 597 | function sortNumbers(a, b) { 598 | var aNum = Number(a.number); 599 | var bNum = Number(b.number); 600 | if (aNum < bNum) { 601 | return -1; 602 | } 603 | if (aNum > bNum) { 604 | return 1; 605 | } 606 | return 0; 607 | } 608 | }; 609 | 610 | var getJSONFromURL = __webpack_require__(1).getJSONFromURL; 611 | var getImageFromURL = __webpack_require__(1).getImageFromURL; 612 | var organiseData = __webpack_require__(1).organiseData; 613 | var logRequest = __webpack_require__(1).logRequest; 614 | var createInfoPage = __webpack_require__(5).createInfoPage; 615 | var getInputFromUser = __webpack_require__(6).getInputFromUser; 616 | var IMPORT = __webpack_require__(2).IMPORT; 617 | 618 | //text colors 619 | var subtitleColor = __webpack_require__(0).subtitleColor; 620 | var groupColor = __webpack_require__(0).groupColor; 621 | var flowColor = __webpack_require__(0).flowColor; 622 | var versionColor = __webpack_require__(0).versionColor; 623 | var hintColor = __webpack_require__(0).hintColor; 624 | 625 | /***/ }), 626 | /* 4 */ 627 | /***/ (function(module, exports) { 628 | 629 | function insertionSort(items) { 630 | var len = items.length, 631 | value, 632 | i, 633 | j; 634 | 635 | for (i = 0; i < len; i++) { 636 | // store the current value because it may shift later 637 | value = items[i]; 638 | for (j = i - 1; j > -1 && Number(items[j].number) > Number(value.number); j--) { 639 | items[j + 1] = items[j]; 640 | } 641 | items[j + 1] = value; 642 | } 643 | return items; 644 | } 645 | 646 | module.exports = { 647 | insertionSort: insertionSort 648 | }; 649 | 650 | /***/ }), 651 | /* 5 */ 652 | /***/ (function(module, exports, __webpack_require__) { 653 | 654 | //create Info Page 655 | //text colors 656 | var subtitleColor = __webpack_require__(0).subtitleColor; 657 | var groupColor = __webpack_require__(0).groupColor; 658 | var flowColor = __webpack_require__(0).flowColor; 659 | var versionColor = __webpack_require__(0).versionColor; 660 | var hintColor = __webpack_require__(0).hintColor; 661 | 662 | var getImageFromURL = __webpack_require__(1).getImageFromURL; 663 | 664 | function createInfoPage(context, document, version) { 665 | var infoPage = context.document.documentData().addBlankPage().setName("Welcome"); 666 | // infoPage.setName("Welcome"); 667 | var page = document.selectedPage; 668 | 669 | var currentPage = context.document.currentPage(); 670 | 671 | var welcome_artboard = MSArtboardGroup["new"](); 672 | welcome_artboard.setName("Welcome"); 673 | var frame = welcome_artboard.frame(); 674 | frame.setX(0); 675 | frame.setY(0); 676 | frame.setWidth(3730); 677 | frame.setHeight(2060); 678 | currentPage.addLayer(welcome_artboard); 679 | 680 | // Reqfire 681 | var welcome_heading_1 = MSTextLayer["new"](); 682 | welcome_heading_1.setName("User Flows"); 683 | welcome_heading_1.setStringValue("User Flows"); 684 | welcome_heading_1.setFontPostscriptName("Helvetica-Bold"); 685 | welcome_heading_1.setFontSize(144); 686 | welcome_heading_1.setTextAlignment(0); 687 | welcome_heading_1.setTextColor(flowColor); 688 | var tFrame = welcome_heading_1.frame(); 689 | tFrame.setX(192); 690 | tFrame.setY(300); 691 | welcome_artboard.addLayer(welcome_heading_1); 692 | 693 | // Sketch Plugin 694 | var welcome_heading_2 = MSTextLayer["new"](); 695 | welcome_heading_2.setName("Sketch plugin"); 696 | welcome_heading_2.setStringValue("Sketch plugin"); 697 | welcome_heading_2.setFontPostscriptName("Helvetica"); 698 | welcome_heading_2.setFontSize(144); 699 | welcome_heading_2.setTextAlignment(0); 700 | welcome_heading_2.setTextColor(flowColor); 701 | var tFrame = welcome_heading_2.frame(); 702 | tFrame.setX(192); 703 | tFrame.setY(450); 704 | welcome_artboard.addLayer(welcome_heading_2); 705 | 706 | // version 707 | var versionLayer = MSTextLayer["new"](); 708 | versionLayer.setName("Version"); 709 | versionLayer.setStringValue(version); 710 | versionLayer.setFontPostscriptName("Helvetica Neue-Light"); 711 | versionLayer.setFontSize(36); 712 | versionLayer.setTextAlignment(0); 713 | versionLayer.setTextColor(versionColor); 714 | var tFrame = versionLayer.frame(); 715 | tFrame.setX(202); 716 | tFrame.setY(630); 717 | welcome_artboard.addLayer(versionLayer); 718 | 719 | // description_1 720 | var welcome_description_1 = MSTextLayer["new"](); 721 | welcome_description_1.setName("Description_1"); 722 | welcome_description_1.setStringValue("A Sketch plug-in for importing/exporting user flows and interfaces from Reqfire."); 723 | welcome_description_1.setFontPostscriptName("Helvetica"); 724 | welcome_description_1.setFontSize(36); 725 | welcome_description_1.setTextAlignment(0); 726 | welcome_description_1.setTextColor(flowColor); 727 | welcome_description_1.setTextBehaviour(1); // text is "fixed" 728 | var tFrame = welcome_description_1.frame(); 729 | tFrame.setX(200); 730 | tFrame.setY(738); 731 | tFrame.setHeight(86); 732 | tFrame.setWidth(1000); 733 | welcome_artboard.addLayer(welcome_description_1); 734 | 735 | // description_2 736 | var welcome_description_2 = MSTextLayer["new"](); 737 | welcome_description_2.setName("Description_2"); 738 | welcome_description_2.setStringValue("User Flows allows the importing of interfaces from Reqfire into a Sketch project, along with the defined Reqfire user flows."); 739 | welcome_description_2.setFontPostscriptName("Helvetica"); 740 | welcome_description_2.setFontSize(36); 741 | welcome_description_2.setTextAlignment(0); 742 | welcome_description_2.setTextColor(flowColor); 743 | welcome_description_2.setTextBehaviour(1); 744 | var tFrame = welcome_description_2.frame(); 745 | tFrame.setX(200); 746 | tFrame.setY(864); 747 | tFrame.setHeight(86); 748 | tFrame.setWidth(1000); 749 | welcome_artboard.addLayer(welcome_description_2); 750 | 751 | // description_3 752 | var welcome_description_3 = MSTextLayer["new"](); 753 | welcome_description_3.setName("Description_3"); 754 | welcome_description_3.setStringValue("Interfaces can be designed in Sketch and then exported back to Reqfire as .png files to be included in the Reqfire project."); 755 | welcome_description_3.setFontPostscriptName("Helvetica"); 756 | welcome_description_3.setFontSize(36); 757 | welcome_description_3.setTextAlignment(0); 758 | welcome_description_3.setTextColor(flowColor); 759 | welcome_description_3.setTextBehaviour(1); 760 | var tFrame = welcome_description_3.frame(); 761 | tFrame.setX(200); 762 | tFrame.setY(990); 763 | tFrame.setHeight(86); 764 | tFrame.setWidth(1000); 765 | welcome_artboard.addLayer(welcome_description_3); 766 | 767 | // description_4 768 | var welcome_description_4 = MSTextLayer["new"](); 769 | welcome_description_4.setName("Description_4"); 770 | welcome_description_4.setStringValue("Subsequent import operations to the same Sketch project will not overwrite existing work, but will add any newly created interfaces and flows."); 771 | welcome_description_4.setFontPostscriptName("Helvetica"); 772 | welcome_description_4.setFontSize(36); 773 | welcome_description_4.setTextAlignment(0); 774 | welcome_description_4.setTextColor(flowColor); 775 | welcome_description_4.setTextBehaviour(1); 776 | var tFrame = welcome_description_4.frame(); 777 | tFrame.setX(200); 778 | tFrame.setY(1116); 779 | tFrame.setHeight(129); 780 | tFrame.setWidth(1000); 781 | welcome_artboard.addLayer(welcome_description_4); 782 | 783 | // hint 784 | var hint = MSTextLayer["new"](); 785 | hint.setName("Hint"); 786 | hint.setStringValue("Create all your designs in the Symbols page\n\nNOTE: Do not change the names of the Symbols"); 787 | hint.setFontPostscriptName("Helvetica"); 788 | hint.setFontSize(48); 789 | hint.setTextAlignment(2); 790 | hint.setTextColor(hintColor); 791 | hint.setTextBehaviour(1); 792 | var tFrame = hint.frame(); 793 | tFrame.setX(1782); 794 | tFrame.setY(552); 795 | tFrame.setHeight(174); 796 | tFrame.setWidth(386); 797 | welcome_artboard.addLayer(hint); 798 | 799 | var FillType = { Solid: 0, Gradient: 1, Pattern: 4, Noise: 5 }; 800 | var PatternFillType = { Tile: 0, Fill: 1, Stretch: 2, Fit: 3 }; 801 | var introRect = MSShapeGroup.shapeWithRect({ 802 | origin: { x: 2172, y: 300 }, 803 | size: { width: 1366, height: 1460 } 804 | }); 805 | var fill = introRect.style().addStylePartOfType(0); 806 | var image = getImageFromURL("https://www.reqfire.com/images/furniture/welcome_images.png"); 807 | 808 | if (image) { 809 | fill.fillType = FillType.Pattern; 810 | fill.patternFillType = PatternFillType.Fit; 811 | fill.image = MSImageData.alloc().initWithImage(image); 812 | } else { 813 | print("Can't load image!"); 814 | } 815 | 816 | welcome_artboard.addLayer(introRect); 817 | symbolZoom(context, welcome_artboard); 818 | context.document.pageTreeLayoutDidChange(); //update the page tree 819 | } 820 | 821 | // zooming out current page 822 | function symbolZoom(context, group) { 823 | var targetRect = group.rect(); 824 | var padding = 0.025; 825 | targetRect.origin.x -= targetRect.size.width * padding; 826 | targetRect.origin.y -= targetRect.size.height * padding; 827 | targetRect.size.width *= 1 + padding * 2; 828 | targetRect.size.height *= 1 + padding * 2; 829 | var view = getCurrentView(context.document); 830 | view.zoomToFitRect(targetRect); 831 | } 832 | 833 | function getCurrentView(doc) { 834 | if (doc.currentView) { 835 | return doc.currentView(); 836 | } else if (doc.contentDrawView) { 837 | return doc.contentDrawView(); 838 | } 839 | 840 | log("ERROR: Can not get currentView"); 841 | return null; 842 | } 843 | 844 | module.exports = { 845 | createInfoPage: createInfoPage 846 | }; 847 | 848 | /***/ }), 849 | /* 6 */ 850 | /***/ (function(module, exports, __webpack_require__) { 851 | 852 | var IMPORT = __webpack_require__(2).IMPORT; 853 | var EXPORT_ALL = __webpack_require__(2).EXPORT_ALL; 854 | var EXPORT_SELECTED = __webpack_require__(2).EXPORT_SELECTED; 855 | 856 | function getInputFromUser(context, type) { 857 | var window = type === IMPORT ? createImportWindow(context) : createExportWindow(context, type); 858 | var alert = window[0]; 859 | var response = alert.runModal(); 860 | if (response == "1000") { 861 | //save user input to user defaults to enable auto filled text field 862 | // userDefaults.setObject_forKey(apiKey, "apiKey"); 863 | // userDefaults.synchronize(); 864 | return jsonTextField.stringValue(); 865 | } else { 866 | return false; 867 | } 868 | } 869 | 870 | function createImportWindow(context) { 871 | userDefaults = NSUserDefaults.alloc().initWithSuiteName("com.bohemiancoding.sketch.exportSelectedInterface"); 872 | 873 | var alert = COSAlertWindow["new"](); 874 | alert.setMessageText("Import Reqfire Project"); 875 | alert.addButtonWithTitle("Confirm"); 876 | alert.addButtonWithTitle("Cancel"); 877 | var viewWidth = 400; 878 | var viewHeight = 150; 879 | var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight)); 880 | alert.addAccessoryView(view); 881 | 882 | var linkLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 55, viewWidth - 100, 35)); 883 | linkLabel.setStringValue("Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch."); 884 | linkLabel.setSelectable(false); 885 | linkLabel.setEditable(false); 886 | linkLabel.setBezeled(false); 887 | linkLabel.setDrawsBackground(false); 888 | 889 | var jsonLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 85, viewWidth - 100, 20)); 890 | jsonLabel.setStringValue("Enter your API key"); 891 | jsonLabel.setSelectable(false); 892 | jsonLabel.setEditable(false); 893 | jsonLabel.setBezeled(false); 894 | jsonLabel.setDrawsBackground(false); 895 | 896 | view.addSubview(linkLabel); 897 | view.addSubview(jsonLabel); 898 | 899 | jsonTextField = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 105, 300, 20)); 900 | view.addSubview(jsonTextField); 901 | 902 | return [alert]; 903 | } 904 | 905 | function createExportWindow(context, type) { 906 | // Creates a user defaults object initialized with the defaults for the specified domain name 907 | userDefaults = NSUserDefaults.alloc().initWithSuiteName("com.bohemiancoding.sketch.exportAllInterface"); 908 | 909 | var alert = COSAlertWindow["new"](); 910 | if (type === EXPORT_ALL) { 911 | alert.setMessageText("Sync All Artboards"); 912 | } else if (type === EXPORT_SELECTED) { 913 | alert.setMessageText("Sync Selected Artboards"); 914 | } 915 | alert.addButtonWithTitle("Confirm"); 916 | alert.addButtonWithTitle("Cancel"); 917 | 918 | var viewWidth = 400; 919 | var viewHeight = 160; 920 | var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight)); 921 | alert.addAccessoryView(view); 922 | 923 | var alertLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 55, viewWidth - 100, 50)); 924 | var infoLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 95, viewWidth - 100, 35)); 925 | var jsonLabel = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 120, viewWidth - 100, 20)); 926 | 927 | alertLabel.setStringValue("Note: Any symbols you create that are not sitting inside the symbol artboards generated by this plugin will not be synced up to your Reqfire project."); 928 | alertLabel.setSelectable(false); 929 | alertLabel.setEditable(false); 930 | alertLabel.setBezeled(false); 931 | alertLabel.setDrawsBackground(false); 932 | 933 | infoLabel.setStringValue("Your Reqfire Project API key can be found within your project at Reqfire > Export > Sync to Sketch."); 934 | infoLabel.setSelectable(false); 935 | infoLabel.setEditable(false); 936 | infoLabel.setBezeled(false); 937 | infoLabel.setDrawsBackground(false); 938 | 939 | jsonLabel.setStringValue("Enter your API key"); 940 | jsonLabel.setSelectable(false); 941 | jsonLabel.setEditable(false); 942 | jsonLabel.setBezeled(false); 943 | jsonLabel.setDrawsBackground(false); 944 | 945 | view.addSubview(alertLabel); 946 | view.addSubview(infoLabel); 947 | view.addSubview(jsonLabel); 948 | 949 | jsonTextField = NSTextField.alloc().initWithFrame(NSMakeRect(0, viewHeight - 140, 300, 20)); 950 | jsonTextField.setStringValue(""); 951 | view.addSubview(jsonTextField); 952 | 953 | return [alert]; 954 | } 955 | 956 | module.exports = { 957 | getInputFromUser: getInputFromUser, 958 | createImportWindow: createImportWindow, 959 | createExportWindow: createExportWindow 960 | }; 961 | 962 | /***/ }) 963 | /******/ ]); 964 | if (key === 'default' && typeof exports === 'function') { 965 | exports(context); 966 | } else { 967 | exports[key](context); 968 | } 969 | } 970 | that['onRun'] = __skpm_run.bind(this, 'default') 971 | -------------------------------------------------------------------------------- /sketch_user_flows.sketchplugin/Contents/Sketch/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sketch User Flows", 3 | "version": "1.0.1", 4 | "author": "Reqfire", 5 | "compatibleVersion": 3, 6 | "bundleVersion": 1, 7 | "commands": [ 8 | { 9 | "name": "Import Reqfire project", 10 | "identifier": "createIFace-identifier", 11 | "handler": "onRun", 12 | "script": "importReqfireProject.js" 13 | }, 14 | { 15 | "name": "Sync all symbols to Reqfire", 16 | "identifier": "exportInterface-identifier", 17 | "script": "exportAllInterface.js" 18 | }, 19 | { 20 | "name": "Sync selected symbols to Reqfire", 21 | "identifier": "exportSelectedInterface-identifier", 22 | "script": "exportSelectedInterface.js" 23 | } 24 | ], 25 | "menu": { 26 | "title": "Sketch User Flows by Reqfire", 27 | "items": [ 28 | "createIFace-identifier", 29 | "exportInterface-identifier", 30 | "exportSelectedInterface-identifier" 31 | ] 32 | }, 33 | "disableCocoaScriptPreprocessor": true, 34 | "appcast": "https://raw.githubusercontent.com//master/.appcast.xml" 35 | } --------------------------------------------------------------------------------