├── .eslintignore ├── images ├── logo.png ├── mcfs.gif ├── manual.png ├── screenshot.png ├── mcfs_filterde.jpg ├── mcfs_runquery.jpg ├── logo.pxd │ ├── metadata.info │ ├── QuickLook │ │ ├── Icon.tiff │ │ └── Thumbnail.tiff │ └── data │ │ ├── originalImportedContentDocumentInfo │ │ ├── 6D902E01-B167-4DFE-91FB-549A74ECF5A0 │ │ └── E58A2659-3DD7-43F9-BD36-9DD3CB7367D4 ├── screenshot_video.gif ├── screenshot_snippets.png ├── screenshot_video_howto.gif ├── screenshot_hoversnippets.jpg ├── run.icon.svg └── filter.icon.svg ├── .gitignore ├── connection-manager ├── img │ ├── howto_1-3.jpg │ ├── howto_4-6.jpg │ └── howto_7-9.jpg ├── css │ └── app.css ├── index.html └── js │ ├── app.js │ ├── app-legacy.js │ ├── app.js.map │ └── app-legacy.js.map ├── src ├── connection-manager │ ├── babel.config.js │ ├── vue.config.js │ ├── public │ │ ├── img │ │ │ ├── howto_1-3.jpg │ │ │ ├── howto_4-6.jpg │ │ │ └── howto_7-9.jpg │ │ └── index.html │ ├── src │ │ ├── main.js │ │ ├── App.vue │ │ └── components │ │ │ └── ConnectionList.vue │ ├── gitignore │ ├── README.md │ └── package.json ├── libs │ ├── folderManagerUri.ts │ ├── asset.ts │ ├── httpUtils.ts │ ├── folderManager.ts │ ├── soapUtils.ts │ ├── folderController.ts │ ├── utils.ts │ ├── folderManagers │ │ ├── scripts.ts │ │ ├── sqlQueries.ts │ │ ├── contentBuilder.ts │ │ └── dataextensions.ts │ └── connectionController.ts ├── mcfsFileSystemProvider.ts └── extension.ts ├── tsconfig.json ├── .eslintrc ├── .vscodeignore ├── .vscode ├── tasks.json └── launch.json ├── tests └── example.amp ├── syntaxes ├── language-configuration.json └── ampscript.tmLanguage.json ├── LICENSE ├── CHANGELOG.md ├── webscraper └── function-convert.py ├── package.json ├── PROMO.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/mcfs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/mcfs.gif -------------------------------------------------------------------------------- /images/manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/manual.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/connection-manager/package-lock.json 2 | *.vsix 3 | .DS_Store 4 | out 5 | node_modules 6 | -------------------------------------------------------------------------------- /images/mcfs_filterde.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/mcfs_filterde.jpg -------------------------------------------------------------------------------- /images/mcfs_runquery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/mcfs_runquery.jpg -------------------------------------------------------------------------------- /images/logo.pxd/metadata.info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.pxd/metadata.info -------------------------------------------------------------------------------- /images/screenshot_video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/screenshot_video.gif -------------------------------------------------------------------------------- /images/screenshot_snippets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/screenshot_snippets.png -------------------------------------------------------------------------------- /images/screenshot_video_howto.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/screenshot_video_howto.gif -------------------------------------------------------------------------------- /connection-manager/img/howto_1-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/connection-manager/img/howto_1-3.jpg -------------------------------------------------------------------------------- /connection-manager/img/howto_4-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/connection-manager/img/howto_4-6.jpg -------------------------------------------------------------------------------- /connection-manager/img/howto_7-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/connection-manager/img/howto_7-9.jpg -------------------------------------------------------------------------------- /images/logo.pxd/QuickLook/Icon.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.pxd/QuickLook/Icon.tiff -------------------------------------------------------------------------------- /images/screenshot_hoversnippets.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/screenshot_hoversnippets.jpg -------------------------------------------------------------------------------- /src/connection-manager/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/connection-manager/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | outputDir: "./../../connection-manager", 3 | filenameHashing: false 4 | } -------------------------------------------------------------------------------- /images/logo.pxd/QuickLook/Thumbnail.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.pxd/QuickLook/Thumbnail.tiff -------------------------------------------------------------------------------- /src/connection-manager/public/img/howto_1-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/src/connection-manager/public/img/howto_1-3.jpg -------------------------------------------------------------------------------- /src/connection-manager/public/img/howto_4-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/src/connection-manager/public/img/howto_4-6.jpg -------------------------------------------------------------------------------- /src/connection-manager/public/img/howto_7-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/src/connection-manager/public/img/howto_7-9.jpg -------------------------------------------------------------------------------- /images/logo.pxd/data/originalImportedContentDocumentInfo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.pxd/data/originalImportedContentDocumentInfo -------------------------------------------------------------------------------- /images/logo.pxd/data/6D902E01-B167-4DFE-91FB-549A74ECF5A0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.pxd/data/6D902E01-B167-4DFE-91FB-549A74ECF5A0 -------------------------------------------------------------------------------- /images/logo.pxd/data/E58A2659-3DD7-43F9-BD36-9DD3CB7367D4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/HEAD/images/logo.pxd/data/E58A2659-3DD7-43F9-BD36-9DD3CB7367D4 -------------------------------------------------------------------------------- /src/connection-manager/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "module": "commonjs", 5 | "target": "es2018", 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "." 10 | }, 11 | "exclude": ["node_modules", ".vscode-test"] 12 | } -------------------------------------------------------------------------------- /src/connection-manager/gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | package-lock.json -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-explicit-any": 0 14 | } 15 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | *.gif 3 | screenshot_snippets.png 4 | screenshot.png 5 | screenshot_hoversnippets.jpg 6 | screenshot_snippets.png 7 | images/screenshot_snippets.png 8 | images/screenshot.png 9 | images/screenshot_hoversnippets.jpg 10 | images/screenshot_snippets.png 11 | images/*.gif 12 | images/*.pxd 13 | .vscode/** 14 | .vscode-test/** 15 | out/test/** 16 | test/** 17 | src/** 18 | **/*.map 19 | tsconfig.json -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": [ 10 | "$tsc-watch" 11 | ], 12 | "isBackground": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/connection-manager/README.md: -------------------------------------------------------------------------------- 1 | # webview 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /src/connection-manager/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcfs-connection-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.8.0", 11 | "vue": "^2.6.12" 12 | }, 13 | "devDependencies": { 14 | "@vue/cli-plugin-babel": "^5.0.6", 15 | "@vue/cli-service": "^5.0.6", 16 | "vue-template-compiler": "^2.6.12" 17 | }, 18 | "browserslist": [ 19 | "> 1%", 20 | "last 2 versions" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [{ 5 | "name": "Launch Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceRoot}" 11 | ], 12 | "stopOnEntry": false, 13 | "sourceMaps": true, 14 | "outFiles": [ 15 | "${workspaceRoot}/out/src/**/*.js" 16 | ], 17 | "preLaunchTask": "npm: watch" 18 | }] 19 | } -------------------------------------------------------------------------------- /tests/example.amp: -------------------------------------------------------------------------------- 1 | %%[ 2 | ContentBlockByKey("INIT") 3 | SET @Dataextension = "MasterData" 4 | SET @Data = LookupRows(@Dataextension, "SubscriberKey", _subscriberkey) 5 | 6 | IF RowCount(@Data) > 0 THEN 7 | SET @DataRow = Row(@Data, 1) 8 | 9 | SET @Image = Field(@DataRow, "Image") 10 | SET @Headline = Field(@DataRow, "Headline") 11 | SET @Text = Field(@DataRow, "Text") 12 | SET @CTA_URL = Field(@DataRow, "CTA_URL") 13 | ENDIF 14 | ]%% 15 | 16 |
17 | 18 |

%%=V(@Headline)=%%

19 |

%%=TreatAsContent(@Text)=%%

20 | INSTALL ME 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /connection-manager/css/app.css: -------------------------------------------------------------------------------- 1 | .vscode-dark input[data-v-e7f38064]{color:#ddd}.vscode-light input[data-v-e7f38064]{color:#444}.vscode-dark button[data-v-e7f38064]{background-color:#333;color:#ddd}.vscode-light button[data-v-e7f38064]{background-color:#ddd;color:#333}.connections[data-v-e7f38064]{width:100%}.connections td[data-v-e7f38064],.connections th[data-v-e7f38064]{text-align:left;border-bottom:1px solid #444}.connections td[data-v-e7f38064]{padding:5px 0}.connections th[data-v-e7f38064]{padding:5px 7px}.connections input[data-v-e7f38064]{display:block;width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;padding:10px 5px;text-overflow:ellipsis;border:0;border-left:2px solid transparent;outline:0;background:transparent}.connections input[data-v-e7f38064]:focus{border-left:2px solid blue}button[data-v-e7f38064]{margin-right:10px;border:1px solid #ddd;border-radius:3px;cursor:pointer}button.delete[data-v-e7f38064]{background-color:darkred;color:#d3d3d3!important}button[data-v-e7f38064]:disabled{cursor:not-allowed;color:#999} -------------------------------------------------------------------------------- /images/run.icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /syntaxes/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | // "lineComment": "//", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["{", "}"], 11 | ["%%[", "]%%"], 12 | ["%%=", "=%%"], 13 | //["[", "]"], 14 | ["(", ")"] 15 | ], 16 | // symbols that are auto closed when typing 17 | "autoClosingPairs": [ 18 | ["{", "}"], 19 | ["%%[", "]%%"], 20 | ["%%=", "=%%"], 21 | //["[", "]"], 22 | ["(", ")"], 23 | ["\"", "\""], 24 | ["'", "'"] 25 | ], 26 | // symbols that that can be used to surround a selection 27 | "surroundingPairs": [ 28 | ["{", "}"], 29 | ["%%[", "]%%"], 30 | ["%%=", "=%%"], 31 | //["[", "]"], 32 | ["(", ")"], 33 | ["\"", "\""], 34 | ["'", "'"] 35 | ] 36 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sergey Agadzhanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "AMPScript" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## [3.0.6] - 2022-11-15 7 | - Support for Shared Dataextensions (only when connected to ENT BU) 8 | 9 | ## [3.0.5] - 2022-09-28 10 | - Switched to native HTTP calls to support Corporate Proxies 11 | 12 | ## [3.0.4] - 2022-06-24 13 | - Edit Automation Studio Script Activities (special thanks go to @danielrubiorueda) 14 | - Fix AXIOS concurrency requests issue 15 | 16 | ## [3.0.2] - 2021-04-08 17 | - Clean Build 18 | 19 | ## [3.0.1] - 2021-03-23 20 | ### Added 21 | - MCFS: SQL Queries 22 | - MCFS: Dataextensions 23 | 24 | ## [2.0.2] - 2020-09-08 25 | ### Added 26 | - MCFS: fix incorrect path names on Windows 27 | 28 | ## [2.0.1] - 2020-08-30 29 | ### Added 30 | - MCFS functionality (ability to connect directly to MC) 31 | - Hover function snippets 32 | 33 | ## [1.4.0] - 2020-02-25 34 | ### Added 35 | - Code snippets for all functions and some language elements 36 | 37 | ## [1.3.0] - 2020-02-19 38 | ### Changed 39 | - Migration of the grammar to a JSON format 40 | - Revision of the grammar 41 | ### Added 42 | - Highlighting in HTML attributes 43 | 44 | ## [1.2.3] - 2020-01-29 45 | ### Added 46 | - Support for Ampscript in HTML comments 47 | - Highlighting for a few missig functions 48 | - Other small changes 49 | 50 | ## [1.1.0] - 2017-07-11 51 | ### Added 52 | - HTML support 53 | 54 | ## [1.0.0] - 2017-07-10 55 | ### Changed 56 | - String escape symbols 57 | 58 | ## [Unreleased] 59 | - Initial release -------------------------------------------------------------------------------- /images/filter.icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/connection-manager/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 76 | 77 | 79 | -------------------------------------------------------------------------------- /src/libs/folderManagerUri.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { FolderController } from './folderController'; 4 | 5 | export class FolderManagerUri { 6 | public readonly connectionId: string; 7 | public readonly name: string; 8 | public readonly globalPath: string; 9 | public readonly mountPath: string; 10 | public readonly localPath: string; 11 | public readonly mountFolderName: string; 12 | public readonly type: vscode.FileType; 13 | public readonly isAsset: boolean; 14 | private readonly uri: vscode.Uri; 15 | 16 | constructor(uri: vscode.Uri) { 17 | this.uri = uri; 18 | this.connectionId = uri.authority; 19 | this.name = path.basename(uri.path); 20 | this.type = FolderController.getInstance().hasFileExtension(path.extname(this.name)) ? vscode.FileType.File : vscode.FileType.Directory; 21 | this.isAsset = this.name.startsWith('Ω'); 22 | const basePath = `${uri.scheme}://${this.connectionId}/`; 23 | 24 | const chunks = uri.path.replace(/\\/g, '/').replace(/(^\/)|(\/$)/g, '').split('/'); 25 | this.mountFolderName = chunks.shift() || ''; 26 | this.localPath = chunks.join('/'); 27 | this.mountPath = this.mountFolderName !== '' ? basePath + this.mountFolderName + '/' : ''; 28 | this.globalPath = this.mountPath + this.localPath; 29 | } 30 | 31 | getChildPath(childDirectoryName: string) { 32 | return this.globalPath + (this.globalPath.endsWith('/') ? '' : '/') + childDirectoryName; 33 | } 34 | 35 | get parent(): FolderManagerUri | undefined { 36 | const chunks = this.localPath.split('/'); 37 | 38 | if (chunks.length > 0) { 39 | chunks.pop(); 40 | return new FolderManagerUri(vscode.Uri.parse(this.mountPath + chunks.join('/'))); 41 | } 42 | 43 | return undefined; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /webscraper/function-convert.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pyquery import PyQuery 3 | import re 4 | 5 | def readParams(html, syntax): 6 | result = {} 7 | result["description"] = "" 8 | result["body"] = [syntax] 9 | 10 | if not html: 11 | return result 12 | 13 | desc = "" 14 | 15 | pq = PyQuery(html) 16 | 17 | for row in pq('tbody tr').items(): 18 | r = PyQuery(row) 19 | index = r('td:first').text().strip() 20 | type = r('td:nth-child(2)').text().strip() 21 | required = r('td:nth-child(3)').text().lower().strip() 22 | description = r('td:nth-child(4)').text().strip() 23 | 24 | desc = desc + "" + index + (" " if not required else "*") + " [" + type.upper() + "] \n" + description + "\n\n" 25 | 26 | if index.isdigit(): 27 | syntax = re.sub("([\(\,]\s*)" + index + "(\s*[\,\)])", "\\1${" + index + ":" + type.upper() + "}\\2", syntax) 28 | else: 29 | print("Check " + syntax + " - " + index) 30 | 31 | result["description"] = desc.strip() 32 | result["body"] = [syntax] 33 | return result 34 | 35 | 36 | 37 | 38 | 39 | result = {} 40 | 41 | with open('functions.json') as json_file: 42 | data = json.load(json_file) 43 | for line in data: 44 | params = readParams(line['params'], line['syntax']) 45 | 46 | result[line['link'].lower()] = { 47 | "prefix": line['link'], 48 | "body": params['body'], 49 | "description": (line['link'] if not line['syntax'] else line['syntax']) 50 | + "\n\n" + line['desc'] 51 | + ("" if not params["description"] else "\n\n========== PARAMETERS ============\n\n" + params["description"]) 52 | + ("" if not line["example"] else "\n\n========== EXAMPLES ==============\n\n" + line["example"]) 53 | + "\n" 54 | } 55 | 56 | with open('function-snippets.json', 'w') as outfile: 57 | json.dump(result, outfile) -------------------------------------------------------------------------------- /src/libs/asset.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export type LazyContentDelegate = (() => Promise) | undefined; 4 | 5 | export class AssetFile { 6 | name: string; 7 | content: string; 8 | path: string; 9 | private getLazyContent: LazyContentDelegate; 10 | 11 | constructor(name: string, content: string, path: string, getLazyContent: LazyContentDelegate = undefined) { 12 | this.name = name; 13 | this.content = content; 14 | this.path = path; 15 | this.getLazyContent = getLazyContent; 16 | } 17 | 18 | write(data: Uint8Array): void { 19 | this.content = new TextDecoder("utf-8").decode(data); 20 | } 21 | 22 | async read(): Promise { 23 | if (this.content === '' && this.getLazyContent !== undefined) { 24 | this.content = await this.getLazyContent(); 25 | } 26 | 27 | return new TextEncoder().encode(this.content); 28 | } 29 | } 30 | 31 | export class Asset { 32 | name: string; 33 | directoryName: string; 34 | content: string; 35 | readonly connectionId: string; 36 | private _files: Array; 37 | 38 | constructor(name: string, directoryName: string, content: string, connectionId: string, files: Array | undefined) { 39 | this.name = name; 40 | this.directoryName = directoryName; 41 | this.content = content; 42 | this.connectionId = connectionId; 43 | this._files = new Array(); 44 | 45 | if (files !== undefined) { 46 | this._files.push(...files); 47 | } 48 | } 49 | 50 | get files(): Array { 51 | return [ 52 | new AssetFile('__raw.readonly.json', this.content, ''), 53 | ...this._files 54 | ]; 55 | } 56 | 57 | getFile(name: string): AssetFile { 58 | const f = this.files.find(f => f.name == name); 59 | 60 | if (f !== undefined) return f; 61 | 62 | throw new Error(`File ${name} not found`); 63 | } 64 | } -------------------------------------------------------------------------------- /src/libs/httpUtils.ts: -------------------------------------------------------------------------------- 1 | import { URL } from 'url'; 2 | import { Buffer } from 'buffer'; 3 | import * as https from 'https'; 4 | import { ClientRequest, IncomingMessage, RequestOptions } from 'http'; 5 | import * as zlib from 'zlib'; 6 | 7 | export class ApiRequestConfig { 8 | public method = ''; 9 | public url = ''; 10 | public data = ''; 11 | public params: any = null; 12 | public baseURL = ''; 13 | public headers: any = {}; 14 | 15 | public constructor(config: any){ 16 | this.method = config.method; 17 | this.url = config.url; 18 | this.data = config.data; 19 | this.params = config.params; 20 | this.baseURL = config.baseURL; 21 | this.headers = config.headers || {}; 22 | } 23 | 24 | public getURL(): URL { 25 | const url = new URL(this.url, this.baseURL); 26 | 27 | if(this.params){ 28 | const searchParams = new URLSearchParams(this.params); 29 | url.search = searchParams.toString(); 30 | } 31 | 32 | return url; 33 | } 34 | } 35 | 36 | export class HttpUtils { 37 | private static instance: HttpUtils | null = null; 38 | 39 | static getInstance(): HttpUtils { 40 | if (HttpUtils.instance === null) { 41 | HttpUtils.instance = new HttpUtils(); 42 | } 43 | 44 | return HttpUtils.instance; 45 | } 46 | 47 | makeRestApiCall(config: ApiRequestConfig): Promise{ 48 | config.headers['Content-Type'] = 'application/json'; 49 | return this.makeApiCall(config).then(data => JSON.parse(data)); 50 | } 51 | 52 | makeApiCall(config: ApiRequestConfig): Promise{ 53 | const options: RequestOptions = { 54 | method: config.method, 55 | headers: config.headers 56 | }; 57 | 58 | return new Promise((resolve: any, reject: any) => { 59 | const req: ClientRequest = https.request(config.getURL().toString(), options, (res: IncomingMessage) => { 60 | const chunks: Array = []; 61 | 62 | res.on('data', (data: Buffer) => { 63 | chunks.push(data); 64 | }); 65 | 66 | res.on('end', () => { 67 | const buffer = Buffer.concat(chunks); 68 | 69 | if(res.headers['content-encoding'] == 'gzip'){ 70 | zlib.gunzip(buffer, (err, output: Buffer) => { 71 | if(!err && res?.statusCode && res.statusCode >= 200 && res.statusCode < 300){ 72 | resolve(output.toString()); 73 | } 74 | else{ 75 | reject(err); 76 | } 77 | }); 78 | } 79 | else{ 80 | const output = buffer.toString(); 81 | if(res?.statusCode && res.statusCode >= 200 && res.statusCode < 300){ 82 | resolve(output); 83 | } 84 | else{ 85 | reject(output); 86 | } 87 | } 88 | }); 89 | }); 90 | 91 | req.on('error', (err) => { 92 | reject(err); 93 | }); 94 | 95 | if (config.data) { 96 | const data = typeof config.data === 'object' ? JSON.stringify(config.data) : config.data; 97 | req.write(data); 98 | } 99 | 100 | req.end(); 101 | }); 102 | } 103 | } -------------------------------------------------------------------------------- /src/mcfsFileSystemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { FolderController } from './libs/folderController'; 3 | import { FolderManagerUri } from './libs/folderManagerUri'; 4 | import { Utils } from './libs/utils'; 5 | 6 | export class MCFS implements vscode.FileSystemProvider { 7 | stat(uri: vscode.Uri): vscode.FileStat { 8 | const fmUri = new FolderManagerUri(uri); 9 | 10 | if (!FolderController.getInstance().hasManager(fmUri)) { 11 | throw vscode.FileSystemError.FileNotFound(); 12 | } 13 | 14 | return { 15 | type: fmUri.type, 16 | mtime: Date.now(), 17 | size: 0, 18 | ctime: 0, 19 | }; 20 | } 21 | 22 | async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> { 23 | const fmUri = new FolderManagerUri(uri); 24 | 25 | if (!FolderController.getInstance().hasManager(fmUri)) { 26 | throw vscode.FileSystemError.FileNotFound(); 27 | } 28 | 29 | const pSubdirectories = FolderController.getInstance().getSubdirectories(fmUri); 30 | const pAssets = FolderController.getInstance().getAssets(fmUri); 31 | const pFiles = FolderController.getInstance().getFiles(fmUri); 32 | 33 | try { 34 | const [subdirectories, assets, files] = await Promise.all([pSubdirectories, pAssets, pFiles]); 35 | 36 | return [ 37 | ...subdirectories.map<[string, vscode.FileType]>(d => [d, vscode.FileType.Directory]), 38 | ...assets.map<[string, vscode.FileType]>(a => [a.directoryName, vscode.FileType.Directory]), 39 | ...files.map<[string, vscode.FileType]>(f => [f.name, vscode.FileType.File]) 40 | ]; 41 | } 42 | catch (err) { 43 | Utils.getInstance().showErrorMessage(err); 44 | throw err; 45 | } 46 | 47 | } 48 | 49 | async readFile(uri: vscode.Uri): Promise { 50 | const fmUri = new FolderManagerUri(uri); 51 | 52 | if (!FolderController.getInstance().hasManager(fmUri)) { 53 | throw vscode.FileSystemError.FileNotFound(); 54 | } 55 | 56 | try { 57 | return FolderController.getInstance().readFile(fmUri); 58 | } 59 | catch (err) { 60 | Utils.getInstance().showErrorMessage(err); 61 | throw err; 62 | } 63 | } 64 | 65 | async writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): Promise { 66 | const fmUri = new FolderManagerUri(uri); 67 | 68 | if (!FolderController.getInstance().hasManager(fmUri)) { 69 | throw vscode.FileSystemError.FileNotFound(); 70 | } 71 | 72 | try { 73 | return FolderController.getInstance().writeFile(fmUri, content); 74 | } 75 | catch (err) { 76 | Utils.getInstance().logError(err); 77 | throw new Error(Utils.getInstance().getErrorMessage(err)); 78 | } 79 | 80 | } 81 | 82 | /* NOT IMPLEMENTED */ 83 | 84 | createDirectory(uri: vscode.Uri): void { 85 | throw new Error("CreateDirectory not implemented yet"); 86 | } 87 | 88 | rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void { 89 | throw new Error("Rename not implemented yet"); 90 | } 91 | 92 | delete(uri: vscode.Uri): void { 93 | throw new Error("Delete not implemented yet"); 94 | } 95 | 96 | private _emitter = new vscode.EventEmitter(); 97 | private _bufferedEvents: vscode.FileChangeEvent[] = []; 98 | private _fireSoonHandle?: NodeJS.Timer; 99 | 100 | readonly onDidChangeFile: vscode.Event = this._emitter.event; 101 | 102 | watch(_resource: vscode.Uri): vscode.Disposable { 103 | // ignore, fires for all changes... 104 | return new vscode.Disposable(() => { }); 105 | } 106 | 107 | private _fireSoon(...events: vscode.FileChangeEvent[]): void { 108 | this._bufferedEvents.push(...events); 109 | 110 | if (this._fireSoonHandle) { 111 | clearTimeout(this._fireSoonHandle); 112 | } 113 | 114 | this._fireSoonHandle = setTimeout(() => { 115 | this._emitter.fire(this._bufferedEvents); 116 | this._bufferedEvents.length = 0; 117 | }, 5); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/libs/folderManager.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Asset, AssetFile } from './asset'; 4 | import { FolderManagerUri } from './folderManagerUri'; 5 | 6 | export interface Directory { 7 | id: number; 8 | parentId: number | undefined; 9 | name: string; 10 | } 11 | 12 | export interface CustomAction { 13 | command: string; 14 | waitLabel: string; 15 | callback: (fmUri: FolderManagerUri, content: string) => Promise; 16 | } 17 | 18 | export abstract class FolderManager { 19 | readonly mountFolderName: string; 20 | protected assetsCache: Map; 21 | 22 | public customActions: Array; 23 | 24 | constructor() { 25 | this.mountFolderName = this.constructor.name; 26 | this.assetsCache = new Map(); 27 | this.customActions = []; 28 | } 29 | 30 | /** 31 | * Returns a list of subdirectories in the provided directory uri 32 | * @param directoryUri 33 | */ 34 | abstract getSubdirectories(directoryUri: FolderManagerUri): Promise>; 35 | 36 | /** 37 | * Returns a list of assets in the provided directory uri. Should save all retrived assets to the assetsCache property 38 | * @param directoryUri 39 | */ 40 | abstract getAssetsInDirectory(directoryUri: FolderManagerUri): Promise>; 41 | 42 | /** 43 | * Saves the content of the file back to the asset object 44 | * @param asset 45 | * @param file 46 | */ 47 | abstract setAssetFile(asset: Asset, file: AssetFile): Promise; 48 | 49 | /** 50 | * Returns an asset based on the provided uri 51 | * @param assetUri - uri of an asset to return 52 | * @param forceRefresh - if TRUE ignores cache and reloads an asset from the backend 53 | */ 54 | async getAsset(assetUri: FolderManagerUri, forceRefresh?: boolean): Promise { 55 | const directoryUri = assetUri.parent; 56 | 57 | let asset = this.assetsCache.get(assetUri.globalPath); 58 | 59 | // If assets have not been loaded yet, or a refresh was requested 60 | if (directoryUri !== undefined && (asset === undefined || forceRefresh === true)) { 61 | await this.getAssetsInDirectory(directoryUri); 62 | asset = this.assetsCache.get(assetUri.globalPath); 63 | } 64 | 65 | if (asset !== undefined) { 66 | return asset; 67 | } 68 | 69 | throw new Error(`Asset ${assetUri.globalPath} not found`); 70 | } 71 | 72 | /** 73 | * Save asset back to the backend 74 | * @param asset 75 | */ 76 | abstract saveAsset(asset: Asset): Promise; 77 | 78 | /** 79 | * Returns a list of files, extracted from an asset 80 | * @param assetUri 81 | */ 82 | async getAssetFiles(assetUri: FolderManagerUri): Promise> { 83 | const asset: Asset = await this.getAsset(assetUri, false); 84 | return asset?.files || []; 85 | } 86 | 87 | /** 88 | * Each asset is represented as a folder. This function returns the name of the asset directory based on the original asset name. 89 | * DIRECTORY NAMES SHOULD ALWAYS START WITH THE Ω SYMBOL FOLLOWED BY A SPACE. 90 | * This is required because VSCode sorts the content of 91 | * each directory by name. Ω at the beginning allows us to push all assets to the end of the list after all folders. 92 | * Ω at the beginning of the asset folder name is also used to distinguish between regular folder and asset folders 93 | * @param name - original name of the asset 94 | * @param assetData - raw asset data 95 | */ 96 | getAssetDirectoryName(name: string, assetData: any): string { 97 | return `Ω 🟦 ${name}.${this.constructor.name.toLowerCase()}`; 98 | } 99 | 100 | /** 101 | * Returns the original name of the asset based on asset directory name 102 | * @param directoryName 103 | */ 104 | getAssetNameByDirectoryName(directoryName: string): string | undefined { 105 | /*eslint no-control-regex: "off"*/ 106 | const match = directoryName.match(/^(?([^\x00-\x7F]|\s)+)(?.+)\.(?[^.]+)$/); 107 | return match?.groups?.name; 108 | } 109 | 110 | /** 111 | * Returns a list of file extension added by the Manager. File extensions are used by the FolderController to distinguish between folders and files. 112 | * Each element in the array should be lowercase and should start with the '.' symbol 113 | * Example: return ['.amp', '.sql', '.json'] 114 | */ 115 | getFileExtensions(): Array { 116 | return []; 117 | } 118 | } -------------------------------------------------------------------------------- /connection-manager/index.html: -------------------------------------------------------------------------------- 1 |

MCFS Connection manager

This extensions can connect Visual Studio Code directly to your Salesforce Marketing Cloud Account. This allows you to easily change content in MC without leaving the text editor and helps you to save time and avoid frequent copy-pasting. It also helps you to better control the content of your emails, content blocks and cloud pages by exposing additional content attributes that are not available in the UI of MC; it allows you to use "Super Content" in almost all supported content types.

  1. In your MC accout, create a new installed package and add a 'Server-to-Server' API integration Component
  2. Add the following permissions:
    • CHANNELS: Email (Read and Write)
    • CHANNELS: Web (Read, Write, Publish)
    • ASSETS: Saved Content (Read and Write)
    • AUTOMATION: Automations (Read, Write, Execute)
    • DATA: Data Extensions (Read, Write)
  3. Grant access to all required BUs
  4. Provide package details in the connection manager below, save it and connect
  5. You'll find the entire Content Builder library in your File Explorer tab
  6. To open Connection Manager next time press 'CMD+Shift+P' (Mac) or 'CTRL+Shift+P' (Windows) and start typing 'MCFS'. Then hit Enter

-------------------------------------------------------------------------------- /src/libs/soapUtils.ts: -------------------------------------------------------------------------------- 1 | import * as JSEP from "jsep"; 2 | 3 | export class SoapUtils { 4 | static getProp(obj: any, path: string, shouldReturnArray = false): any { 5 | let result: any = obj; 6 | 7 | const chunks: Array = path.split('.'); 8 | 9 | for (const c of chunks) { 10 | if (result?.[c] !== undefined) { 11 | result = result?.[c]; 12 | } 13 | else { 14 | result = result?.[0]?.[c]; 15 | } 16 | } 17 | 18 | if (Array.isArray(result) && result.length > 0 && !shouldReturnArray) { 19 | result = result?.[0]; 20 | } 21 | 22 | 23 | return result; 24 | } 25 | 26 | static getStrProp(obj: any, path: string): string { 27 | return SoapUtils.getProp(obj, path, false) || ""; 28 | } 29 | 30 | static getArrProp(obj: any, path: string): Array { 31 | return SoapUtils.getProp(obj, path, true) || []; 32 | } 33 | 34 | static createRetrieveBody(objectType: string, properties: Array, filter?: any): any { 35 | const result: any = { 36 | RetrieveRequestMsg: { 37 | $: { "xmlns": "http://exacttarget.com/wsdl/partnerAPI" }, 38 | RetrieveRequest: { 39 | ObjectType: objectType, 40 | Properties: properties 41 | } 42 | } 43 | }; 44 | 45 | if (filter !== undefined) { 46 | result.RetrieveRequestMsg.RetrieveRequest["Filter"] = SoapUtils.prepareSoapFilter(filter); 47 | } 48 | 49 | return result; 50 | } 51 | 52 | static createUpdateBody(objectType: string, objects: Array): any { 53 | const result: any = { 54 | UpdateRequest: { 55 | $: { "xmlns": "http://exacttarget.com/wsdl/partnerAPI" }, 56 | Options: { 57 | SaveOptions: { 58 | SaveOption: { 59 | PropertyName: "*", 60 | SaveAction: "UpdateAdd" 61 | } 62 | } 63 | }, 64 | Objects: [] 65 | } 66 | }; 67 | 68 | objects.forEach((o: any) => { 69 | o.$ = { "xsi:type": objectType }; 70 | result.UpdateRequest.Objects.push(o); 71 | }); 72 | 73 | return result; 74 | } 75 | 76 | static prepareSoapFilter(filter: any): any { 77 | // Simple filter 78 | if (filter?.SimpleOperator !== undefined) { 79 | filter["$"] = { "xsi:type": "SimpleFilterPart" }; 80 | return filter; 81 | } 82 | 83 | // Complex filter 84 | if (filter?.LogicalOperator !== undefined) { 85 | filter["$"] = { "xsi:type": "ComplexFilterPart" }; 86 | filter["LeftOperand"] = SoapUtils.prepareSoapFilter(filter["LeftOperand"]); 87 | filter["RightOperand"] = SoapUtils.prepareSoapFilter(filter["RightOperand"]); 88 | return filter; 89 | } 90 | 91 | return undefined; 92 | } 93 | } 94 | 95 | export class SoapFilterExpression { 96 | expression: JSEP.Expression; 97 | 98 | constructor(expressionString: string) { 99 | JSEP.addBinaryOp("and", 2); 100 | JSEP.addBinaryOp("AND", 2); 101 | JSEP.addBinaryOp("or", 1); 102 | JSEP.addBinaryOp("OR", 1); 103 | JSEP.addBinaryOp("=", 6); 104 | JSEP.addBinaryOp("<>", 6); 105 | JSEP.addBinaryOp("like", 6); 106 | JSEP.addBinaryOp("LIKE", 6); 107 | 108 | this.expression = JSEP(expressionString); 109 | } 110 | 111 | get filter(): any { 112 | return this.getFilter(this.expression); 113 | } 114 | 115 | getFilter(expression: any): any { 116 | if (expression?.operator === undefined) { 117 | return undefined; 118 | } 119 | 120 | if (this.getSimpleOperator(expression.operator) !== undefined) { 121 | return { 122 | Property: this.getFilterSide(expression.left), 123 | SimpleOperator: this.getSimpleOperator(expression.operator), 124 | Value: this.getFilterSide(expression.right) 125 | }; 126 | } 127 | 128 | return { 129 | LeftOperand: this.getFilter(expression.left), 130 | RightOperand: this.getFilter(expression.right), 131 | LogicalOperator: this.getLogicalOperator(expression.operator) 132 | }; 133 | } 134 | 135 | 136 | getFilterSide(side: any): any { 137 | let name = ""; 138 | 139 | switch (side.type) { 140 | case "Literal": 141 | return side.value; 142 | case "Identifier": 143 | return side.name; 144 | case "ArrayExpression": 145 | side.elements.forEach((e: any) => { name += e.name + " "; }); 146 | return name.trim(); 147 | } 148 | 149 | return side.value || ""; 150 | } 151 | 152 | getLogicalOperator(operator: string): string { 153 | switch (operator.toLowerCase()) { 154 | case "&&": 155 | case "and": 156 | return "AND"; 157 | case "or": 158 | case "||": 159 | return "OR"; 160 | } 161 | 162 | return operator.toUpperCase(); 163 | } 164 | 165 | getSimpleOperator(operator: string): string | undefined { 166 | switch (operator.toLowerCase()) { 167 | case "==": 168 | case "=": 169 | return "equals"; 170 | case "!=": 171 | case "<>": 172 | return "notEquals"; 173 | case ">": 174 | return "greaterThan"; 175 | case "<": 176 | return "lessThan"; 177 | case "like": 178 | return "like"; 179 | } 180 | 181 | return undefined; 182 | } 183 | } -------------------------------------------------------------------------------- /src/connection-manager/src/components/ConnectionList.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 137 | 138 | 139 | 208 | -------------------------------------------------------------------------------- /src/libs/folderController.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CustomAction, FolderManager } from './folderManager'; 4 | import { FolderManagerUri } from './folderManagerUri'; 5 | import { Asset, AssetFile } from './asset'; 6 | import { ContentBuilderFolderManager, AssetSubtype, ContentBuilderStandardTypes } from './folderManagers/contentBuilder'; 7 | import { SqlQueriesFolderManager } from './folderManagers/sqlQueries'; 8 | import { ScriptsFolderManager } from './folderManagers/scripts'; 9 | import { DataextensionFolderManager } from './folderManagers/dataextensions'; 10 | import { Utils } from './utils'; 11 | 12 | export class FolderController { 13 | private static instance: FolderController | null = null; 14 | private managers: Map; 15 | private fileExensions: Set; 16 | public customActions: Array = []; 17 | 18 | constructor() { 19 | this.managers = new Map(); 20 | this.fileExensions = new Set(); 21 | 22 | this.addManager(new ContentBuilderFolderManager("Content Builder", false, ContentBuilderStandardTypes, false)); 23 | this.addManager(new ContentBuilderFolderManager("Content Builder: Shared Content", true, ContentBuilderStandardTypes, false)); 24 | this.addManager(new ContentBuilderFolderManager("Cloud Pages", false, [AssetSubtype.WEBPAGE], true)); 25 | this.addManager(new SqlQueriesFolderManager()); 26 | this.addManager(new ScriptsFolderManager()); 27 | this.addManager(new DataextensionFolderManager("Dataextensions", false, "dataextension_default", "dataextension")); 28 | this.addManager(new DataextensionFolderManager("Dataextensions: Shared", false, "shared_dataextension_default", "shared_dataextension")); 29 | } 30 | 31 | static getInstance(): FolderController { 32 | if (FolderController.instance === null) { 33 | FolderController.instance = new FolderController(); 34 | } 35 | 36 | return FolderController.instance; 37 | } 38 | 39 | private get rootFolders(): Array { 40 | return Array.from(this.managers.keys()); 41 | } 42 | 43 | addManager(manager: FolderManager): void { 44 | this.managers.set(manager.mountFolderName, manager); 45 | 46 | manager.getFileExtensions().forEach(ext => { 47 | this.fileExensions.add(ext.toLowerCase()); 48 | }); 49 | 50 | this.customActions.push(...manager.customActions); 51 | } 52 | 53 | hasManager(uri: FolderManagerUri): boolean { 54 | return uri.mountPath === '' || this.managers.get(uri.mountFolderName) !== undefined; 55 | } 56 | 57 | getManager(mountFolderName: string): FolderManager | undefined { 58 | return this.managers.get(mountFolderName); 59 | } 60 | 61 | hasFileExtension(extension: string): boolean { 62 | return this.fileExensions.has(extension.toLowerCase()); 63 | } 64 | 65 | 66 | async getSubdirectories(uri: FolderManagerUri): Promise> { 67 | if (uri.mountPath === '') { 68 | return this.rootFolders; 69 | } 70 | 71 | if (uri.isAsset) { 72 | return []; 73 | } 74 | 75 | const telementryEvent = "manager-" + uri.mountFolderName.toLowerCase().replace(/\s/g, ''); 76 | Utils.getInstance().sendTelemetryEvent(telementryEvent, true); 77 | 78 | const manager = this.managers.get(uri.mountFolderName); 79 | 80 | return manager === undefined ? [] : manager.getSubdirectories(uri); 81 | } 82 | 83 | async getAssets(uri: FolderManagerUri): Promise> { 84 | if (uri.isAsset) { 85 | return []; 86 | } 87 | 88 | const manager = this.managers.get(uri.mountFolderName); 89 | 90 | return manager === undefined ? [] : manager.getAssetsInDirectory(uri); 91 | } 92 | 93 | async getFiles(uri: FolderManagerUri): Promise> { 94 | if (!uri.isAsset) return []; 95 | 96 | const manager = this.managers.get(uri.mountFolderName); 97 | 98 | return manager === undefined ? [] : manager.getAssetFiles(uri); 99 | } 100 | 101 | async readFile(fileUri: FolderManagerUri): Promise { 102 | const assetUri = fileUri.parent; 103 | const manager = this.managers.get(fileUri.mountFolderName); 104 | 105 | if (assetUri == undefined || !assetUri.isAsset || manager == undefined) { 106 | throw new Error(`Can't read file ${fileUri.globalPath}`); 107 | } 108 | 109 | const asset = await manager.getAsset(assetUri); 110 | const file = asset.getFile(fileUri.name); 111 | 112 | return file.read(); 113 | } 114 | 115 | async writeFile(uri: FolderManagerUri, data: Uint8Array): Promise { 116 | const parent = uri.parent; 117 | const manager = this.managers.get(uri.mountFolderName); 118 | 119 | if (parent == undefined || !parent.isAsset || manager == undefined) { 120 | throw new Error(`Can't read file ${uri.name}`); 121 | } 122 | 123 | if (uri.name.includes('.readonly.')) { 124 | throw new Error(`You can't change a readonly file ${uri.name}`); 125 | } 126 | 127 | const asset = await manager.getAsset(parent); 128 | const file = asset.getFile(uri.name); 129 | 130 | file.write(data); 131 | 132 | await manager.setAssetFile(asset, file); 133 | await manager.saveAsset(asset); 134 | } 135 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ampscript", 3 | "displayName": "MCFS [AMPScript]", 4 | "description": "Connect VSCode directly to your Marketing Cloud Account, enable syntax highlighting for AMPScript, get built-in AMPScript documentation and much more", 5 | "version": "3.0.6", 6 | "publisher": "sergey-agadzhanov", 7 | "engines": { 8 | "vscode": "^1.54.0" 9 | }, 10 | "categories": [ 11 | "Programming Languages" 12 | ], 13 | "keywords": [ 14 | "ampscript", 15 | "amp", 16 | "salesforce", 17 | "marketing cloud", 18 | "salesforce marketing cloud", 19 | "exacttarget", 20 | "sfmc", 21 | "mc", 22 | "content builder", 23 | "mcfs" 24 | ], 25 | "main": "./out/src/extension", 26 | "scripts": { 27 | "vscode:prepublish": "npm run compile; npm run connectionmanager", 28 | "connectionmanager": "cd ./src/connection-manager && npm run build", 29 | "compile": "tsc -p ./", 30 | "watch": "tsc -watch -p ./", 31 | "lint": "eslint . --ext .ts --fix" 32 | }, 33 | "activationEvents": [ 34 | "onCommand:mcfs.open", 35 | "onFileSystem:mcfs", 36 | "onLanguage:AMPscript" 37 | ], 38 | "contributes": { 39 | "languages": [ 40 | { 41 | "id": "AMPscript", 42 | "aliases": [ 43 | "AMPscript", 44 | "ampscript" 45 | ], 46 | "extensions": [ 47 | ".amp", 48 | ".ampscript" 49 | ], 50 | "configuration": "./syntaxes/language-configuration.json" 51 | } 52 | ], 53 | "grammars": [ 54 | { 55 | "language": "AMPscript", 56 | "scopeName": "source.amp", 57 | "path": "./syntaxes/ampscript.tmLanguage.json" 58 | } 59 | ], 60 | "snippets": [ 61 | { 62 | "language": "AMPscript", 63 | "path": "./syntaxes/snippets.json" 64 | } 65 | ], 66 | "commands": [ 67 | { 68 | "command": "mcfs.open", 69 | "title": "Connection Manager", 70 | "category": "MCFS" 71 | }, 72 | { 73 | "command": "mcfs.dataextension.filter", 74 | "title": "Filter a dataextension", 75 | "category": "MCFS", 76 | "icon": { 77 | "light": "images/filter.icon.svg", 78 | "dark": "images/filter.icon.svg" 79 | } 80 | }, 81 | { 82 | "command": "mcfs.query.run", 83 | "title": "Run SQL Query", 84 | "category": "MCFS", 85 | "icon": { 86 | "light": "images/run.icon.svg", 87 | "dark": "images/run.icon.svg" 88 | } 89 | } 90 | ], 91 | "menus": { 92 | "commandPalette": [ 93 | { 94 | "command": "mcfs.dataextension.filter", 95 | "when": "resourceExtname == .csv && resourceScheme == mcfs" 96 | }, 97 | { 98 | "command": "mcfs.query.run", 99 | "when": "resourceExtname == .sql && resourceScheme == mcfs" 100 | } 101 | ], 102 | "editor/title": [ 103 | { 104 | "when": "resourceExtname == .csv && resourceScheme == mcfs", 105 | "command": "mcfs.dataextension.filter", 106 | "group": "navigation" 107 | }, 108 | { 109 | "when": "resourceExtname == .sql && resourceScheme == mcfs", 110 | "command": "mcfs.query.run", 111 | "group": "navigation" 112 | } 113 | ] 114 | }, 115 | "viewsWelcome": [ 116 | { 117 | "view": "explorer", 118 | "contents": "Open MCFS connection manager and connect directly to your Salesforce Marketing Cloud account \n[Connect to SFMC](command:mcfs.open)", 119 | "when": "" 120 | } 121 | ], 122 | "configuration": { 123 | "title": "MCFS Configuration", 124 | "properties": { 125 | "mcfs.connections": { 126 | "title": "A list of MCFS connections", 127 | "description": "Use the Settings UI to edit the list of connectons. Run command 'MCFS: Connect to account'", 128 | "markdownDescription": "Use the Settings UI to edit configurations. Run command 'MCFS: Connect to account'", 129 | "type": "array", 130 | "items": "object", 131 | "default": [] 132 | }, 133 | "mcfs.notifications": { 134 | "title": "Notification paramters", 135 | "description": "Use the Settings UI to edit the list of connectons. Run command 'MCFS: Connect to account'", 136 | "markdownDescription": "Use the Settings UI to edit configurations. Run command 'MCFS: Connect to account'", 137 | "type": "object", 138 | "default": { 139 | "hasOpenedConnectionManager": false, 140 | "hasConnectedToMC": false, 141 | "dontShowConnectionManagerAlert": false, 142 | "hasShownChangelog": false 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | "__metadata": { 149 | "id": "47cde916-d321-4431-996b-009c1166cecf", 150 | "publisherId": "887b5b26-7dcf-4f99-9675-b1935ffcdc24", 151 | "publisherDisplayName": "Agadzhanov Sergey" 152 | }, 153 | "repository": { 154 | "type": "git", 155 | "url": "https://github.com/Bizcuit/vscode-ampscript.git" 156 | }, 157 | "icon": "images/logo.png", 158 | "devDependencies": { 159 | "@types/node": "^14.14.35", 160 | "@types/papaparse": "^5.2.5", 161 | "@types/vscode": "^1.54.0", 162 | "@types/xml2js": "^0.4.8", 163 | "@typescript-eslint/eslint-plugin": "^4.19.0", 164 | "@typescript-eslint/parser": "^4.19.0", 165 | "eslint": "^7.22.0", 166 | "typescript": "^4.2.3" 167 | }, 168 | "dependencies": { 169 | "axios": "^0.27.2", 170 | "jsep": "^0.4.0", 171 | "papaparse": "^5.3.0", 172 | "vscode-extension-telemetry": "^0.1.7", 173 | "xml2js": "^0.4.23" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/connection-manager/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 31 | 42 | 43 | 44 | 48 | 49 |

MCFS Connection manager

50 | 51 |
52 |

This extensions can connect Visual Studio Code directly to your Salesforce Marketing Cloud Account. This 53 | allows you to easily change content in MC without leaving the text editor and helps you to 54 | save time and avoid frequent copy-pasting. It also helps you to better control the content of your 55 | emails, content blocks and cloud pages by exposing additional content attributes that are not available in 56 | the UI of MC; it allows you to use "Super Content" in almost all supported content types.

57 | 58 |
    59 |
  1. In your MC accout, create a new installed package and add a 'Server-to-Server' API integration Component 60 |
  2. 61 |
  3. Add the following permissions: 62 |
      63 |
    • CHANNELS: Email (Read and Write)
    • 64 |
    • CHANNELS: Web (Read, Write, Publish)
    • 65 |
    • ASSETS: Saved Content (Read and Write)
    • 66 |
    • AUTOMATION: Automations (Read, Write, Execute)
    • 67 |
    • DATA: Data Extensions (Read, Write)
    • 68 |
    69 |
  4. 70 |
  5. Grant access to all required BUs
  6. 71 |
  7. Provide package details in the connection manager below, save it and connect
  8. 72 |
  9. You'll find the entire Content Builder library in your File Explorer tab
  10. 73 |
  11. To open Connection Manager next time press 'CMD+Shift+P' (Mac) or 'CTRL+Shift+P' (Windows) and start 74 | typing 'MCFS'. Then hit Enter
  12. 75 |
76 | 77 |

78 | 79 |

80 | 81 | 154 | 155 |
156 | 157 | 158 |
159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /connection-manager/js/app.js: -------------------------------------------------------------------------------- 1 | (function(){"use strict";var t={762:function(t,n,e){var o=e(144),i=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("div",{attrs:{id:"app"}},[e("ConnectionList",{attrs:{connections:t.connections},on:{connect:function(n){return t.connect(n)},save:function(n){return t.save(n)}}}),e("Help")],1)},c=[],s=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("div",{staticClass:"hello"},[e("h2",[t._v("Connections list")]),e("div",{staticStyle:{"margin-bottom":"10px"}},[e("button",{on:{click:function(n){return t.add()}}},[t._v("NEW CONNECTION")]),e("button",{on:{click:function(n){return t.save()}}},[t._v("SAVE CHANGES")])]),e("table",{staticClass:"connections",attrs:{cellpadding:"0",cellspacing:"0",border:"0"}},[t._m(0),e("tbody",t._l(this.localConnections,(function(n,o){return e("tr",{key:o},[e("td",[e("button",{ref:"btn_connect",refInFor:!0,on:{click:function(e){return t.connect(n,o)}}},[t._v("CONNECT")])]),e("td",[e("input",{directives:[{name:"model",rawName:"v-model",value:n.name,expression:"c.name"}],attrs:{type:"text",placeholder:"connection name"},domProps:{value:n.name},on:{change:function(n){t.hasChanges=!0},input:function(e){e.target.composing||t.$set(n,"name",e.target.value)}}})]),e("td",[e("input",{directives:[{name:"model",rawName:"v-model",value:n.account_id,expression:"c.account_id"}],attrs:{type:"text",placeholder:"business unit id"},domProps:{value:n.account_id},on:{change:function(n){t.hasChanges=!0},input:function(e){e.target.composing||t.$set(n,"account_id",e.target.value)}}})]),e("td",[e("input",{directives:[{name:"model",rawName:"v-model",value:n.authBaseUri,expression:"c.authBaseUri"}],attrs:{type:"text",placeholder:"api auth base uri"},domProps:{value:n.authBaseUri},on:{change:function(n){t.hasChanges=!0},input:function(e){e.target.composing||t.$set(n,"authBaseUri",e.target.value)}}})]),e("td",[e("input",{directives:[{name:"model",rawName:"v-model",value:n.client_id,expression:"c.client_id"}],attrs:{type:"password",placeholder:"client id"},domProps:{value:n.client_id},on:{change:function(n){t.hasChanges=!0},input:function(e){e.target.composing||t.$set(n,"client_id",e.target.value)}}})]),e("td",[e("input",{directives:[{name:"model",rawName:"v-model",value:n.client_secret,expression:"c.client_secret"}],attrs:{type:"password",placeholder:"client secret"},domProps:{value:n.client_secret},on:{change:function(n){t.hasChanges=!0},input:function(e){e.target.composing||t.$set(n,"client_secret",e.target.value)}}})]),e("td",[e("button",{staticClass:"delete",on:{click:function(n){return t.remove(o)}}},[t._v("✕")])])])})),0)])])},a=[function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("thead",[e("tr",[e("th",{attrs:{width:"100"}}),e("th",{attrs:{width:"100"}},[t._v("Label")]),e("th",{attrs:{width:"100"}},[t._v("MID")]),e("th",[t._v("Auth Base Uri")]),e("th",{attrs:{width:"200"}},[t._v("Client ID")]),e("th",{attrs:{width:"200"}},[t._v("Client Secret")]),e("th",{attrs:{width:"50§"}})])])}],r={name:"connectionList",props:{connections:Array},data:function(){return{localConnections:[],hasChanges:!1}},methods:{add:function(){this.hasChanges=!0,this.localConnections.push({name:"my connection "+(this.localConnections.length+1),account_id:"",authBaseUri:"",client_id:"",client_secret:""})},remove:function(t){this.localConnections.splice(t,1)},connect:function(t,n){this.$refs.btn_connect&&this.$refs.btn_connect.length>n&&(this.$refs.btn_connect[n].setAttribute("disabled","true"),this.$refs.btn_connect[n].innerHTML="CONNECTED"),this.hasChanges&&this.save(),setTimeout((()=>{this.$emit("connect",t)}),100)},save:function(){this.hasChanges=!1,this.$emit("save",this.localConnections)}},watch:{connections:function(t){this.localConnections=t.slice(0)}},components:{}},u=r,l=e(1),d=(0,l.Z)(u,s,a,!1,null,"e7f38064",null),h=d.exports,f={name:"app",data:function(){return{vscode:null,connections:[]}},methods:{save:function(t){this.connections=t,this.vscode.postMessage({action:"UPDATE",content:t})},connect:function(t){this.vscode.postMessage({action:"CONNECT",content:t})},onMessageReceived:function(t){switch(t.data.action){case"SET_CONFIGS":this.connections=t.data.content;break;default:break}}},mounted:function(){"function"===typeof acquireVsCodeApi?this.vscode=acquireVsCodeApi():this.vscode={postMessage:t=>{console.log("postMessage",t)}},this.vscode.postMessage({action:"SEND_CONFIGS"}),window.addEventListener("message",(t=>{this.onMessageReceived(t)}))},components:{ConnectionList:h}},p=f,v=(0,l.Z)(p,i,c,!1,null,null,null),m=v.exports;o.Z.config.productionTip=!1,new o.Z({render:t=>t(m)}).$mount("#app")}},n={};function e(o){var i=n[o];if(void 0!==i)return i.exports;var c=n[o]={exports:{}};return t[o](c,c.exports,e),c.exports}e.m=t,function(){var t=[];e.O=function(n,o,i,c){if(!o){var s=1/0;for(l=0;l=c)&&Object.keys(e.O).every((function(t){return e.O[t](o[r])}))?o.splice(r--,1):(a=!1,c0&&t[l-1][2]>c;l--)t[l]=t[l-1];t[l]=[o,i,c]}}(),function(){e.d=function(t,n){for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})}}(),function(){e.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"===typeof window)return window}}()}(),function(){e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)}}(),function(){var t={143:0};e.O.j=function(n){return 0===t[n]};var n=function(n,o){var i,c,s=o[0],a=o[1],r=o[2],u=0;if(s.some((function(n){return 0!==t[n]}))){for(i in a)e.o(a,i)&&(e.m[i]=a[i]);if(r)var l=r(e)}for(n&&n(o);un&&(this.$refs.btn_connect[n].setAttribute("disabled","true"),this.$refs.btn_connect[n].innerHTML="CONNECTED"),this.hasChanges&&this.save(),setTimeout((function(){e.$emit("connect",t)}),100)},save:function(){this.hasChanges=!1,this.$emit("save",this.localConnections)}},watch:{connections:function(t){this.localConnections=t.slice(0)}},components:{}}),u=r,l=e(1001),d=(0,l.Z)(u,s,a,!1,null,"e7f38064",null),h=d.exports,f={name:"app",data:function(){return{vscode:null,connections:[]}},methods:{save:function(t){this.connections=t,this.vscode.postMessage({action:"UPDATE",content:t})},connect:function(t){this.vscode.postMessage({action:"CONNECT",content:t})},onMessageReceived:function(t){switch(t.data.action){case"SET_CONFIGS":this.connections=t.data.content;break;default:break}}},mounted:function(){var t=this;"function"===typeof acquireVsCodeApi?this.vscode=acquireVsCodeApi():this.vscode={postMessage:function(t){console.log("postMessage",t)}},this.vscode.postMessage({action:"SEND_CONFIGS"}),window.addEventListener("message",(function(n){t.onMessageReceived(n)}))},components:{ConnectionList:h}},p=f,v=(0,l.Z)(p,i,c,!1,null,null,null),m=v.exports;o.Z.config.productionTip=!1,new o.Z({render:function(t){return t(m)}}).$mount("#app")}},n={};function e(o){var i=n[o];if(void 0!==i)return i.exports;var c=n[o]={exports:{}};return t[o](c,c.exports,e),c.exports}e.m=t,function(){var t=[];e.O=function(n,o,i,c){if(!o){var s=1/0;for(l=0;l=c)&&Object.keys(e.O).every((function(t){return e.O[t](o[r])}))?o.splice(r--,1):(a=!1,c0&&t[l-1][2]>c;l--)t[l]=t[l-1];t[l]=[o,i,c]}}(),function(){e.d=function(t,n){for(var o in n)e.o(n,o)&&!e.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:n[o]})}}(),function(){e.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"===typeof window)return window}}()}(),function(){e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)}}(),function(){var t={143:0};e.O.j=function(n){return 0===t[n]};var n=function(n,o){var i,c,s=o[0],a=o[1],r=o[2],u=0;if(s.some((function(n){return 0!==t[n]}))){for(i in a)e.o(a,i)&&(e.m[i]=a[i]);if(r)var l=r(e)}for(n&&n(o);u = []; 14 | 15 | public static readonly extensionId = "sergey-agadzhanov.AMPscript"; 16 | public static get extensionVersion(): string { 17 | return vscode.extensions.getExtension(Utils.extensionId)?.packageJSON?.version || ""; 18 | } 19 | 20 | 21 | static getInstance(): Utils { 22 | if (Utils.instance === null) { 23 | Utils.instance = new Utils(); 24 | } 25 | 26 | return Utils.instance; 27 | } 28 | 29 | constructor() { 30 | this.channel = vscode.window.createOutputChannel('MCFS'); 31 | this.telemetry = new TelemetryReporter( 32 | "mcfs", 33 | Utils.extensionVersion, 34 | Buffer.from("OTc1M2Y5OTAtOTY0Yy00M2Q2LWFiYTEtYjZiMmQyZmVlZDNi", "base64").toString("utf-8") 35 | ); 36 | } 37 | 38 | sendTelemetryEvent(event: string, deduplicate = false, isError = false): void { 39 | try{ 40 | if (deduplicate) { 41 | if (this.telementryEventLog.includes(event)) return; 42 | else this.telementryEventLog.push(event); 43 | } 44 | 45 | if (isError) this.telemetry.sendTelemetryErrorEvent(event); 46 | else this.telemetry.sendTelemetryEvent(event); 47 | } 48 | catch(err: any){ 49 | this.logError(err) 50 | } 51 | } 52 | 53 | showInformationMessage(message: string): void { 54 | this.log(message) 55 | vscode.window.showInformationMessage(message); 56 | } 57 | 58 | showErrorMessage(err: any, isModal = false): void { 59 | const message = this.getErrorMessage(err); 60 | this.logError(err); 61 | vscode.window.showErrorMessage(message, { 62 | modal: isModal 63 | } as vscode.MessageOptions); 64 | } 65 | 66 | getErrorMessage(err: any): string { 67 | let message = ''; 68 | 69 | if (typeof err === 'string') { 70 | message = err; 71 | } 72 | 73 | message += err?.message ? err?.message + '. ' : ''; 74 | message += err?.details ? err?.details + '. ' : ''; 75 | message += err?.data ? err.data : ''; 76 | 77 | return message; 78 | } 79 | 80 | log(message: string): void { 81 | this.channel.appendLine(`${new Date().toISOString()} => ${message}`); 82 | } 83 | 84 | logError(err: any): void { 85 | const message: string = this.getErrorMessage(err); 86 | 87 | this.log('ERROR ********************************************'); 88 | this.log(message); 89 | this.log(JSON.stringify(err, null, 2)); 90 | this.log('**************************************************'); 91 | 92 | } 93 | 94 | readJSON(path: string): Promise { 95 | return new Promise((resolve, reject) => { 96 | fs.readFile(require.resolve(path), (err, data) => { 97 | if (err) { 98 | reject(err) 99 | } 100 | else { 101 | resolve(JSON.parse(data.toString('utf-8'))); 102 | } 103 | }) 104 | }); 105 | } 106 | 107 | getConfig(section: string): any { 108 | const config = vscode.workspace.getConfiguration('mcfs'); 109 | return config?.get(section); 110 | } 111 | 112 | setConfig(section: string, value: any): void { 113 | const config = vscode.workspace.getConfiguration('mcfs'); 114 | 115 | const updateInterval = setInterval(_ => { 116 | if (this.isConfigUpdated) { 117 | this.isConfigUpdated = false; 118 | config?.update(section, value, true).then(_ => { 119 | this.isConfigUpdated = true; 120 | clearInterval(updateInterval); 121 | }); 122 | } 123 | }, 100); 124 | } 125 | 126 | setConfigField(section: string, field: string, value: any): void { 127 | const data = this.getConfig(section); 128 | data[field] = value; 129 | this.setConfig(section, data); 130 | } 131 | 132 | delay(time: number): any { 133 | return new Promise((resolve) => { 134 | setTimeout(() => resolve(time), time); 135 | }); 136 | } 137 | } 138 | 139 | 140 | 141 | export class WebPanelMessage { 142 | public action = ''; 143 | public content: any = null; 144 | } 145 | 146 | export class WebPanel { 147 | private panel: vscode.WebviewPanel | undefined; 148 | private id: string; 149 | private title: string; 150 | 151 | public onMessageReceived: (message: any) => void; 152 | 153 | constructor(id: string, title: string) { 154 | this.id = id; 155 | this.title = title; 156 | this.onMessageReceived = () => null; 157 | } 158 | 159 | public open(webviewPath: string): void { 160 | const webviewPathUri = vscode.Uri.file(webviewPath); 161 | const indexPath = path.join(webviewPathUri.fsPath, 'index.html'); 162 | 163 | this.panel = vscode.window.createWebviewPanel( 164 | this.id, 165 | this.title, 166 | vscode.ViewColumn.One, 167 | { 168 | enableScripts: true, 169 | localResourceRoots: [webviewPathUri] 170 | } 171 | ); 172 | 173 | this.panel?.webview.onDidReceiveMessage((message: any) => { 174 | this.onMessageReceived(message); 175 | }); 176 | 177 | let content = ''; 178 | 179 | fs.readFile(indexPath, (err, data) => { 180 | if (err) { 181 | Utils.getInstance().logError(err); 182 | content = `Error: unable to open confirguration manager using path ${indexPath}. ${err.toString()}`; 183 | return; 184 | } 185 | else { 186 | content = data 187 | .toString() 188 | .replace(/\/(css|js|assets|img)\//g, `${this.panel?.webview.asWebviewUri(webviewPathUri)}/$1/`); 189 | } 190 | 191 | if (this.panel) { 192 | this.panel.webview.html = content; 193 | } 194 | }); 195 | } 196 | 197 | public close(): void { 198 | this.panel?.dispose(); 199 | } 200 | 201 | public postMessage(message: WebPanelMessage): void { 202 | this.panel?.webview.postMessage(message); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/libs/folderManagers/scripts.ts: -------------------------------------------------------------------------------- 1 | import { Asset, AssetFile } from '../asset'; 2 | import { FolderManagerUri } from '../folderManagerUri'; 3 | import { FolderManager, Directory } from '../folderManager'; 4 | import { ConnectionController } from '../connectionController'; 5 | import { Utils } from '../utils'; 6 | import { ApiRequestConfig } from '../httpUtils'; 7 | 8 | 9 | export class ScriptsFolderManager extends FolderManager { 10 | readonly mountFolderName: string = "Scripts"; 11 | private directoriesCache: Map>>; 12 | 13 | constructor() { 14 | super(); 15 | this.directoriesCache = new Map>>(); 16 | } 17 | 18 | /* Interface implementation */ 19 | 20 | async getAssetsInDirectory(directoryUri: FolderManagerUri): Promise { 21 | const directoryId: number = await this.getDirectoryId(directoryUri); 22 | 23 | const hasTokenScopes = await ConnectionController.getInstance().hasTokenRequiredScopes( 24 | directoryUri.connectionId, 25 | ['automations_execute', 'automations_read', 'automations_write'] 26 | ); 27 | 28 | if (!hasTokenScopes) { 29 | Utils.getInstance().sendTelemetryEvent("error.manager-automations.missing_api_scope", true, true); 30 | throw new Error('Additional permissions are required for this function: AUTOMATION: Automations (Read, Write, Execute). Please update your installed package and restart VSCode'); 31 | } 32 | 33 | const config = new ApiRequestConfig({ 34 | method: 'get', 35 | url: `/automation/v1/scripts/category/${directoryId}`, 36 | params: { 37 | '$page': 1, 38 | '$pageSize': 100, 39 | 'retrievalType': 1 40 | } 41 | }); 42 | 43 | const data: any = await ConnectionController.getInstance().restRequest(directoryUri.connectionId, config); 44 | 45 | const assets: Array = new Array(); 46 | 47 | (data.items as Array).forEach(a => { 48 | const asset: Asset = new Asset( 49 | a.name || '???', 50 | //AssetSubtype.SQL, 51 | this.getAssetDirectoryName(a.name, a), 52 | JSON.stringify(a, null, 2), 53 | directoryUri.connectionId, 54 | this.extractFiles(a) 55 | ); 56 | //this.assetsCache.set(directoryUri.getChildPath(asset.fsName), asset); 57 | this.assetsCache.set(directoryUri.getChildPath(asset.directoryName), asset); 58 | assets.push(asset); 59 | }); 60 | 61 | return assets; 62 | } 63 | 64 | async getSubdirectories(directoryUri: FolderManagerUri): Promise { 65 | const directoryId: number = await this.getDirectoryId(directoryUri); 66 | 67 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(directoryUri, directoryId); 68 | return subdirectories.map(d => d.name); 69 | } 70 | 71 | async saveAsset(asset: Asset): Promise { 72 | const assetData: any = JSON.parse(asset.content); 73 | 74 | const config = new ApiRequestConfig({ 75 | method: 'patch', 76 | url: `/automation/v1/scripts/${assetData.ssjsActivityId}`, 77 | data: assetData 78 | }); 79 | 80 | await ConnectionController.getInstance().restRequest(asset.connectionId, config); 81 | } 82 | 83 | async setAssetFile(asset: Asset, file: AssetFile): Promise { 84 | const assetData: any = JSON.parse(asset.content); 85 | assetData[file.path] = file.content; 86 | asset.content = JSON.stringify(assetData, null, 2); 87 | } 88 | 89 | getAssetDirectoryName(name: string, assetData: any): string { 90 | return `Ω 🟪 ${name}.script`; 91 | } 92 | 93 | getFileExtensions(): Array { 94 | return ['.js', '.json']; 95 | } 96 | 97 | private extractFiles(assetData: any): Array { 98 | const result: Array = []; 99 | 100 | if (assetData?.script !== undefined) { 101 | result.push(new AssetFile( 102 | "script.js", 103 | assetData?.script, 104 | "script" 105 | )); 106 | } 107 | 108 | return result; 109 | } 110 | 111 | private async getAllDirectories(connectionId: string): Promise> { 112 | const cached = this.directoriesCache.get(connectionId); 113 | 114 | if (cached !== undefined) { 115 | return cached; 116 | } 117 | 118 | const config = new ApiRequestConfig({ 119 | method: 'get', 120 | url: '/automation/v1/folders/', 121 | params: { 122 | '$pagesize': '200', 123 | '$filter': `categorytype eq ssjsactivity` 124 | } 125 | }); 126 | 127 | const pDirectories = ConnectionController.getInstance() 128 | .restRequest(connectionId, config) 129 | .then(response => { 130 | const directories = new Array(); 131 | 132 | response?.items?.forEach((e: any) => { 133 | directories.push({ 134 | id: e.categoryId, 135 | parentId: e.parentId, 136 | name: e.name 137 | } as Directory); 138 | }); 139 | 140 | return directories; 141 | }); 142 | 143 | this.directoriesCache.set(connectionId, pDirectories); 144 | 145 | return pDirectories; 146 | } 147 | 148 | private async getSubdirectoriesByDirectoryId(uri: FolderManagerUri, directoryId: number): Promise> { 149 | const allDirectories = await this.getAllDirectories(uri.connectionId); 150 | 151 | return allDirectories.filter(d => d.parentId === directoryId); 152 | } 153 | 154 | private async getDirectoryId(uri: FolderManagerUri): Promise { 155 | const allDirectories = await this.getAllDirectories(uri.connectionId); 156 | 157 | if (uri.localPath === '') { 158 | const root: Directory | undefined = allDirectories.find(d => d.parentId === 0); 159 | if (root !== undefined) { 160 | return root.id; 161 | } 162 | } 163 | 164 | if (uri.parent !== undefined) { 165 | const parentDirectoryId = await this.getDirectoryId(uri.parent); 166 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(uri, parentDirectoryId); 167 | 168 | for (const d of subdirectories) { 169 | if (d.name === uri.name) { 170 | return d.id; 171 | } 172 | } 173 | } 174 | 175 | throw new Error(`Path not found: ${uri.globalPath}`); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /PROMO.md: -------------------------------------------------------------------------------- 1 | # MCFS [AMPScript] v3.0.6 2 | 3 | Greetings Marketing Cloud Experts! You've just updated (or installed) an **AMPscript [MCFS]** extension for Visual Studio Code. This version brings some really cool new features, that I would like to share with you. Share your ideas using [this form](https://docs.google.com/forms/d/e/1FAIpQLSc8NCJcqTxMIIJ5J1pWKTnPY2JewvTS8GU6b9-Lvhdze1N4RA/viewform?usp=sf_link), leave your feedback on the [Extension Page](https://marketplace.visualstudio.com/items?itemName=sergey-agadzhanov.AMPscript) or add a star on my [github repository](https://github.com/Bizcuit/vscode-ampscript). 4 | 5 | ```diff 6 | + === NEW FEATURES === 7 | 8 | + Extension now works with Corporate Proxies 9 | + Support for shared Dataextensions (you need to be connected to Ent BU) 10 | ``` 11 | 12 | ### To run an SQL query 13 | * Connect to your MC account 14 | * Find your SQL Query asset in the "SQL Queries" folder and open a "query.sql" file 15 | * Click a "Run SQL Query" button located in the top right corner of the editor (or run a "MCFS: Run SQL Query" command from the Command Pallet) 16 | 17 | ![SQL Queries](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/mcfs_runquery.jpg) 18 | 19 | 20 | ### To filter a Dataextension 21 | * Connect to your MC account 22 | * Find your Dataextension asset in the "Dataextensions" folder and open a "rows.csv" file 23 | * Click a "Filter a Dataextension" button located in the top right corner of the editor (or run a "MCFS: Filter a Dataextension" command from the Command Pallet) 24 | * Set the filter and hit enter 25 | * Filter example: OrderID = 'ORD2123F2' AND SubscriberKey = 'ABC' 26 | 27 | ![Dataextensions](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/mcfs_filterde.jpg) 28 | 29 | 30 | ## SFMC DevTools published 31 | 32 | We have been talking about this behind the scenes already for quite some time but on March 26 the [SFMC DevTools](https://bit.ly/mc-devtools) were finally open-sourced. It allows you to up-/download all kinds of metadata, run mass-deployments to multiple BUs and on top it can be integrated into your IDE or CI/CD solution. And here comes the best part: We are looking into the possibility of integrating it into this VSCode extension. 33 | 34 | 35 | # Direct connection to Marketing Cloud 36 | 37 | ## 1. Connect directly to your Marketing Cloud Account 38 | 39 | With a quick 5 minutes setup you'll be able to edit content blocks, emails, cloudpages, dataextensions and SQL queries without leaving Visual Studio Code. You can now avoid frequent copy-pasting and focus on your work. Have a look a quick demo below. To open Connection Manager: 40 | * Press F1 (or 'CMD+Shift+P' on Mac and 'CTRL+Shift+P' on Windows) 41 | * Start typing 'MCFS' 42 | * Find 'MCFS Connecton Manager' and then press Enter 43 | * You'll find detailed setup instuctions there 44 | 45 | ![AMPScript](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/mcfs.gif) 46 | 47 | ## 1.a How to connect to Marketing Cloud 48 | 49 | As of now, you **can only edit existing assets** (content blocks, emails, cloudpage and json message). Functionality that is not supported at the moment: create new asset, rename asset, move asset to a different folder, delete asset. 50 | 51 | * In your MC accout, create a new installed package and add a 'Server-to-Server' API integration Component 52 | * Add the following permissions: 53 | * CHANNELS: Email (Read and Write) 54 | * CHANNELS: Web (Read, Write, Publish) 55 | * ASSETS: Saved Content (Read and Write) 56 | * AUTOMATION: Automations (Read, Write, Execute) 57 | * DATA: Data Extensions (Read, Write) 58 | * Grant access to all required BUs 59 | * Provide package details in the connection manager below, save it and connect 60 | * You'll find the entire Content Builder library in your File Explorer tab 61 | * To open Connection Manager next time press F1 (or 'CMD+Shift+P' on Mac and 'CTRL+Shift+P' on Windows) and start typing 'MCFS'. Find 'MCFS Connecton Manager' and then hit Enter 62 | 63 | Detailed instructions with screenshots are available directly in the Connection Manager. To open Connection Manager press F1 (or 'CMD+Shift+P' on Mac and 'CTRL+Shift+P' on Windows) and start typing 'MCFS'. Find 'MCFS Connecton Manager' and then hit Enter. 64 | 65 | ### Assets that you can work with 66 | * Content Builder assets (Emails, Messages and Content Blocks) 67 | * Landing Pages (created with Content Builder editor) 68 | * Dataextensions (Edit data in dataextensions, apply filters, export to CSV etc.) 69 | * SQL Queries (Edit queries and Run them) 70 | 71 | ### 1.b How to edit assets directly from Visual Studio Code 72 | 73 | Each asset is presented as a folder that starts with an 'Ω' symbol. You can easily distinguish different asset types based on the colored square that goes after 'Ω': 74 | * 🟥 - blocks 75 | * 🟦 - emails 76 | * 🟨 - templates 77 | * 🟩 - cloudpages 78 | * 🟪 - mobile messages 79 | 80 | Each asset folder includes a readonly '__raw.readonly.json' file. This is an API representation of the asset. You can not modify. Instead you can modify all other files available under the asset folder. Each file represents a specific part of the asset. For the template based email you will see for example smth like: 81 | * _htmlcontent.amp - template used to create an email 82 | * _subject.amp - subject line of the email 83 | * _preheader.amp - preheader of the email 84 | * s01.b01.content.amp - content of the first content block (b01) that is located in the first template stack/placeholder (s01) 85 | * s01.b01.super.amp - super content of the block above. Learn more about super content [here](https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/design_super_content.htm) 86 | * s01.b02.content.amp - content of the second block in the first template stack 87 | * s01.b02.super.amp - super content of the second block in the first template stack 88 | * s02.b01.content.amp - content of the first block in the second template stack 89 | * s02.b01.super.amp - super content of the first block in the second template stack 90 | 91 | ## 2. Hover code snippets 92 | 93 | Now you can mouse hover a function name in your code and a small popup window including documentation on this function will show up. Check a small example below 94 | 95 | ![Hover snippets](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/screenshot_hoversnippets.jpg) 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCFS [AMPScript]: Virtual filesystem for Marketing Cloud, syntax highlighting, code snippets and more 2 | 3 | ![AMPScript](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/mcfs.gif) 4 | 5 | AMPScript is the language used to program emails, content blocks, webpages and script activities in the Salesforce Marketing Cloud. It is not a simple task to write code directly in the UI of MC. This extensions helps you solve this problem. 6 | 7 | This extension allows you to connect Visual Studio Code directly to your MC Account, enables syntax highlighting for AMPScript, has built-in documentation for all AMPScript functions and also adds code snippets for language elements and functions. Each snippet includes a detailed description of the function and its parameters. Snippets also show up when you hover a function name. 8 | 9 | With direct connection to MC you can: easily change content in MC without leaving your text editor, save time and avoid frequent copy-pasting. It also helps you to better control the content of your emails, content blocks and cloud pages by exposing additional content attributes that are not available in the UI of MC. 10 | 11 | ### How to enable syntax highlighting 12 | 13 | You have two options on how to enable syntax highlighting: 14 | 15 | * Open a file that has an ".amp" or an ".ampscript" file extension 16 | * Manually set the language of the file to "AMPscript" (check the video below) 17 | 18 | ![AMPScript](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/screenshot_video_howto.gif) 19 | 20 | 21 | ### How to connect to Marketing Cloud 22 | 23 | As of now, you **can only edit existing assets** (content blocks, emails, cloudpage, json message, sql queries and dataextensions). Functionality that is not supported at the moment: create new asset, rename asset, move asset to a different folder, delete asset. 24 | 25 | * In your MC accout, create a new installed package and add a 'Server-to-Server' API integration Component 26 | * Add the following permissions: 27 | * CHANNELS: Email (Read and Write) 28 | * CHANNELS: Web (Read, Write, Publish) 29 | * ASSETS: Saved Content (Read and Write) 30 | * AUTOMATION: Automations (Read, Write, Execute) 31 | * DATA: Data Extensions (Read, Write) 32 | * Grant access to all required BUs 33 | * Provide package details in the connection manager below, save it and connect 34 | * You'll find the entire Content Builder library in your File Explorer tab 35 | * To open Connection Manager next time press F1 (or 'CMD+Shift+P' on Mac and 'CTRL+Shift+P' on Windows) and start typing 'MCFS'. Find 'MCFS Connecton Manager' and then hit Enter 36 | 37 | Detailed instructions with screenshots are available directly in the Connection Manager. To open Connection Manager press F1 (or 'CMD+Shift+P' on Mac and 'CTRL+Shift+P' on Windows) and start typing 'MCFS'. Find 'MCFS Connecton Manager' and then hit Enter. 38 | 39 | ### How to edit Content Builder assets directly from Visual Studio Code 40 | 41 | Each asset is presented as a folder that starts with an 'Ω' symbol. You can easily distinguish different asset types based on the colored square that goes after 'Ω': 42 | * 🟥 - blocks 43 | * 🟦 - emails 44 | * 🟨 - templates 45 | * 🟩 - cloudpages 46 | * 🟪 - mobile messages 47 | 48 | Each asset folder includes a readonly '__raw.readonly.json' file. This is an API representation of the asset. You can not modify. Instead you can modify all other files available under the asset folder. Each file represents a specific part of the asset. For the template based email you will see for example smth like: 49 | * _htmlcontent.amp - template used to create an email 50 | * _subject.amp - subject line of the email 51 | * _preheader.amp - preheader of the email 52 | * s01.b01.content.amp - content of the first content block (b01) that is located in the first template stack/placeholder (s01) 53 | * s01.b01.super.amp - super content of the block above. Learn more about super content [here](https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/design_super_content.htm) 54 | * s01.b02.content.amp - content of the second block in the first template stack 55 | * s01.b02.super.amp - super content of the second block in the first template stack 56 | * s02.b01.content.amp - content of the first block in the second template stack 57 | * s02.b01.super.amp - super content of the first block in the second template stack 58 | 59 | ### How to run an SQL query 60 | * Connect to your MC account 61 | * Find your SQL Query asset in the "SQL Queries" folder and open a "query.sql" file 62 | * Click a "Run SQL Query" button located in the top right corner of the editor (or run a "MCFS: Run SQL Query" command from the Command Pallet) 63 | 64 | ![SQL Queries](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/mcfs_runquery.jpg) 65 | 66 | 67 | ### How to filter a Dataextension 68 | * Connect to your MC account 69 | * Find your Dataextension asset in the "Dataextensions" folder and open a "rows.csv" file 70 | * Click a "Filter a Dataextension" button located in the top right corner of the editor (or run a "MCFS: Filter a Dataextension" command from the Command Pallet) 71 | * Set the filter and hit enter 72 | * Filter example: OrderID = 'ORD2123F2' AND SubscriberKey = 'ABC' 73 | 74 | ![Dataextensions](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/mcfs_filterde.jpg) 75 | 76 | ### How it looks and works 77 | 78 | #### Demo 79 | 80 | ![Demo](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/screenshot_video.gif) 81 | 82 | #### Hover snippets 83 | 84 | ![Hover snippets](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/screenshot_hoversnippets.jpg) 85 | 86 | #### Code snippets 87 | 88 | ![Function snippets](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/screenshot_snippets.png) 89 | 90 | #### Syntax highlighting 91 | 92 | ![Syntax highlighting](https://raw.githubusercontent.com/Bizcuit/vscode-ampscript/master/images/screenshot.png) 93 | 94 | 95 | 96 | #### Copyright 2017-2021 Sergey Agadzhanov 97 | 98 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 99 | 100 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 101 | 102 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 103 | -------------------------------------------------------------------------------- /src/libs/connectionController.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as xml2js from 'xml2js'; 4 | import { ApiRequestConfig, HttpUtils } from './httpUtils'; 5 | import { Utils } from './utils'; 6 | 7 | interface Token { 8 | rest_instance_url: string; 9 | soap_instance_url: string; 10 | access_token: string; 11 | token_type: string; 12 | expires_in: number; 13 | scope: string; 14 | expires: Date; 15 | } 16 | 17 | export interface Connection { 18 | name: string; 19 | account_id: string; 20 | authBaseUri: string; 21 | client_id: string; 22 | client_secret: string; 23 | grant_type: string | undefined; 24 | } 25 | 26 | export class APIException extends Error { 27 | public message: string; 28 | public details: string; 29 | public data: string; 30 | public readonly innerException: any; 31 | 32 | constructor(message: string, details: string, innerException?: any) { 33 | super(); 34 | this.message = message; 35 | this.details = details; 36 | this.data = ''; 37 | this.innerException = innerException; 38 | 39 | const data = this.innerException?.response?.data; 40 | 41 | if (data?.errors) { 42 | this.data = 'Errors: ' + data?.errors?.map((err: any) => err.message)?.join(' | ') || ''; 43 | } 44 | else if (data?.error_description) { 45 | this.data = 'Errors: ' + data?.error_description; 46 | } 47 | else { 48 | this.data = JSON.stringify(data); 49 | } 50 | } 51 | } 52 | 53 | export enum SoapOperation { 54 | RETRIEVE = "Retrieve", 55 | UPDATE = "Update" 56 | } 57 | 58 | export interface SoapRequestConfig { 59 | operation: SoapOperation; 60 | transformResponse?: (responseBody: any) => any; 61 | body: any; 62 | } 63 | 64 | export class ConnectionController { 65 | private connections: Map; 66 | private tokens: Map>; 67 | 68 | private static instance: ConnectionController | null = null; 69 | 70 | constructor() { 71 | this.connections = new Map(); 72 | this.tokens = new Map>(); 73 | } 74 | 75 | static getInstance(): ConnectionController { 76 | if (ConnectionController.instance === null) { 77 | ConnectionController.instance = new ConnectionController(); 78 | } 79 | return ConnectionController.instance; 80 | } 81 | 82 | setConnections(connections: Array): void { 83 | connections.forEach(c => { 84 | if (c.grant_type === undefined) { 85 | c.grant_type = 'client_credentials' 86 | } 87 | this.connections.set(c.account_id, c); 88 | }); 89 | } 90 | 91 | async hasTokenRequiredScopes(connectionId: string, scopes: Array): Promise { 92 | const token = await this.getToken(connectionId); 93 | 94 | for (const scope of scopes) { 95 | if (!token.scope.includes(scope)) return false; 96 | } 97 | 98 | return true; 99 | } 100 | 101 | async getToken(connectionId: string): Promise { 102 | const pToken: Promise = this.tokens.get(connectionId) || this.refreshToken(connectionId); 103 | let error: any = undefined; 104 | 105 | try { 106 | let token = await pToken; 107 | 108 | if (token.expires == null || token.expires < new Date()) { 109 | token = await this.refreshToken(connectionId); 110 | } 111 | 112 | if (token !== undefined) { 113 | return token; 114 | } 115 | } 116 | catch (err: any) { 117 | error = err; 118 | } 119 | 120 | throw new APIException( 121 | 'Connection Issue', 122 | `Failed to get a token for connection "${connectionId}" 123 | Check your configuration and make sure that all required 124 | permissions were set for the installed Package`, 125 | error 126 | ); 127 | } 128 | 129 | async refreshToken(connectionId: string): Promise { 130 | const connection = this.connections.get(connectionId); 131 | 132 | if (connection === undefined) { 133 | throw new APIException( 134 | 'Connection Issue', 135 | `Connection "${connectionId}" has not been found in the Connection Manager`); 136 | } 137 | 138 | const now = new Date().getTime(); 139 | 140 | const apiConfig = new ApiRequestConfig({ 141 | baseURL: connection.authBaseUri, 142 | method: "POST", 143 | url: "/v2/token", 144 | data: JSON.stringify(connection) 145 | }); 146 | 147 | const pToken = HttpUtils.getInstance().makeRestApiCall(apiConfig).then(tokenData => { 148 | const token = tokenData as Token; 149 | token.expires = new Date(now + (token.expires_in - 5) * 1000); 150 | Utils.getInstance().log(`Token object is ready for ${connectionId}: ${JSON.stringify(token.token_type)}`); 151 | return token; 152 | }).catch(err => { 153 | Utils.getInstance().logError(err); 154 | throw err; 155 | }); 156 | 157 | this.tokens.set(connectionId, pToken); 158 | 159 | Utils.getInstance().log(`Token for ${connectionId} is added to the connections pool: size=${this.tokens?.size}`); 160 | 161 | return pToken; 162 | } 163 | 164 | async restRequest(connectionId: string, config: ApiRequestConfig): Promise { 165 | try { 166 | const token: Token = await this.getToken(connectionId); 167 | 168 | config.baseURL = token.rest_instance_url; 169 | config.headers = { 170 | 'Authorization': `${token.token_type} ${token.access_token}` 171 | }; 172 | 173 | const data = await HttpUtils.getInstance().makeRestApiCall(config); 174 | return data; 175 | 176 | } 177 | catch (ex: any) { 178 | throw (ex instanceof APIException ? ex : new APIException('REST API failed', ex.message, ex)); 179 | } 180 | } 181 | 182 | async soapRequest(connectionId: string, config: SoapRequestConfig): Promise { 183 | try { 184 | const token: Token = await this.getToken(connectionId); 185 | const xmlBuilder = new xml2js.Builder(); 186 | 187 | const body: any = { 188 | Envelope: { 189 | $: { 190 | "xmlns": "http://schemas.xmlsoap.org/soap/envelope/", 191 | "xmlns:xsd": "http://www.w3.org/2001/XMLSchema", 192 | "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance" 193 | }, 194 | Header: { 195 | fueloauth: { 196 | $: { 197 | "xmlns": "http://exacttarget.com" 198 | }, 199 | _: token.access_token 200 | } 201 | }, 202 | Body: config.body 203 | } 204 | }; 205 | 206 | const requestConfig: ApiRequestConfig = new ApiRequestConfig({ 207 | baseURL: token.soap_instance_url, 208 | url: '/Service.asmx', 209 | method: 'post', 210 | data: xmlBuilder.buildObject(body), 211 | headers: { 212 | 'Content-Type': 'text/xml', 213 | 'SOAPAction': config.operation 214 | } 215 | }); 216 | 217 | const responseData = await HttpUtils.getInstance().makeApiCall(requestConfig); 218 | const parser = new xml2js.Parser(); 219 | 220 | return parser.parseStringPromise(responseData).then(result => { 221 | const body = result["soap:Envelope"]["soap:Body"]; 222 | return config.transformResponse === undefined ? body : config.transformResponse(body); 223 | }); 224 | } 225 | catch (ex: any) { 226 | throw (ex instanceof APIException ? ex : new APIException('SOAP API failed', ex.message, ex)); 227 | } 228 | } 229 | 230 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import * as path from 'path'; 5 | import { MCFS } from './mcfsFileSystemProvider'; 6 | import { Connection } from './libs/connectionController'; 7 | import { Utils, WebPanel, WebPanelMessage } from './libs/utils'; 8 | import { ConnectionController } from './libs/connectionController'; 9 | import { FolderManagerUri } from './libs/folderManagerUri'; 10 | import { FolderController } from './libs/folderController'; 11 | 12 | let isConnectionManagerOpened = false; 13 | 14 | export async function activate(context: vscode.ExtensionContext) { 15 | try { 16 | context.subscriptions.push(Utils.getInstance().telemetry); 17 | Utils.getInstance().sendTelemetryEvent("activated"); 18 | Utils.getInstance().log('MCFS extension activated'); 19 | 20 | const mcfs = new MCFS(); 21 | 22 | let connections = Utils.getInstance().getConfig('connections'); 23 | 24 | ConnectionController.getInstance().setConnections(connections); 25 | 26 | const panel = new WebPanel('mcfs_connection_manager', 'MCFS Connection Manager'); 27 | 28 | const openConnectionManager = () => { 29 | Utils.getInstance().sendTelemetryEvent("connection-manager"); 30 | Utils.getInstance().setConfigField('notifications', 'hasOpenedConnectionManager', true); 31 | 32 | panel.onMessageReceived = (message: any) => { 33 | 34 | switch (message?.action) { 35 | case 'SEND_CONFIGS': 36 | panel.postMessage({ 37 | action: 'SET_CONFIGS', 38 | content: connections 39 | } as WebPanelMessage); 40 | break; 41 | 42 | case 'CONNECT': 43 | connect(message.content as Connection); 44 | Utils.getInstance().setConfigField('notifications', 'hasConnectedToMC', true); 45 | panel.close(); 46 | break; 47 | 48 | case 'UPDATE': 49 | connections = message.content; 50 | ConnectionController.getInstance().setConnections(connections); 51 | Utils.getInstance().setConfig('connections', connections); 52 | Utils.getInstance().showInformationMessage('Connections saved. Press "Connect" and then open File Explorer'); 53 | break; 54 | } 55 | }; 56 | 57 | panel.open(path.join(context.extensionPath, 'connection-manager')); 58 | }; 59 | 60 | context.subscriptions.push(vscode.workspace.registerFileSystemProvider('mcfs', mcfs, { isCaseSensitive: false })); 61 | 62 | context.subscriptions.push(vscode.commands.registerCommand('mcfs.open', _ => { 63 | isConnectionManagerOpened = true; 64 | openConnectionManager(); 65 | })); 66 | 67 | const registeredCommands: Array = []; 68 | 69 | FolderController.getInstance().customActions.forEach((a) => { 70 | if(registeredCommands.includes(a.command)) return; 71 | 72 | registeredCommands.push(a.command); 73 | 74 | context.subscriptions.push(vscode.commands.registerTextEditorCommand(a.command, async (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit, args: any[]) => { 75 | Utils.getInstance().sendTelemetryEvent(`customaction-${a.command}`); 76 | 77 | const uri = textEditor?.document?.uri; 78 | 79 | if (uri === undefined) return; 80 | 81 | const fmUri = new FolderManagerUri(uri); 82 | const currentContent = textEditor.document.getText(); 83 | 84 | vscode.window.withProgress({ 85 | location: vscode.ProgressLocation.Notification, 86 | title: a.waitLabel, 87 | cancellable: true 88 | }, async (progress, token) => { 89 | const result = await FolderController.getInstance().getManager(fmUri.mountFolderName) 90 | ?.customActions 91 | ?.find((c) => c.command == a.command) 92 | ?.callback(fmUri, currentContent); 93 | 94 | if (result !== undefined && currentContent !== result) { 95 | textEditor.edit((editBuilder) => { 96 | editBuilder.replace(new vscode.Selection(0, 0, textEditor.document.lineCount, 0), result); 97 | }); 98 | } 99 | 100 | return; 101 | }); 102 | })); 103 | }); 104 | 105 | setTimeout(_ => { 106 | if (!isConnectionManagerOpened) { 107 | if (!showPromoPage(context.extensionPath)) { 108 | showPromoBanner(openConnectionManager); 109 | } 110 | } 111 | }, 5000); 112 | 113 | enableSnippets(context.extensionPath); 114 | } 115 | catch (err) { 116 | Utils.getInstance().showErrorMessage(err); 117 | } 118 | } 119 | 120 | export function deactivate() { 121 | Utils.getInstance().telemetry.dispose(); 122 | } 123 | 124 | function connect(connection: Connection): void { 125 | Utils.getInstance().sendTelemetryEvent("connect"); 126 | 127 | const mcfsUri = vscode.Uri.parse('mcfs://' + connection.account_id + '/'); 128 | 129 | //TODO: replace folder 130 | 131 | if (undefined === vscode.workspace.getWorkspaceFolder(mcfsUri)) { 132 | vscode.workspace.updateWorkspaceFolders( 133 | vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, 0, 134 | { 135 | uri: mcfsUri, 136 | name: `MCFS_${connection.account_id}: ${connection.name}` 137 | } 138 | ); 139 | } 140 | 141 | vscode.commands.executeCommand('workbench.view.explorer'); 142 | Utils.getInstance().showInformationMessage(`Connected to ${connection.account_id}. Open File Explorer...`); 143 | } 144 | 145 | function enableSnippets(extensionPath: string) { 146 | Utils.getInstance().readJSON(extensionPath + '/syntaxes/snippets.json').then(snippets => { 147 | vscode.languages.registerHoverProvider('AMPscript', { 148 | provideHover(document, position, token) { 149 | let word = document.getText(document.getWordRangeAtPosition(position)); 150 | 151 | if (!word || word.length > 100) { 152 | return null; 153 | } 154 | 155 | word = word.toLowerCase(); 156 | 157 | if (snippets[word] !== undefined && snippets[word].description) { 158 | return { 159 | contents: [snippets[word].description] 160 | }; 161 | } 162 | return null; 163 | } 164 | }); 165 | }); 166 | } 167 | 168 | function showPromoBanner(connectionManagerCallback: () => void) { 169 | const notifications = Utils.getInstance().getConfig('notifications'); 170 | 171 | if (!notifications || notifications["dontShowConnectionManagerAlert"] || notifications["hasConnectedToMC"]) { 172 | return; 173 | } 174 | 175 | vscode.window.showInformationMessage( 176 | `Would you like to connect VSCode directly to Marketing Cloud?`, 177 | "YES, SOUNDS INTERESTING", 178 | "CHECK ON GITHUB", 179 | "NO") 180 | .then(selection => { 181 | if (selection == "YES, SOUNDS INTERESTING") { 182 | connectionManagerCallback(); 183 | } 184 | else if (selection == "CHECK ON GITHUB") { 185 | vscode.env.openExternal(vscode.Uri.parse('https://github.com/Bizcuit/vscode-ampscript')); 186 | } 187 | else if (selection == "NO") { 188 | Utils.getInstance().setConfigField('notifications', 'dontShowConnectionManagerAlert', true); 189 | } 190 | }); 191 | } 192 | 193 | function showPromoPage(externsionPath: string) { 194 | const notifications = Utils.getInstance().getConfig('notifications'); 195 | const version = Utils.extensionVersion.split(".", 2).join("."); 196 | 197 | if (notifications["hasSeenPromoForVersion"] === version) { 198 | return false; 199 | } 200 | 201 | const uri = vscode.Uri.file(path.join(externsionPath, 'PROMO.md')) 202 | 203 | Utils.getInstance().setConfigField('notifications', 'hasSeenPromoForVersion', version); 204 | 205 | vscode.commands.executeCommand('markdown.showPreview', uri); 206 | 207 | return true; 208 | } -------------------------------------------------------------------------------- /syntaxes/ampscript.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "name": "AMPScript", 4 | "scopeName": "source.amp", 5 | "keyEquivalent": "@A", 6 | "foldingStartMarker": "%%\\[\\s*$", 7 | "foldingStopMarker": "^\\s*\\]%%$", 8 | "injections": { 9 | "R:comment.block,comment.block.html,meta.attribute": { 10 | "patterns": [ 11 | { 12 | "include": "#ampscript" 13 | }, 14 | { 15 | "include": "#ampscript-substitutions" 16 | } 17 | ] 18 | } 19 | }, 20 | "patterns": [ 21 | { 22 | "include": "#ampscript" 23 | }, 24 | { 25 | "include": "#ampscript-substitutions" 26 | }, 27 | { 28 | "include": "text.html.basic" 29 | } 30 | ], 31 | "repository": { 32 | "ampscript": { 33 | "name": "meta.embedded.amp", 34 | "begin": "(%%[=\\[])", 35 | "end": "([\\]=]%%)", 36 | "beginCaptures": { 37 | "1": { 38 | "name": "keyword.other.namespace.amp" 39 | } 40 | }, 41 | "endCaptures": { 42 | "1": { 43 | "name": "keyword.other.namespace.amp" 44 | } 45 | }, 46 | "patterns": [ 47 | { 48 | "include": "#ampscript-comments" 49 | }, 50 | { 51 | "include": "#ampscript-functions" 52 | }, 53 | { 54 | "include": "#ampscript-numeric" 55 | }, 56 | { 57 | "include": "#ampscript-contstants" 58 | }, 59 | { 60 | "include": "#ampscript-language-elements" 61 | }, 62 | { 63 | "include": "#ampscript-strings" 64 | } 65 | ] 66 | }, 67 | "ampscript-comments": { 68 | "patterns": [ 69 | { 70 | "name": "comment.block.amp", 71 | "begin": "/\\*", 72 | "captures": { 73 | "0": { 74 | "name": "punctuation.definition.comment.amp" 75 | } 76 | }, 77 | "end": "\\*/" 78 | } 79 | ] 80 | }, 81 | "ampscript-functions": { 82 | "name": "support.function.amp", 83 | "match": "((?i:addobjectarrayitem|createobject|invokecreate|invokedelete|invokeexecute|invokeperform|invokeretrieve|invokeupdate|raiseerror|setobjectproperty|upsertcontact|attachfile|barcodeurl|beginimpressionregion|buildoptionlist|buildrowsetfromstring|buildrowsetfromxml|contentarea|contentblockbyid|contentareabyname|contentblockbyid|contentblockbykey|contentblockbyname|contentimagebyid|contentimagebykey|createsmsconversation|endimpressionregion|endsmsconversation|getportfolioitem|image|setsmsconversationnextkeyword|transformxml|treatascontent|treatascontentarea|wat|watp|claimrow|claimrowvalue|dataextensionrowcount|deletedata|deletede|executefilter|executefilterorderedrows|field|insertdata|insertde|lookup|lookuporderedrows|lookuprows|lookuprowscs|row|rowcount|updatedata|updatede|upsertdata|upsertde|dateadd|datediff|dateparse|datepart|formatdate|localdatetosystemdate|now|systemdatetolocaldate|base64decode|base64encode|decryptsymmetric|encryptsymmetric|guid|md5|sha1|sha256|sha512|httpget|httppost|httppost2|httprequestheader|ischtmlbrowser|redirectto|urlencode|wraplongurl|add|divide|formatcurrency|formatnumber|mod|multiply|random|subtract|addmscrmlistmember|createmscrmrecord|describemscrmentities|describemscrmentityattributes|retrievemscrmrecords|retrievemscrmrecordsfetchxml|setstatemscrmrecord|updatemscrmrecords|upsertmscrmrecord|createsalesforceobject|longsfid|retrievesalesforcejobsources|retrievesalesforceobjects|updatesinglesalesforceobject|authenticatedemployeeid|authenticatedemployeenotificationaddress|authenticatedemployeeusername|authenticatedenterpriseid|authenticatedmemberid|authenticatedmembername|cloudpagesurl|isnulldefault|livecontentmicrositeurl|micrositeurl|queryparameter|redirect|requestparameter|getpublishedsocialcontent|getsocialpublishurl|getsocialpublishurlbyname|char|concat|format|indexof|length|lowercase|propercase|regexmatch|replace|replacelist|stringtodate|stringtohex|substring|trim|uppercase|attributevalue|domain|empty|iif|isemailaddress|isnull|isphonenumber|output|outputline|v)\\b)(?=\\()" 84 | }, 85 | "ampscript-numeric": { 86 | "name": "constant.numeric.amp", 87 | "match": "\\b((0(x|X)[0-9a-fA-F]+)|([0-9]+(\\.[0-9]+)?))\\b" 88 | }, 89 | "ampscript-contstants": { 90 | "patterns": [ 91 | { 92 | "name": "constant.language.boolean.true.amp", 93 | "match": "((?i:true)\\b)" 94 | }, 95 | { 96 | "name": "constant.language.boolean.false.amp", 97 | "match": "((?i:false)\\b)" 98 | }, 99 | { 100 | "name": "constant.language.boolean.null.amp", 101 | "match": "((?i:null)\\b)" 102 | } 103 | ] 104 | }, 105 | "ampscript-language-elements": { 106 | "patterns": [ 107 | { 108 | "name": "keyword.control.amp", 109 | "match": "((?i:do|else|elseif|for|if|endif|next|then|to|downto)\\b)" 110 | }, 111 | { 112 | "name": "storage.type.amp", 113 | "match": "((?i:var|set)\\b)" 114 | }, 115 | { 116 | "name": "variable.parameter.amp", 117 | "match": "\\@[a-zA-Z0-9_]+" 118 | }, 119 | { 120 | "name": "variable.parameter.amp", 121 | "match": "\\[[a-zA-Z0-9_]+\\]" 122 | }, 123 | { 124 | "name": "variable.language.amp", 125 | "match": "\\b((?i:xtmonth|xtmonthnumeric|xtday|xtdayofweek|xtyear|xtshortdate|xtlongdate|linkname|linkname|emailname_|_messagecontext|_messagetypepreference|_replycontent|_istestsend|jobid|_preheader|double_opt_in_url|emailaddr|fullname_|fullname|firstname_|firstname|lastname_|lastname|comment_|comment|subscriberid|_subscriberkey|listid|list_|listsubid|_messagetypepreference|mobile_number|short_code|_listname|_emailid|_jobsubscriberbatchid|_datasourcename|_impressionregionid|_impressionregionname|replyname|replyemailaddress|memberid|member_busname|member_addr|member_city|member_state|member_postalcode|member_country|view_email_url|ftaf_url|subscription_center_url|profile_center_url|unsub_center_url|mobile_number|short_code|line_address_id|line_job_id|line_subscriber_id|additionalinfo_|__additionalemailattribute1|__additionalemailattribute2|__additionalemailattribute3|__additionalemailattribute4|__additionalemailattribute5))\\b" 126 | }, 127 | { 128 | "name": "support.class.amp", 129 | "match": "((?i:and|or|not)\\b)" 130 | }, 131 | { 132 | "name": "variable.operator.amp", 133 | "match": "==|!=|>|<|>=|<=|=" 134 | } 135 | ] 136 | }, 137 | "ampscript-strings": { 138 | "patterns": [ 139 | { 140 | "name": "string.quoted.double.amp", 141 | "begin": "\"", 142 | "end": "\"", 143 | "beginCaptures": { 144 | "0": { 145 | "name": "punctuation.definition.string.begin.amp" 146 | } 147 | }, 148 | "endCaptures": { 149 | "0": { 150 | "name": "punctuation.definition.string.end.amp" 151 | } 152 | }, 153 | "patterns": [ 154 | { 155 | "name": "constant.character.escape.amp", 156 | "match": "\"\"" 157 | } 158 | ] 159 | }, 160 | { 161 | "name": "string.quoted.single.amp", 162 | "begin": "'", 163 | "end": "'", 164 | "beginCaptures": { 165 | "0": { 166 | "name": "punctuation.definition.string.begin.amp" 167 | } 168 | }, 169 | "endCaptures": { 170 | "0": { 171 | "name": "punctuation.definition.string.end.amp" 172 | } 173 | }, 174 | "patterns": [ 175 | { 176 | "name": "constant.character.escape.amp", 177 | "match": "''" 178 | } 179 | ] 180 | } 181 | ] 182 | }, 183 | "ampscript-substitutions": { 184 | "name": "meta.embedded.amp", 185 | "begin": "(%%)", 186 | "end": "(%%)", 187 | "beginCaptures": { 188 | "1": { 189 | "name": "keyword.other.namespace.amp" 190 | } 191 | }, 192 | "endCaptures": { 193 | "1": { 194 | "name": "keyword.other.namespace.amp" 195 | } 196 | }, 197 | "patterns": [ 198 | { 199 | "name": "variable.parameter.amp", 200 | "match": "[a-zA-Z0-9_]+" 201 | } 202 | ] 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /src/libs/folderManagers/sqlQueries.ts: -------------------------------------------------------------------------------- 1 | import { Asset, AssetFile } from '../asset'; 2 | import { FolderManagerUri } from '../folderManagerUri'; 3 | import { FolderManager, Directory, CustomAction } from '../folderManager'; 4 | import { ConnectionController } from '../connectionController'; 5 | import { Utils } from '../utils'; 6 | import { ApiRequestConfig } from '../httpUtils'; 7 | 8 | 9 | export class SqlQueriesFolderManager extends FolderManager { 10 | readonly mountFolderName: string = "SQL Queries"; 11 | private directoriesCache: Map>>; 12 | 13 | constructor() { 14 | super(); 15 | this.directoriesCache = new Map>>(); 16 | 17 | this.customActions.push({ 18 | command: "mcfs.query.run", 19 | waitLabel: "Running a Query", 20 | callback: (fmUri: FolderManagerUri, content: string): Promise => this.customActionRunQuery(fmUri, content) 21 | } as CustomAction); 22 | } 23 | 24 | /* Interface implementation */ 25 | 26 | async getAssetsInDirectory(directoryUri: FolderManagerUri): Promise { 27 | const directoryId: number = await this.getDirectoryId(directoryUri); 28 | 29 | const hasTokenScopes = await ConnectionController.getInstance().hasTokenRequiredScopes( 30 | directoryUri.connectionId, 31 | ['automations_execute', 'automations_read', 'automations_write'] 32 | ); 33 | 34 | if (!hasTokenScopes) { 35 | Utils.getInstance().sendTelemetryEvent("error.manager-sqlqueries.missing_api_scope", true, true); 36 | throw new Error('Additional permissions are required for this function: AUTOMATION: Automations (Read, Write, Execute). Please update your installed package and restart VSCode'); 37 | } 38 | 39 | const config = new ApiRequestConfig({ 40 | method: 'get', 41 | url: `/automation/v1/queries/category/${directoryId}`, 42 | params: { 43 | '$page': 1, 44 | '$pageSize': 100, 45 | 'retrievalType': 1 46 | } 47 | }); 48 | 49 | const data: any = await ConnectionController.getInstance().restRequest(directoryUri.connectionId, config); 50 | 51 | const assets: Array = new Array(); 52 | 53 | (data.items as Array).forEach(a => { 54 | const asset: Asset = new Asset( 55 | a.name || '???', 56 | //AssetSubtype.SQL, 57 | this.getAssetDirectoryName(a.name, a), 58 | JSON.stringify(a, null, 2), 59 | directoryUri.connectionId, 60 | this.extractFiles(a) 61 | ); 62 | //this.assetsCache.set(directoryUri.getChildPath(asset.fsName), asset); 63 | this.assetsCache.set(directoryUri.getChildPath(asset.directoryName), asset); 64 | assets.push(asset); 65 | }); 66 | 67 | return assets; 68 | } 69 | 70 | async getSubdirectories(directoryUri: FolderManagerUri): Promise { 71 | const directoryId: number = await this.getDirectoryId(directoryUri); 72 | 73 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(directoryUri, directoryId); 74 | return subdirectories.map(d => d.name); 75 | } 76 | 77 | async saveAsset(asset: Asset): Promise { 78 | const assetData: any = JSON.parse(asset.content); 79 | 80 | const config = new ApiRequestConfig({ 81 | method: 'patch', 82 | url: `/automation/v1/queries/${assetData.queryDefinitionId}`, 83 | data: assetData 84 | }); 85 | 86 | await ConnectionController.getInstance().restRequest(asset.connectionId, config); 87 | } 88 | 89 | async setAssetFile(asset: Asset, file: AssetFile): Promise { 90 | const assetData: any = JSON.parse(asset.content); 91 | assetData[file.path] = file.content; 92 | asset.content = JSON.stringify(assetData, null, 2); 93 | } 94 | 95 | getAssetDirectoryName(name: string, assetData: any): string { 96 | return `Ω 🟥 ${name}.query`; 97 | } 98 | 99 | getFileExtensions(): Array { 100 | return ['.sql', '.json']; 101 | } 102 | 103 | /* Support methods */ 104 | 105 | public async customActionRunQuery(fmUri: FolderManagerUri, content: string): Promise { 106 | const assetUri = fmUri.isAsset ? fmUri : fmUri.parent; 107 | 108 | if (assetUri === undefined) return; 109 | 110 | const asset = await this.getAsset(assetUri, false); 111 | const assetMetadata: any = JSON.parse(asset.content); 112 | const queryId = assetMetadata?.["queryDefinitionId"]; 113 | 114 | if (!queryId) return; 115 | 116 | await this.runQuery(queryId, fmUri.connectionId); 117 | 118 | const retriesDelay = 6000; 119 | const maxNumberOfRetries = 20; 120 | let currentRetry = 0; 121 | let hasFinished = false; 122 | 123 | while (maxNumberOfRetries > currentRetry++) { 124 | await Utils.getInstance().delay(retriesDelay); 125 | const isRunning = await this.isQueryRunning(queryId, fmUri.connectionId); 126 | 127 | if (!isRunning) { 128 | hasFinished = true; 129 | break; 130 | } 131 | } 132 | 133 | if (hasFinished) { 134 | Utils.getInstance().showInformationMessage("Query executed successfully"); 135 | } 136 | else { 137 | Utils.getInstance().showErrorMessage("Query wait timeout"); 138 | } 139 | 140 | return; 141 | } 142 | 143 | 144 | 145 | private async runQuery(queryId: string, connectionId: string): Promise { 146 | const config = new ApiRequestConfig({ 147 | method: 'post', 148 | url: `automation/v1/queries/${queryId}/actions/start` 149 | }); 150 | 151 | await ConnectionController.getInstance().restRequest(connectionId, config); 152 | return; 153 | } 154 | 155 | private async isQueryRunning(queryId: string, connectionId: string): Promise { 156 | const config = new ApiRequestConfig({ 157 | method: 'get', 158 | url: `automation/v1/queries/${queryId}/actions/isrunning` 159 | }); 160 | 161 | const data: any = await ConnectionController.getInstance().restRequest(connectionId, config); 162 | 163 | Utils.getInstance().log(JSON.stringify(data)); 164 | 165 | return data.isRunning; 166 | } 167 | 168 | private extractFiles(assetData: any): Array { 169 | const result: Array = []; 170 | 171 | if (assetData?.queryText !== undefined) { 172 | result.push(new AssetFile( 173 | "query.sql", 174 | assetData?.queryText, 175 | "queryText" 176 | )); 177 | } 178 | 179 | return result; 180 | } 181 | 182 | private async getAllDirectories(connectionId: string): Promise> { 183 | const cached = this.directoriesCache.get(connectionId); 184 | 185 | if (cached !== undefined) { 186 | return cached; 187 | } 188 | 189 | const config = new ApiRequestConfig({ 190 | method: 'get', 191 | url: '/automation/v1/folders/', 192 | params: { 193 | '$pagesize': '200', 194 | '$filter': `categorytype eq queryactivity` 195 | } 196 | }); 197 | 198 | const pDirectories = ConnectionController.getInstance() 199 | .restRequest(connectionId, config) 200 | .then(response => { 201 | const directories = new Array(); 202 | 203 | response?.items?.forEach((e: any) => { 204 | directories.push({ 205 | id: e.categoryId, 206 | parentId: e.parentId, 207 | name: e.name 208 | } as Directory); 209 | }); 210 | 211 | return directories; 212 | }); 213 | 214 | this.directoriesCache.set(connectionId, pDirectories); 215 | 216 | return pDirectories; 217 | } 218 | 219 | private async getSubdirectoriesByDirectoryId(uri: FolderManagerUri, directoryId: number): Promise> { 220 | const allDirectories = await this.getAllDirectories(uri.connectionId); 221 | 222 | return allDirectories.filter(d => d.parentId === directoryId); 223 | } 224 | 225 | private async getDirectoryId(uri: FolderManagerUri): Promise { 226 | const allDirectories = await this.getAllDirectories(uri.connectionId); 227 | 228 | if (uri.localPath === '') { 229 | const root: Directory | undefined = allDirectories.find(d => d.parentId === 0); 230 | if (root !== undefined) { 231 | return root.id; 232 | } 233 | } 234 | 235 | if (uri.parent !== undefined) { 236 | const parentDirectoryId = await this.getDirectoryId(uri.parent); 237 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(uri, parentDirectoryId); 238 | 239 | for (const d of subdirectories) { 240 | if (d.name === uri.name) { 241 | return d.id; 242 | } 243 | } 244 | } 245 | 246 | throw new Error(`Path not found: ${uri.globalPath}`); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/libs/folderManagers/contentBuilder.ts: -------------------------------------------------------------------------------- 1 | import { Asset, AssetFile } from '../asset'; 2 | import { ConnectionController } from '../connectionController'; 3 | import { FolderManager, Directory } from '../folderManager'; 4 | import { FolderManagerUri } from '../folderManagerUri'; 5 | import { ApiRequestConfig } from '../httpUtils'; 6 | 7 | export enum AssetType { 8 | UNKNOWN = 0, 9 | BLOCK = 1, 10 | TEMPLATE = 2, 11 | EMAIL = 3, 12 | WEBPAGE = 4, 13 | JSON_MESSAGE = 5 14 | } 15 | 16 | export enum AssetSubtype { 17 | UNKNOWN = 0, 18 | TEMPLATE = 4, 19 | EMAIL_HTML = 208, 20 | EMAIL_TEMPLATEBASED = 207, 21 | EMAIL_TEXT = 209, 22 | BLOCK_CODESNIPPET = 220, 23 | BLOCK_FREEFORM = 195, 24 | BLOCK_TEXT = 196, 25 | BLOCK_HTML = 197, 26 | WEBPAGE = 205, 27 | JSON_MESSAGE = 230 28 | } 29 | 30 | export const ContentBuilderStandardTypes = [ 31 | AssetSubtype.TEMPLATE, 32 | AssetSubtype.EMAIL_HTML, 33 | AssetSubtype.EMAIL_TEMPLATEBASED, 34 | AssetSubtype.EMAIL_TEXT, 35 | AssetSubtype.BLOCK_CODESNIPPET, 36 | AssetSubtype.BLOCK_FREEFORM, 37 | AssetSubtype.BLOCK_TEXT, 38 | AssetSubtype.BLOCK_HTML, 39 | AssetSubtype.JSON_MESSAGE 40 | ]; 41 | 42 | export class ContentBuilderFolderManager extends FolderManager { 43 | readonly mountFolderName: string; 44 | 45 | private directoriesCache: Map; 46 | private readonly assetSubtypesFilter: Array; 47 | private readonly readOnlyRootFolder: boolean; 48 | private readonly isSharedFolderScope: boolean; 49 | 50 | constructor( 51 | mountFolderName: string, 52 | isSharedFolderScope = false, 53 | assetSubtypesFilter: Array, 54 | readOnlyRootFolder: boolean 55 | ) { 56 | super(); 57 | this.directoriesCache = new Map(); 58 | this.mountFolderName = mountFolderName; 59 | this.assetSubtypesFilter = assetSubtypesFilter; 60 | this.readOnlyRootFolder = readOnlyRootFolder; 61 | this.isSharedFolderScope = isSharedFolderScope; 62 | } 63 | 64 | /* Interface implementation */ 65 | async getSubdirectories(directoryUri: FolderManagerUri): Promise { 66 | const directoryId: number = await this.getDirectoryId(directoryUri); 67 | 68 | if (this.readOnlyRootFolder && directoryId != 0) return []; 69 | 70 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(directoryUri, directoryId); 71 | 72 | return subdirectories.map(d => d.name); 73 | } 74 | 75 | async getAssetsInDirectory(directoryUri: FolderManagerUri): Promise { 76 | const directoryId: number = await this.getDirectoryId(directoryUri); 77 | return this.getAssetsByDirectoryId(directoryUri, directoryId); 78 | } 79 | 80 | async saveAsset(asset: Asset): Promise { 81 | const assetData: any = JSON.parse(asset.content); 82 | 83 | const data: any = { 84 | id: assetData.id 85 | }; 86 | 87 | if (assetData.content) { 88 | data['content'] = assetData.content; 89 | } 90 | 91 | if (assetData.views) { 92 | data['views'] = assetData.views; 93 | } 94 | 95 | 96 | const config = new ApiRequestConfig({ 97 | method: 'patch', 98 | url: `/asset/v1/content/assets/${assetData.id}`, 99 | data: data 100 | }); 101 | 102 | await ConnectionController.getInstance().restRequest(asset.connectionId, config); 103 | } 104 | 105 | async setAssetFile(asset: Asset, file: AssetFile): Promise { 106 | const assetData: any = JSON.parse(asset.content); 107 | 108 | if (file.path === '') { 109 | asset.content = file.content; 110 | } 111 | else { 112 | const path = file.path.split('/'); 113 | let ref: any = assetData; 114 | 115 | for (let i = 0; i < path.length - 1; i++) { 116 | ref = ref?.[path[i]]; 117 | } 118 | 119 | const prop: string = path.pop() || ''; 120 | 121 | if (typeof ref[prop] === "object") { 122 | ref[prop] = JSON.parse(file.content); 123 | } 124 | else { 125 | ref[prop] = file.content; 126 | } 127 | 128 | asset.content = JSON.stringify(assetData, null, 2); 129 | } 130 | } 131 | 132 | getAssetDirectoryName(name: string, assetData: any): string { 133 | const subtype: AssetSubtype = assetData?.assetType?.id as AssetSubtype || AssetSubtype.UNKNOWN; 134 | const type: AssetType = ContentBuilderFolderManager.getAssetTypeBySubtype(subtype); 135 | 136 | let suffix = '.unknown'; 137 | let prefix = '⬛'; 138 | 139 | switch (type) { 140 | case AssetType.BLOCK: 141 | prefix = '🟥'; 142 | suffix = '.block'; 143 | break; 144 | 145 | case AssetType.EMAIL: 146 | prefix = '🟦'; 147 | suffix = '.email'; 148 | break; 149 | 150 | case AssetType.TEMPLATE: 151 | prefix = '🟨'; 152 | suffix = '.template'; 153 | break; 154 | 155 | case AssetType.WEBPAGE: 156 | prefix = '🟩'; 157 | suffix = '.cloudpage' 158 | break; 159 | 160 | case AssetType.JSON_MESSAGE: 161 | prefix = '🟪'; 162 | suffix = '.jsonmessage' 163 | break; 164 | 165 | default: 166 | prefix = '⬛'; 167 | suffix = '.unknown' 168 | break; 169 | } 170 | 171 | return `Ω ${prefix} ${name}${suffix}`; 172 | } 173 | 174 | getFileExtensions(): Array { 175 | return ['.amp', '.json']; 176 | } 177 | 178 | /* Support methods */ 179 | 180 | private static getAssetTypeBySubtype(subtype: AssetSubtype): AssetType { 181 | switch (subtype) { 182 | case AssetSubtype.BLOCK_CODESNIPPET: 183 | case AssetSubtype.BLOCK_FREEFORM: 184 | case AssetSubtype.BLOCK_HTML: 185 | case AssetSubtype.BLOCK_TEXT: 186 | return AssetType.BLOCK; 187 | case AssetSubtype.EMAIL_HTML: 188 | case AssetSubtype.EMAIL_TEMPLATEBASED: 189 | case AssetSubtype.EMAIL_TEXT: 190 | return AssetType.EMAIL; 191 | case AssetSubtype.TEMPLATE: 192 | return AssetType.TEMPLATE; 193 | case AssetSubtype.WEBPAGE: 194 | return AssetType.WEBPAGE; 195 | case AssetSubtype.JSON_MESSAGE: 196 | return AssetType.JSON_MESSAGE; 197 | } 198 | return AssetType.UNKNOWN; 199 | } 200 | 201 | private async getAssetsByDirectoryId(uri: FolderManagerUri, directoryId: number): Promise> { 202 | const config = new ApiRequestConfig({ 203 | method: 'post', 204 | url: '/asset/v1/content/assets/query', 205 | data: { 206 | "page": 207 | { 208 | "page": 1, 209 | "pageSize": 100 210 | }, 211 | "query": 212 | { 213 | "leftOperand": 214 | { 215 | "property": "category.id", 216 | "simpleOperator": "equal", 217 | "value": directoryId 218 | }, 219 | "logicalOperator": "AND", 220 | "rightOperand": 221 | { 222 | "property": "assetType.id", 223 | "simpleOperator": "in", 224 | "value": this.assetSubtypesFilter 225 | } 226 | } 227 | } 228 | }); 229 | 230 | const data: any = await ConnectionController.getInstance().restRequest(uri.connectionId, config); 231 | 232 | const assets: Array = new Array(); 233 | 234 | (data.items as Array).forEach(a => { 235 | const asset = new Asset( 236 | a.name, 237 | this.getAssetDirectoryName(a.name, a), 238 | JSON.stringify(a, null, 2), 239 | uri.connectionId, 240 | this.extractFiles(a) 241 | ); 242 | 243 | this.assetsCache.set(uri.getChildPath(asset.directoryName), asset) 244 | assets.push(asset); 245 | }); 246 | 247 | return assets; 248 | } 249 | 250 | private extractFiles(assetData: any): Array { 251 | const result: Array = []; 252 | 253 | if (assetData?.views?.subjectline?.content !== undefined) { 254 | result.push(new AssetFile( 255 | "_subject.amp", 256 | assetData?.views?.subjectline?.content, 257 | "views/subjectline/content" 258 | )); 259 | } 260 | 261 | if (assetData?.views?.preheader?.content !== undefined) { 262 | result.push(new AssetFile( 263 | "_preheader.amp", 264 | assetData?.views?.subjectline?.content, 265 | "views/preheader/content" 266 | )); 267 | } 268 | 269 | if (assetData?.views?.html?.content !== undefined) { 270 | result.push(new AssetFile( 271 | '_htmlcontent.amp', 272 | assetData?.views?.html?.content || '', 273 | 'views/html/content', 274 | )); 275 | } 276 | 277 | /* Templates and emails */ 278 | if (assetData?.content !== undefined) { 279 | result.push(new AssetFile( 280 | '_content.amp', 281 | assetData?.content || '', 282 | 'content' 283 | )); 284 | } 285 | 286 | /* JSON messages */ 287 | if (assetData?.views !== undefined) { 288 | const views: any = assetData?.views; 289 | 290 | for (const viewName in views) { 291 | const data: any = assetData?.views[viewName]?.meta?.options?.customBlockData; 292 | 293 | if (data) { 294 | result.push(new AssetFile( 295 | viewName.toLowerCase() + '.json', 296 | JSON.stringify(data, null, 2), 297 | `views/${viewName}/meta/options/customBlockData` 298 | )); 299 | 300 | const fields = ["display:message", "display:message:display", "display:title", "display:title:display", "display:subtitle", "display:subtitle:display"]; 301 | 302 | fields.forEach((f: string) => { 303 | if (data[f] !== undefined) { 304 | result.push(new AssetFile( 305 | f.toLowerCase().replace(/[:]/gi, '_') + '.amp', 306 | data[f], 307 | `views/${viewName}/meta/options/customBlockData/${f}` 308 | )); 309 | } 310 | }) 311 | } 312 | } 313 | } 314 | 315 | /* Emails and Cloud Pages */ 316 | if (assetData?.views?.html?.slots !== undefined) { 317 | const slots: any = assetData?.views?.html?.slots; 318 | let slotIndex = 0; 319 | 320 | for (const s in slots) { 321 | const slot = slots[s]; 322 | const blocks = slot.blocks || {}; 323 | let blockIndex = 1; 324 | 325 | slotIndex++; 326 | 327 | for (const b in blocks) { 328 | const block = blocks[b]; 329 | const path = `views/html/slots/${s}/blocks/${b}/`; 330 | const slotName = 's' + (slotIndex < 10 ? '0' : '') + slotIndex; 331 | const blockName = 'b' + (blockIndex < 10 ? '0' : '') + blockIndex; 332 | 333 | blockIndex++; 334 | 335 | result.push(new AssetFile( 336 | `${slotName}.${blockName}.content.amp`, 337 | block.content || "", 338 | path + "content", 339 | )); 340 | 341 | result.push(new AssetFile( 342 | `${slotName}.${blockName}.super.amp`, 343 | block.superContent || "", 344 | path + "superContent" 345 | )); 346 | } 347 | } 348 | 349 | } 350 | 351 | return result; 352 | } 353 | 354 | private async getDirectoryId(uri: FolderManagerUri): Promise { 355 | if (this.directoriesCache.get(uri.globalPath) !== undefined) { 356 | return this.directoriesCache.get(uri.globalPath) || 0; 357 | } 358 | 359 | if (uri.localPath === '') { 360 | return this.getRootDirectoryId(uri); 361 | } 362 | 363 | if (uri.parent !== undefined) { 364 | const parentDirectoryId = await this.getDirectoryId(uri.parent); 365 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(uri, parentDirectoryId); 366 | 367 | for (const d of subdirectories) { 368 | if (d.name === uri.name) { 369 | return d.id; 370 | } 371 | } 372 | } 373 | 374 | throw new Error(`Path not found: ${uri.globalPath}`); 375 | } 376 | 377 | private async getSubdirectoriesByDirectoryId(uri: FolderManagerUri, directoryId: number): Promise> { 378 | const scope = this.isSharedFolderScope ? "Shared" : "Ours"; 379 | 380 | const config = new ApiRequestConfig({ 381 | method: 'get', 382 | url: '/asset/v1/content/categories/', 383 | params: { 384 | '$pagesize': '100', 385 | '$filter': `parentId eq ${directoryId}`, 386 | 'scope': scope 387 | } 388 | }); 389 | 390 | const data: any = await ConnectionController.getInstance().restRequest(uri.connectionId, config); 391 | 392 | if (directoryId !== 0) { 393 | for (const d of data.items as Array) { 394 | this.directoriesCache.set(uri.getChildPath(d.name), d.id); 395 | } 396 | } 397 | 398 | return data.items as Array; 399 | } 400 | 401 | private async getRootDirectoryId(uri: FolderManagerUri): Promise { 402 | if (this.directoriesCache.get(uri.mountPath) !== undefined) { 403 | return this.directoriesCache.get(uri.mountPath) || 0; 404 | } 405 | 406 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(uri, 0); 407 | 408 | this.directoriesCache.set(uri.globalPath, subdirectories[0].id); 409 | 410 | return subdirectories[0].id; 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /src/libs/folderManagers/dataextensions.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Asset, AssetFile } from '../asset'; 3 | import { ConnectionController, SoapOperation, SoapRequestConfig } from '../connectionController'; 4 | import { FolderManager, Directory, CustomAction } from '../folderManager'; 5 | import { FolderManagerUri } from '../folderManagerUri'; 6 | import { Utils } from '../utils'; 7 | import * as Papa from 'papaparse'; 8 | import { SoapFilterExpression, SoapUtils } from '../soapUtils'; 9 | import * as vscode from 'vscode'; 10 | 11 | interface DataextensionColumn { 12 | Name: string; 13 | FieldType: string; 14 | ObjectID: string; 15 | MaxLength: string | undefined; 16 | Scale: string | undefined; 17 | IsRequired: string; 18 | IsPrimaryKey: string; 19 | DefaultValue: string | undefined; 20 | Ordinal: number; 21 | } 22 | 23 | export class DataextensionFolderManager extends FolderManager { 24 | readonly mountFolderName: string; 25 | readonly ignoreDirectories: boolean; 26 | readonly rootFolderCustomerKey: string; 27 | readonly folderContentType: string; 28 | private directoriesCache: Map; 29 | private filterString = ""; 30 | 31 | 32 | constructor( 33 | mountFolderName: string, 34 | ignoreDirectories: boolean, 35 | rootFolderCustomerKey = "dataextension_default", 36 | folderContentType = "dataextension") 37 | { 38 | super(); 39 | 40 | this.mountFolderName = mountFolderName; 41 | this.ignoreDirectories = ignoreDirectories; 42 | this.rootFolderCustomerKey = rootFolderCustomerKey; 43 | this.folderContentType = folderContentType; 44 | this.directoriesCache = new Map(); 45 | 46 | this.customActions.push({ 47 | command: "mcfs.dataextension.filter", 48 | waitLabel: "Filtering a Dataextension", 49 | callback: (fmUri: FolderManagerUri, content: string): Promise => this.customActionFilter(fmUri, content) 50 | } as CustomAction); 51 | } 52 | 53 | /* Interface implementation */ 54 | 55 | getAssetDirectoryName(name: string, assetData: any): string { 56 | return `Ω 🟦 ${name}.dataext`; 57 | } 58 | 59 | getFileExtensions(): Array { 60 | return ['.csv', '.txt']; 61 | } 62 | 63 | async getSubdirectories(directoryUri: FolderManagerUri): Promise { 64 | const directoryId: number = await this.getDirectoryId(directoryUri); 65 | 66 | if (this.ignoreDirectories && directoryId != 0) return []; 67 | 68 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(directoryUri, directoryId); 69 | 70 | return subdirectories.map(d => d.name); 71 | } 72 | 73 | async getAssetsInDirectory(directoryUri: FolderManagerUri): Promise { 74 | const directoryId: number = await this.getDirectoryId(directoryUri); 75 | 76 | const assets = await ConnectionController.getInstance().soapRequest(directoryUri.connectionId, { 77 | operation: SoapOperation.RETRIEVE, 78 | body: SoapUtils.createRetrieveBody( 79 | "DataExtension", 80 | ["Name", "CustomerKey"], 81 | { 82 | Property: "CategoryID", 83 | SimpleOperator: "equals", 84 | Value: directoryId 85 | } 86 | ), 87 | transformResponse: (body) => { 88 | return SoapUtils.getArrProp(body, "RetrieveResponseMsg.Results").map((e: any) => { 89 | const name: string = SoapUtils.getStrProp(e, "Name"); 90 | const customerKey: string = SoapUtils.getStrProp(e, "CustomerKey"); 91 | 92 | return new Asset( 93 | name, 94 | this.getAssetDirectoryName(name, ''), 95 | JSON.stringify(e, null, 2), 96 | directoryUri.connectionId, 97 | [ 98 | new AssetFile("rows.csv", "", "", async () => { 99 | const rows = await this.getDataextensionRows(directoryUri.connectionId, customerKey); 100 | return rows !== undefined ? Papa.unparse(rows) : ""; 101 | }), 102 | new AssetFile("rows.json", "", "", async () => { 103 | const rows = await this.getDataextensionRows(directoryUri.connectionId, customerKey); 104 | return JSON.stringify(rows, null, 2); 105 | }), 106 | new AssetFile("_columns.readonly.json", "", "", async () => { 107 | const columns = await this.getDataextensionColumns(directoryUri.connectionId, customerKey); 108 | return JSON.stringify(columns, null, 2); 109 | }), 110 | new AssetFile("_docs.readonly.txt", "", "", async () => { 111 | const columns = await this.getDataextensionColumns(directoryUri.connectionId, customerKey); 112 | return this.getDocumentationFileContent(name, columns); 113 | }) 114 | ] 115 | ); 116 | }); 117 | } 118 | } as SoapRequestConfig); 119 | 120 | if (assets === undefined) return []; 121 | 122 | for (const asset of assets) { 123 | this.assetsCache.set(directoryUri.getChildPath(asset.directoryName), asset); 124 | } 125 | 126 | return assets; 127 | } 128 | 129 | async setAssetFile(asset: Asset, file: AssetFile): Promise { 130 | const data = JSON.parse(asset.content); 131 | const customerKey = data?.CustomerKey?.[0]; 132 | 133 | if (file.name === "rows.csv") { 134 | const rows = Papa.parse(file.content, { 135 | header: true 136 | }).data; 137 | await this.upsertDataextensionRows(asset.connectionId, customerKey, rows); 138 | } 139 | 140 | else if (file.name === "rows.json") { 141 | const rows = JSON.parse(file.content); 142 | await this.upsertDataextensionRows(asset.connectionId, customerKey, rows); 143 | } 144 | 145 | return; 146 | } 147 | 148 | async saveAsset(asset: Asset): Promise { 149 | return; 150 | } 151 | 152 | 153 | 154 | /* Support methods */ 155 | 156 | private getDocumentationFileContent(name: string, columns: any) { 157 | let content = `Dataextension name: ${name}\r\n\r\n`; 158 | 159 | content += '| Name | Type | Not NULL | PK | Default Value |\r\n'; 160 | content += '| -------------------- | --------------- | -------- | -------- | --------------- |\r\n'; 161 | 162 | columns.forEach((c: any) => { 163 | let type = c.FieldType; 164 | 165 | if (c.FieldType == 'Decimal' && c.MaxLength) { 166 | type += '(' + c.MaxLength + (c.Scale ? ',' + c.Scale : '') + ')'; 167 | } 168 | 169 | if (c.FieldType == 'Text' && c.MaxLength) { 170 | type += '(' + c.MaxLength + ')'; 171 | } 172 | 173 | content += '| ' + c.Name.padEnd(20, ' ') 174 | + ' | ' + type.padEnd(15, ' ') 175 | + ' | ' + c.IsRequired.padEnd(8, ' ') 176 | + ' | ' + c.IsPrimaryKey.padEnd(8, ' ') 177 | + ' | ' + c.DefaultValue.padEnd(15, ' ') 178 | + ' |\r\n'; 179 | }) 180 | 181 | return content; 182 | } 183 | 184 | public async customActionFilter(fmUri: FolderManagerUri, content: string): Promise { 185 | const assetUri = fmUri.isAsset ? fmUri : fmUri.parent; 186 | 187 | if (assetUri === undefined) { 188 | return ""; 189 | } 190 | 191 | const filterString = await vscode.window.showInputBox({ 192 | value: this.filterString, 193 | ignoreFocusOut: true, 194 | placeHolder: "Your filter query string. EG: OrderID = 'ORD2123F2' AND SubscriberKey = 'ABC'" 195 | }); 196 | 197 | this.filterString = filterString || ""; 198 | 199 | const filter = new SoapFilterExpression(filterString as string).filter; 200 | const asset = await this.getAsset(assetUri, false); 201 | const customerKey = SoapUtils.getStrProp(JSON.parse(asset.content), "CustomerKey"); 202 | const rows = await this.getDataextensionRows(assetUri.connectionId, customerKey, filter); 203 | 204 | if (!rows?.length) { 205 | Utils.getInstance().showErrorMessage(new Error(`There are no data rows that match your filter: '${filterString}'. Non-filtered content will be shown`)); 206 | return undefined; 207 | } 208 | 209 | Utils.getInstance().showInformationMessage("Filter applied"); 210 | 211 | return fmUri.name.endsWith(".csv") ? Papa.unparse(rows) : JSON.stringify(rows, null, 2) 212 | } 213 | 214 | private async upsertDataextensionRows(connectionId: string, customerKey: string, rows: Array): Promise { 215 | const soapRows: Array = rows.map(row => { 216 | const o = { 217 | CustomerKey: customerKey, 218 | Properties: { 219 | Property: new Array() 220 | } 221 | }; 222 | 223 | for (const col in row) { 224 | if (col.toLowerCase() !== '_customobjectkey') { 225 | o.Properties.Property.push({ 226 | Name: col, 227 | Value: row[col] 228 | }); 229 | } 230 | } 231 | 232 | return o; 233 | }); 234 | 235 | return ConnectionController.getInstance().soapRequest(connectionId, { 236 | operation: SoapOperation.UPDATE, 237 | body: SoapUtils.createUpdateBody( 238 | "DataExtensionObject", 239 | soapRows 240 | ) 241 | }); 242 | } 243 | 244 | private async getDataextensionRows(connectionId: string, customerKey: string, filter?: any): Promise> { 245 | const columns = await this.getDataextensionColumns(connectionId, customerKey); 246 | 247 | return ConnectionController.getInstance().soapRequest(connectionId, { 248 | operation: SoapOperation.RETRIEVE, 249 | body: SoapUtils.createRetrieveBody( 250 | `DataExtensionObject[${customerKey}]`, 251 | [ 252 | "_CustomObjectKey", 253 | ...columns.map(c => c.Name.trim()) 254 | ], 255 | filter 256 | ), 257 | transformResponse: (body) => { 258 | return SoapUtils.getArrProp(body, "RetrieveResponseMsg.Results").map((e: any) => { 259 | const row: any = {}; 260 | 261 | SoapUtils.getArrProp(e, "Properties.Property").forEach((c: any) => { 262 | row[SoapUtils.getStrProp(c, "Name")] = SoapUtils.getStrProp(c, "Value"); 263 | }); 264 | 265 | return row; 266 | }); 267 | } 268 | } as SoapRequestConfig); 269 | } 270 | 271 | private async getDataextensionColumns(connectionId: string, customerKey: string): Promise> { 272 | const columns = await ConnectionController.getInstance().soapRequest(connectionId, { 273 | operation: SoapOperation.RETRIEVE, 274 | body: SoapUtils.createRetrieveBody( 275 | "DataExtensionField", 276 | [ 277 | "Name", 278 | "FieldType", 279 | "ObjectID", 280 | "MaxLength", 281 | "Scale", 282 | "IsRequired", 283 | "IsPrimaryKey", 284 | "DefaultValue", 285 | "Ordinal" 286 | ], 287 | { 288 | Property: "DataExtension.CustomerKey", 289 | SimpleOperator: "equals", 290 | Value: customerKey 291 | } 292 | ), 293 | transformResponse: (body) => { 294 | return SoapUtils.getArrProp(body, "RetrieveResponseMsg.Results").map((e: any) => { 295 | return { 296 | Name: SoapUtils.getStrProp(e, "Name"), 297 | FieldType: SoapUtils.getStrProp(e, "FieldType"), 298 | ObjectID: SoapUtils.getStrProp(e, "ObjectID"), 299 | MaxLength: SoapUtils.getStrProp(e, "MaxLength"), 300 | Scale: SoapUtils.getStrProp(e, "Scale"), 301 | IsRequired: SoapUtils.getStrProp(e, "IsRequired"), 302 | IsPrimaryKey: SoapUtils.getStrProp(e, "IsPrimaryKey"), 303 | DefaultValue: SoapUtils.getStrProp(e, "DefaultValue"), 304 | Ordinal: parseInt(SoapUtils.getStrProp(e, "Ordinal") || "0") 305 | } as DataextensionColumn; 306 | }); 307 | } 308 | } as SoapRequestConfig); 309 | 310 | return columns.sort((a: DataextensionColumn, b: DataextensionColumn) => { return a.Ordinal - b.Ordinal }); 311 | } 312 | 313 | private async getDirectoryId(uri: FolderManagerUri): Promise { 314 | if (this.directoriesCache.get(uri.globalPath) !== undefined) { 315 | return this.directoriesCache.get(uri.globalPath) || 0; 316 | } 317 | 318 | if (uri.localPath === '') { 319 | return this.getRootDirectoryId(uri); 320 | } 321 | 322 | if (uri.parent !== undefined) { 323 | const parentDirectoryId = await this.getDirectoryId(uri.parent); 324 | const subdirectories: Array = await this.getSubdirectoriesByDirectoryId(uri, parentDirectoryId); 325 | 326 | for (const d of subdirectories) { 327 | if (d.name === uri.name) { 328 | return d.id; 329 | } 330 | } 331 | } 332 | 333 | throw new Error(`Path not found: ${uri.globalPath}`); 334 | } 335 | 336 | private async findDirectories(uri: FolderManagerUri, filter: any): Promise> { 337 | const hasTokenScopes = await ConnectionController.getInstance().hasTokenRequiredScopes( 338 | uri.connectionId, 339 | ['data_extensions_read', 'data_extensions_write'] 340 | ); 341 | 342 | if (!hasTokenScopes) { 343 | Utils.getInstance().sendTelemetryEvent("error.manager-dataextensions.missing_api_scope", true, true); 344 | throw new Error('Additional permissions are required for this function: DATA => Data Extensions (Read, Write). Please update your MC installed package and restart VSCode'); 345 | } 346 | 347 | const data = await ConnectionController.getInstance().soapRequest(uri.connectionId, { 348 | operation: SoapOperation.RETRIEVE, 349 | body: SoapUtils.createRetrieveBody( 350 | "DataFolder", 351 | ["ID", "Name", "ParentFolder.ID"], 352 | filter 353 | ), 354 | transformResponse: (body) => { 355 | return SoapUtils.getArrProp(body, "RetrieveResponseMsg.Results").map((e: any) => { 356 | return { 357 | id: parseInt(SoapUtils.getStrProp(e, "ID")), 358 | parentId: parseInt(SoapUtils.getStrProp(e, "ParentFolder.ID")), 359 | name: SoapUtils.getStrProp(e, "Name") 360 | } as Directory 361 | }); 362 | } 363 | } as SoapRequestConfig); 364 | 365 | if (data === undefined) return []; 366 | 367 | return data as Array 368 | } 369 | 370 | private async getSubdirectoriesByDirectoryId(uri: FolderManagerUri, directoryId: number): Promise> { 371 | const data = await this.findDirectories(uri, { 372 | LeftOperand: { 373 | Property: "ParentFolder.ID", 374 | SimpleOperator: "equals", 375 | Value: directoryId 376 | }, 377 | LogicalOperator: "AND", 378 | RightOperand: { 379 | Property: "ContentType", 380 | SimpleOperator: "equals", 381 | Value: this.folderContentType 382 | } 383 | }); 384 | 385 | if (directoryId !== 0) { 386 | for (const d of data as Array) { 387 | this.directoriesCache.set(uri.getChildPath(d.name), d.id); 388 | } 389 | } 390 | 391 | return data; 392 | } 393 | 394 | private async getRootDirectoryId(uri: FolderManagerUri): Promise { 395 | if (this.directoriesCache.get(uri.mountPath) !== undefined) { 396 | return this.directoriesCache.get(uri.mountPath) || 0; 397 | } 398 | 399 | const subdirectories: Array = await this.findDirectories(uri, { 400 | LeftOperand: { 401 | Property: "CustomerKey", 402 | SimpleOperator: "equals", 403 | Value: this.rootFolderCustomerKey 404 | }, 405 | LogicalOperator: "AND", 406 | RightOperand: { 407 | Property: "ContentType", 408 | SimpleOperator: "equals", 409 | Value: this.folderContentType 410 | } 411 | }); 412 | 413 | /** Access to shared DEs in only possible through the Ent BU */ 414 | if(this.folderContentType == "shared_dataextension" && subdirectories && subdirectories.length == 0){ 415 | throw new Error("Connect to the Enterprise Business Unit (top level) to get access to shared Dataextensions"); 416 | } 417 | 418 | if(!subdirectories || subdirectories.length == 0){ 419 | throw new Error("Can't find the root Dataextensions folder"); 420 | } 421 | 422 | this.directoriesCache.set(uri.globalPath, subdirectories[0].id); 423 | 424 | return subdirectories[0].id; 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /connection-manager/js/app.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"js/app.js","mappings":"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","sources":["webpack://mcfs-connection-manager/./src/App.vue?83ed","webpack://mcfs-connection-manager/./src/components/ConnectionList.vue?3056","webpack://mcfs-connection-manager/src/components/ConnectionList.vue","webpack://mcfs-connection-manager/./src/components/ConnectionList.vue?728c","webpack://mcfs-connection-manager/./src/components/ConnectionList.vue","webpack://mcfs-connection-manager/src/App.vue","webpack://mcfs-connection-manager/./src/App.vue?facb","webpack://mcfs-connection-manager/./src/App.vue","webpack://mcfs-connection-manager/./src/main.js","webpack://mcfs-connection-manager/webpack/bootstrap","webpack://mcfs-connection-manager/webpack/runtime/chunk loaded","webpack://mcfs-connection-manager/webpack/runtime/define property getters","webpack://mcfs-connection-manager/webpack/runtime/global","webpack://mcfs-connection-manager/webpack/runtime/hasOwnProperty shorthand","webpack://mcfs-connection-manager/webpack/runtime/jsonp chunk loading","webpack://mcfs-connection-manager/webpack/startup"],"sourcesContent":["var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"app\"}},[_c('ConnectionList',{attrs:{\"connections\":_vm.connections},on:{\"connect\":function($event){return _vm.connect($event)},\"save\":function($event){return _vm.save($event)}}}),_c('Help')],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"hello\"},[_c('h2',[_vm._v(\"Connections list\")]),_c('div',{staticStyle:{\"margin-bottom\":\"10px\"}},[_c('button',{on:{\"click\":function($event){return _vm.add()}}},[_vm._v(\"NEW CONNECTION\")]),_c('button',{on:{\"click\":function($event){return _vm.save()}}},[_vm._v(\"SAVE CHANGES\")])]),_c('table',{staticClass:\"connections\",attrs:{\"cellpadding\":\"0\",\"cellspacing\":\"0\",\"border\":\"0\"}},[_vm._m(0),_c('tbody',_vm._l((this.localConnections),function(c,i){return _c('tr',{key:i},[_c('td',[_c('button',{ref:\"btn_connect\",refInFor:true,on:{\"click\":function($event){return _vm.connect(c, i)}}},[_vm._v(\"CONNECT\")])]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.name),expression:\"c.name\"}],attrs:{\"type\":\"text\",\"placeholder\":\"connection name\"},domProps:{\"value\":(c.name)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"name\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.account_id),expression:\"c.account_id\"}],attrs:{\"type\":\"text\",\"placeholder\":\"business unit id\"},domProps:{\"value\":(c.account_id)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"account_id\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.authBaseUri),expression:\"c.authBaseUri\"}],attrs:{\"type\":\"text\",\"placeholder\":\"api auth base uri\"},domProps:{\"value\":(c.authBaseUri)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"authBaseUri\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.client_id),expression:\"c.client_id\"}],attrs:{\"type\":\"password\",\"placeholder\":\"client id\"},domProps:{\"value\":(c.client_id)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"client_id\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.client_secret),expression:\"c.client_secret\"}],attrs:{\"type\":\"password\",\"placeholder\":\"client secret\"},domProps:{\"value\":(c.client_secret)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"client_secret\", $event.target.value)}}})]),_c('td',[_c('button',{staticClass:\"delete\",on:{\"click\":function($event){return _vm.remove(i)}}},[_vm._v(\"✕\")])])])}),0)])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('thead',[_c('tr',[_c('th',{attrs:{\"width\":\"100\"}}),_c('th',{attrs:{\"width\":\"100\"}},[_vm._v(\"Label\")]),_c('th',{attrs:{\"width\":\"100\"}},[_vm._v(\"MID\")]),_c('th',[_vm._v(\"Auth Base Uri\")]),_c('th',{attrs:{\"width\":\"200\"}},[_vm._v(\"Client ID\")]),_c('th',{attrs:{\"width\":\"200\"}},[_vm._v(\"Client Secret\")]),_c('th',{attrs:{\"width\":\"50§\"}})])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ConnectionList.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ConnectionList.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./ConnectionList.vue?vue&type=template&id=e7f38064&scoped=true&\"\nimport script from \"./ConnectionList.vue?vue&type=script&lang=js&\"\nexport * from \"./ConnectionList.vue?vue&type=script&lang=js&\"\nimport style0 from \"./ConnectionList.vue?vue&type=style&index=0&id=e7f38064&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"e7f38064\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./App.vue?vue&type=template&id=bf783f4c&\"\nimport script from \"./App.vue?vue&type=script&lang=js&\"\nexport * from \"./App.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import Vue from 'vue'\nimport App from './App.vue'\n\nVue.config.productionTip = false\n\nnew Vue({\n\trender: h => h(App),\n}).$mount('#app')","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","var deferred = [];\n__webpack_require__.O = function(result, chunkIds, fn, priority) {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar chunkIds = deferred[i][0];\n\t\tvar fn = deferred[i][1];\n\t\tvar priority = deferred[i][2];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t143: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = function(parentChunkLoadingFunction, data) {\n\tvar chunkIds = data[0];\n\tvar moreModules = data[1];\n\tvar runtime = data[2];\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunkmcfs_connection_manager\"] = self[\"webpackChunkmcfs_connection_manager\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [998], function() { return __webpack_require__(762); })\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["render","_vm","this","_h","$createElement","_c","_self","attrs","connections","on","$event","connect","save","staticRenderFns","staticClass","_v","staticStyle","add","_m","_l","c","i","key","ref","refInFor","directives","name","rawName","value","expression","domProps","hasChanges","target","composing","$set","remove","props","data","localConnections","methods","account_id","authBaseUri","client_id","client_secret","setTimeout","watch","components","component","vscode","action","content","onMessageReceived","mounted","postMessage","console","window","ConnectionList","Vue","h","App","$mount","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","m","deferred","O","result","chunkIds","fn","priority","notFulfilled","Infinity","length","fulfilled","j","Object","keys","every","splice","r","d","definition","o","defineProperty","enumerable","get","g","globalThis","Function","e","obj","prop","prototype","hasOwnProperty","call","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","some","id","chunkLoadingGlobal","self","forEach","bind","push","__webpack_exports__"],"sourceRoot":""} -------------------------------------------------------------------------------- /connection-manager/js/app-legacy.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"js/app-legacy.js","mappings":"kGAAIA,EAAS,WAAa,IAAIC,EAAIC,KAASC,EAAGF,EAAIG,eAAmBC,EAAGJ,EAAIK,MAAMD,IAAIF,EAAG,OAAOE,EAAG,MAAM,CAACE,MAAM,CAAC,GAAK,QAAQ,CAACF,EAAG,iBAAiB,CAACE,MAAM,CAAC,YAAcN,EAAIO,aAAaC,GAAG,CAAC,QAAU,SAASC,GAAQ,OAAOT,EAAIU,QAAQD,IAAS,KAAO,SAASA,GAAQ,OAAOT,EAAIW,KAAKF,OAAYL,EAAG,SAAS,IACjTQ,EAAkB,GCDlB,EAAS,WAAa,IAAIZ,EAAIC,KAASC,EAAGF,EAAIG,eAAmBC,EAAGJ,EAAIK,MAAMD,IAAIF,EAAG,OAAOE,EAAG,MAAM,CAACS,YAAY,SAAS,CAACT,EAAG,KAAK,CAACJ,EAAIc,GAAG,sBAAsBV,EAAG,MAAM,CAACW,YAAY,CAAC,gBAAgB,SAAS,CAACX,EAAG,SAAS,CAACI,GAAG,CAAC,MAAQ,SAASC,GAAQ,OAAOT,EAAIgB,SAAS,CAAChB,EAAIc,GAAG,oBAAoBV,EAAG,SAAS,CAACI,GAAG,CAAC,MAAQ,SAASC,GAAQ,OAAOT,EAAIW,UAAU,CAACX,EAAIc,GAAG,oBAAoBV,EAAG,QAAQ,CAACS,YAAY,cAAcP,MAAM,CAAC,YAAc,IAAI,YAAc,IAAI,OAAS,MAAM,CAACN,EAAIiB,GAAG,GAAGb,EAAG,QAAQJ,EAAIkB,GAAIjB,KAAqB,kBAAE,SAASkB,EAAEC,GAAG,OAAOhB,EAAG,KAAK,CAACiB,IAAID,GAAG,CAAChB,EAAG,KAAK,CAACA,EAAG,SAAS,CAACkB,IAAI,cAAcC,UAAS,EAAKf,GAAG,CAAC,MAAQ,SAASC,GAAQ,OAAOT,EAAIU,QAAQS,EAAGC,MAAM,CAACpB,EAAIc,GAAG,eAAeV,EAAG,KAAK,CAACA,EAAG,QAAQ,CAACoB,WAAW,CAAC,CAACC,KAAK,QAAQC,QAAQ,UAAUC,MAAOR,EAAM,KAAES,WAAW,WAAWtB,MAAM,CAAC,KAAO,OAAO,YAAc,mBAAmBuB,SAAS,CAAC,MAASV,EAAM,MAAGX,GAAG,CAAC,OAAS,SAASC,GAAQT,EAAI8B,YAAa,GAAM,MAAQ,SAASrB,GAAWA,EAAOsB,OAAOC,WAAqBhC,EAAIiC,KAAKd,EAAG,OAAQV,EAAOsB,OAAOJ,aAAavB,EAAG,KAAK,CAACA,EAAG,QAAQ,CAACoB,WAAW,CAAC,CAACC,KAAK,QAAQC,QAAQ,UAAUC,MAAOR,EAAY,WAAES,WAAW,iBAAiBtB,MAAM,CAAC,KAAO,OAAO,YAAc,oBAAoBuB,SAAS,CAAC,MAASV,EAAY,YAAGX,GAAG,CAAC,OAAS,SAASC,GAAQT,EAAI8B,YAAa,GAAM,MAAQ,SAASrB,GAAWA,EAAOsB,OAAOC,WAAqBhC,EAAIiC,KAAKd,EAAG,aAAcV,EAAOsB,OAAOJ,aAAavB,EAAG,KAAK,CAACA,EAAG,QAAQ,CAACoB,WAAW,CAAC,CAACC,KAAK,QAAQC,QAAQ,UAAUC,MAAOR,EAAa,YAAES,WAAW,kBAAkBtB,MAAM,CAAC,KAAO,OAAO,YAAc,qBAAqBuB,SAAS,CAAC,MAASV,EAAa,aAAGX,GAAG,CAAC,OAAS,SAASC,GAAQT,EAAI8B,YAAa,GAAM,MAAQ,SAASrB,GAAWA,EAAOsB,OAAOC,WAAqBhC,EAAIiC,KAAKd,EAAG,cAAeV,EAAOsB,OAAOJ,aAAavB,EAAG,KAAK,CAACA,EAAG,QAAQ,CAACoB,WAAW,CAAC,CAACC,KAAK,QAAQC,QAAQ,UAAUC,MAAOR,EAAW,UAAES,WAAW,gBAAgBtB,MAAM,CAAC,KAAO,WAAW,YAAc,aAAauB,SAAS,CAAC,MAASV,EAAW,WAAGX,GAAG,CAAC,OAAS,SAASC,GAAQT,EAAI8B,YAAa,GAAM,MAAQ,SAASrB,GAAWA,EAAOsB,OAAOC,WAAqBhC,EAAIiC,KAAKd,EAAG,YAAaV,EAAOsB,OAAOJ,aAAavB,EAAG,KAAK,CAACA,EAAG,QAAQ,CAACoB,WAAW,CAAC,CAACC,KAAK,QAAQC,QAAQ,UAAUC,MAAOR,EAAe,cAAES,WAAW,oBAAoBtB,MAAM,CAAC,KAAO,WAAW,YAAc,iBAAiBuB,SAAS,CAAC,MAASV,EAAe,eAAGX,GAAG,CAAC,OAAS,SAASC,GAAQT,EAAI8B,YAAa,GAAM,MAAQ,SAASrB,GAAWA,EAAOsB,OAAOC,WAAqBhC,EAAIiC,KAAKd,EAAG,gBAAiBV,EAAOsB,OAAOJ,aAAavB,EAAG,KAAK,CAACA,EAAG,SAAS,CAACS,YAAY,SAASL,GAAG,CAAC,MAAQ,SAASC,GAAQ,OAAOT,EAAIkC,OAAOd,MAAM,CAACpB,EAAIc,GAAG,cAAa,QACpnF,EAAkB,CAAC,WAAa,IAAId,EAAIC,KAASC,EAAGF,EAAIG,eAAmBC,EAAGJ,EAAIK,MAAMD,IAAIF,EAAG,OAAOE,EAAG,QAAQ,CAACA,EAAG,KAAK,CAACA,EAAG,KAAK,CAACE,MAAM,CAAC,MAAQ,SAASF,EAAG,KAAK,CAACE,MAAM,CAAC,MAAQ,QAAQ,CAACN,EAAIc,GAAG,WAAWV,EAAG,KAAK,CAACE,MAAM,CAAC,MAAQ,QAAQ,CAACN,EAAIc,GAAG,SAASV,EAAG,KAAK,CAACJ,EAAIc,GAAG,mBAAmBV,EAAG,KAAK,CAACE,MAAM,CAAC,MAAQ,QAAQ,CAACN,EAAIc,GAAG,eAAeV,EAAG,KAAK,CAACE,MAAM,CAAC,MAAQ,QAAQ,CAACN,EAAIc,GAAG,mBAAmBV,EAAG,KAAK,CAACE,MAAM,CAAC,MAAQ,eC2Ejb,G,eAAA,CACAmB,KAAAA,iBACAU,MAAAA,CACA5B,YAAAA,OAEA6B,KAAAA,WACA,OACAC,iBAAAA,GACAP,YAAAA,IAGAQ,QAAAA,CACAtB,IAAAA,WACA,mBACA,4BACAS,KAAAA,kBAAAA,KAAAA,iBAAAA,OAAAA,GACAc,WAAAA,GACAC,YAAAA,GACAC,UAAAA,GACAC,cAAAA,MAIAR,OAAAA,SAAAA,GAEA,mCAGAxB,QAAAA,SAAAA,EAAAA,GAAA,WAEA,wBACA,kCAEA,0DACA,iDAGA,iBACA,YAGAiC,YAAAA,WACA,uBACA,MAGAhC,KAAAA,WACA,mBACA,2CAGAiC,MAAAA,CACArC,YAAAA,SAAAA,GACA,mCAIAsC,WAAAA,KCrI0Q,I,UCQtQC,GAAY,OACd,EACA,EACA,GACA,EACA,KACA,WACA,MAIF,EAAeA,EAAiB,QCLhC,GACArB,KAAAA,MACAW,KAAAA,WACA,OACAW,OAAAA,KACAxC,YAAAA,KAGA+B,QAAAA,CACA3B,KAAAA,SAAAA,GACA,mBAEA,yBACAqC,OAAAA,SACAC,QAAAA,KAIAvC,QAAAA,SAAAA,GACA,yBACAsC,OAAAA,UACAC,QAAAA,KAIAC,kBAAAA,SAAAA,GACA,sBACA,kBACA,gCACA,MACA,QACA,SAIAC,QAAAA,WAAA,WAEA,qCACA,+BAEA,aACAC,YAAAA,SAAAA,GACAC,QAAAA,IAAAA,cAAAA,KAMA,yBACAL,OAAAA,iBAGAM,OAAAA,iBAAAA,WAAAA,SAAAA,GACA,2BAGAT,WAAAA,CACAU,eAAAA,ICvEsP,ICOlP,GAAY,OACd,EACAxD,EACAa,GACA,EACA,KACA,KACA,MAIF,EAAe,EAAiB,QCfhC4C,EAAAA,EAAAA,OAAAA,eAA2B,EAE3B,IAAIA,EAAAA,EAAI,CACPzD,OAAQ,SAAA0D,GAAC,OAAIA,EAAEC,MACbC,OAAO,UCNNC,EAA2B,GAG/B,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBE,IAAjBD,EACH,OAAOA,EAAaE,QAGrB,IAAIC,EAASN,EAAyBE,GAAY,CAGjDG,QAAS,IAOV,OAHAE,EAAoBL,GAAUI,EAAQA,EAAOD,QAASJ,GAG/CK,EAAOD,QAIfJ,EAAoBO,EAAID,E,WCzBxB,IAAIE,EAAW,GACfR,EAAoBS,EAAI,SAASC,EAAQC,EAAUC,EAAIC,GACtD,IAAGF,EAAH,CAMA,IAAIG,EAAeC,IACnB,IAASxD,EAAI,EAAGA,EAAIiD,EAASQ,OAAQzD,IAAK,CACrCoD,EAAWH,EAASjD,GAAG,GACvBqD,EAAKJ,EAASjD,GAAG,GACjBsD,EAAWL,EAASjD,GAAG,GAE3B,IAJA,IAGI0D,GAAY,EACPC,EAAI,EAAGA,EAAIP,EAASK,OAAQE,MACpB,EAAXL,GAAsBC,GAAgBD,IAAaM,OAAOC,KAAKpB,EAAoBS,GAAGY,OAAM,SAAS7D,GAAO,OAAOwC,EAAoBS,EAAEjD,GAAKmD,EAASO,OAC3JP,EAASW,OAAOJ,IAAK,IAErBD,GAAY,EACTJ,EAAWC,IAAcA,EAAeD,IAG7C,GAAGI,EAAW,CACbT,EAASc,OAAO/D,IAAK,GACrB,IAAIgE,EAAIX,SACET,IAANoB,IAAiBb,EAASa,IAGhC,OAAOb,EAzBNG,EAAWA,GAAY,EACvB,IAAI,IAAItD,EAAIiD,EAASQ,OAAQzD,EAAI,GAAKiD,EAASjD,EAAI,GAAG,GAAKsD,EAAUtD,IAAKiD,EAASjD,GAAKiD,EAASjD,EAAI,GACrGiD,EAASjD,GAAK,CAACoD,EAAUC,EAAIC,I,cCJ/Bb,EAAoBwB,EAAI,SAASpB,EAASqB,GACzC,IAAI,IAAIjE,KAAOiE,EACXzB,EAAoB0B,EAAED,EAAYjE,KAASwC,EAAoB0B,EAAEtB,EAAS5C,IAC5E2D,OAAOQ,eAAevB,EAAS5C,EAAK,CAAEoE,YAAY,EAAMC,IAAKJ,EAAWjE,M,cCJ3EwC,EAAoB8B,EAAI,WACvB,GAA0B,kBAAfC,WAAyB,OAAOA,WAC3C,IACC,OAAO3F,MAAQ,IAAI4F,SAAS,cAAb,GACd,MAAOC,GACR,GAAsB,kBAAXxC,OAAqB,OAAOA,QALjB,G,cCAxBO,EAAoB0B,EAAI,SAASQ,EAAKC,GAAQ,OAAOhB,OAAOiB,UAAUC,eAAeC,KAAKJ,EAAKC,I,cCK/F,IAAII,EAAkB,CACrB,IAAK,GAaNvC,EAAoBS,EAAES,EAAI,SAASsB,GAAW,OAAoC,IAA7BD,EAAgBC,IAGrE,IAAIC,EAAuB,SAASC,EAA4BnE,GAC/D,IAKI0B,EAAUuC,EALV7B,EAAWpC,EAAK,GAChBoE,EAAcpE,EAAK,GACnBqE,EAAUrE,EAAK,GAGIhB,EAAI,EAC3B,GAAGoD,EAASkC,MAAK,SAASC,GAAM,OAA+B,IAAxBP,EAAgBO,MAAe,CACrE,IAAI7C,KAAY0C,EACZ3C,EAAoB0B,EAAEiB,EAAa1C,KACrCD,EAAoBO,EAAEN,GAAY0C,EAAY1C,IAGhD,GAAG2C,EAAS,IAAIlC,EAASkC,EAAQ5C,GAGlC,IADG0C,GAA4BA,EAA2BnE,GACrDhB,EAAIoD,EAASK,OAAQzD,IACzBiF,EAAU7B,EAASpD,GAChByC,EAAoB0B,EAAEa,EAAiBC,IAAYD,EAAgBC,IACrED,EAAgBC,GAAS,KAE1BD,EAAgBC,GAAW,EAE5B,OAAOxC,EAAoBS,EAAEC,IAG1BqC,EAAqBC,KAAK,uCAAyCA,KAAK,wCAA0C,GACtHD,EAAmBE,QAAQR,EAAqBS,KAAK,KAAM,IAC3DH,EAAmBI,KAAOV,EAAqBS,KAAK,KAAMH,EAAmBI,KAAKD,KAAKH,I,GC/CvF,IAAIK,EAAsBpD,EAAoBS,OAAEN,EAAW,CAAC,MAAM,WAAa,OAAOH,EAAoB,SAC1GoD,EAAsBpD,EAAoBS,EAAE2C,I","sources":["webpack://mcfs-connection-manager/./src/App.vue?83ed","webpack://mcfs-connection-manager/./src/components/ConnectionList.vue?3056","webpack://mcfs-connection-manager/src/components/ConnectionList.vue","webpack://mcfs-connection-manager/./src/components/ConnectionList.vue?728c","webpack://mcfs-connection-manager/./src/components/ConnectionList.vue","webpack://mcfs-connection-manager/src/App.vue","webpack://mcfs-connection-manager/./src/App.vue?facb","webpack://mcfs-connection-manager/./src/App.vue","webpack://mcfs-connection-manager/./src/main.js","webpack://mcfs-connection-manager/webpack/bootstrap","webpack://mcfs-connection-manager/webpack/runtime/chunk loaded","webpack://mcfs-connection-manager/webpack/runtime/define property getters","webpack://mcfs-connection-manager/webpack/runtime/global","webpack://mcfs-connection-manager/webpack/runtime/hasOwnProperty shorthand","webpack://mcfs-connection-manager/webpack/runtime/jsonp chunk loading","webpack://mcfs-connection-manager/webpack/startup"],"sourcesContent":["var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"app\"}},[_c('ConnectionList',{attrs:{\"connections\":_vm.connections},on:{\"connect\":function($event){return _vm.connect($event)},\"save\":function($event){return _vm.save($event)}}}),_c('Help')],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"hello\"},[_c('h2',[_vm._v(\"Connections list\")]),_c('div',{staticStyle:{\"margin-bottom\":\"10px\"}},[_c('button',{on:{\"click\":function($event){return _vm.add()}}},[_vm._v(\"NEW CONNECTION\")]),_c('button',{on:{\"click\":function($event){return _vm.save()}}},[_vm._v(\"SAVE CHANGES\")])]),_c('table',{staticClass:\"connections\",attrs:{\"cellpadding\":\"0\",\"cellspacing\":\"0\",\"border\":\"0\"}},[_vm._m(0),_c('tbody',_vm._l((this.localConnections),function(c,i){return _c('tr',{key:i},[_c('td',[_c('button',{ref:\"btn_connect\",refInFor:true,on:{\"click\":function($event){return _vm.connect(c, i)}}},[_vm._v(\"CONNECT\")])]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.name),expression:\"c.name\"}],attrs:{\"type\":\"text\",\"placeholder\":\"connection name\"},domProps:{\"value\":(c.name)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"name\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.account_id),expression:\"c.account_id\"}],attrs:{\"type\":\"text\",\"placeholder\":\"business unit id\"},domProps:{\"value\":(c.account_id)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"account_id\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.authBaseUri),expression:\"c.authBaseUri\"}],attrs:{\"type\":\"text\",\"placeholder\":\"api auth base uri\"},domProps:{\"value\":(c.authBaseUri)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"authBaseUri\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.client_id),expression:\"c.client_id\"}],attrs:{\"type\":\"password\",\"placeholder\":\"client id\"},domProps:{\"value\":(c.client_id)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"client_id\", $event.target.value)}}})]),_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(c.client_secret),expression:\"c.client_secret\"}],attrs:{\"type\":\"password\",\"placeholder\":\"client secret\"},domProps:{\"value\":(c.client_secret)},on:{\"change\":function($event){_vm.hasChanges = true},\"input\":function($event){if($event.target.composing){ return; }_vm.$set(c, \"client_secret\", $event.target.value)}}})]),_c('td',[_c('button',{staticClass:\"delete\",on:{\"click\":function($event){return _vm.remove(i)}}},[_vm._v(\"✕\")])])])}),0)])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('thead',[_c('tr',[_c('th',{attrs:{\"width\":\"100\"}}),_c('th',{attrs:{\"width\":\"100\"}},[_vm._v(\"Label\")]),_c('th',{attrs:{\"width\":\"100\"}},[_vm._v(\"MID\")]),_c('th',[_vm._v(\"Auth Base Uri\")]),_c('th',{attrs:{\"width\":\"200\"}},[_vm._v(\"Client ID\")]),_c('th',{attrs:{\"width\":\"200\"}},[_vm._v(\"Client Secret\")]),_c('th',{attrs:{\"width\":\"50§\"}})])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n\n\n","import mod from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ConnectionList.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./ConnectionList.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./ConnectionList.vue?vue&type=template&id=e7f38064&scoped=true&\"\nimport script from \"./ConnectionList.vue?vue&type=script&lang=js&\"\nexport * from \"./ConnectionList.vue?vue&type=script&lang=js&\"\nimport style0 from \"./ConnectionList.vue?vue&type=style&index=0&id=e7f38064&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"e7f38064\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../node_modules/thread-loader/dist/cjs.js!../node_modules/babel-loader/lib/index.js??clonedRuleSet-40[0].rules[0].use[1]!../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./App.vue?vue&type=template&id=bf783f4c&\"\nimport script from \"./App.vue?vue&type=script&lang=js&\"\nexport * from \"./App.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import Vue from 'vue'\nimport App from './App.vue'\n\nVue.config.productionTip = false\n\nnew Vue({\n\trender: h => h(App),\n}).$mount('#app')","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","var deferred = [];\n__webpack_require__.O = function(result, chunkIds, fn, priority) {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar chunkIds = deferred[i][0];\n\t\tvar fn = deferred[i][1];\n\t\tvar priority = deferred[i][2];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t143: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = function(parentChunkLoadingFunction, data) {\n\tvar chunkIds = data[0];\n\tvar moreModules = data[1];\n\tvar runtime = data[2];\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunkmcfs_connection_manager\"] = self[\"webpackChunkmcfs_connection_manager\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [998], function() { return __webpack_require__(3762); })\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["render","_vm","this","_h","$createElement","_c","_self","attrs","connections","on","$event","connect","save","staticRenderFns","staticClass","_v","staticStyle","add","_m","_l","c","i","key","ref","refInFor","directives","name","rawName","value","expression","domProps","hasChanges","target","composing","$set","remove","props","data","localConnections","methods","account_id","authBaseUri","client_id","client_secret","setTimeout","watch","components","component","vscode","action","content","onMessageReceived","mounted","postMessage","console","window","ConnectionList","Vue","h","App","$mount","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","undefined","exports","module","__webpack_modules__","m","deferred","O","result","chunkIds","fn","priority","notFulfilled","Infinity","length","fulfilled","j","Object","keys","every","splice","r","d","definition","o","defineProperty","enumerable","get","g","globalThis","Function","e","obj","prop","prototype","hasOwnProperty","call","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","some","id","chunkLoadingGlobal","self","forEach","bind","push","__webpack_exports__"],"sourceRoot":""} --------------------------------------------------------------------------------