Finish migration to V2.
|
@ -1,5 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -2,3 +2,4 @@
|
|||
debug.log
|
||||
.editorconfig
|
||||
node_modules
|
||||
.vscode
|
||||
|
|
246
README.md
|
@ -1,41 +1,235 @@
|
|||
# Visual Studio Team Services App for Zendesk
|
||||
*Use of this software is subject to important terms and conditions as set forth in the License file*
|
||||
|
||||
> Get the latest version of the app: [Download v0.5.0](https://github.com/Microsoft/vsts-zendesk-app/releases/download/v0.5.0/vsts-zendesk-app-0.5.0.zip)
|
||||
# App Scaffold
|
||||
|
||||
Unite your customer support and development teams. Quickly create or link work items to tickets, enable efficient two-way communication, and stop using email to check status.
|
||||
## Description
|
||||
This repo contains a scaffold to help developers build [apps for Zendesk products](https://developer.zendesk.com/apps/docs/apps-v2/getting_started).
|
||||
|
||||
### Create work items for your engineers right from Zendesk
|
||||
## Getting Started
|
||||
|
||||
With the Visual Studio Team Services app for Zendesk, users in Zendesk can quickly create a new work item from a Zendesk ticket.
|
||||
### Dependencies
|
||||
- [Node.js](https://nodejs.org/en/) >= 6.3.x
|
||||
- [Ruby](https://www.ruby-lang.org/) >= 2.0.x
|
||||
|
||||
![img](https://i3-vso.sec.s-msft.com/dynimg/IC729561.png)
|
||||
### Setup
|
||||
1. Clone or fork this repo
|
||||
2. Change (`cd`) into the `app_scaffold` directory
|
||||
3. Run `npm install`
|
||||
|
||||
### Get instant access to the status of linked work items
|
||||
To run your app locally in Zendesk, you need the [Zendesk Apps Tools (ZAT)](https://github.com/zendesk/zendesk_apps_tools).
|
||||
|
||||
Give your customer support team easy access to the information they need. See details about work items linked to a Zendesk ticket.
|
||||
You'll also need to run a couple of command-line Node.js-based tools that are installed using `npm`. For a node module to be available from the command-line, it must be installed globally.
|
||||
|
||||
![img](https://ms-vsts.gallery.vsassets.io/_apis/public/gallery/publisher/ms-vsts/extension/services-zendesk/latest/assetbyname/images/zendesk-linked.png)
|
||||
To setup these and other dependencies, run these commands:
|
||||
|
||||
## How to install and setup
|
||||
```
|
||||
gem install zendesk_apps_tools
|
||||
npm install --global webpack foreman karma-cli
|
||||
```
|
||||
|
||||
### Install the app to Zendesk
|
||||
Note: Foreman was originally created as a Ruby tool. If you prefer, you can install it by `gem install foreman` instead.
|
||||
|
||||
1. Download the latest release .zip file: **[version 0.5.0](https://github.com/Microsoft/vsts-zendesk-app/releases/download/v0.5.0/vsts-zendesk-app-0.5.0.zip)**
|
||||
1. From Zendesk, click the settings icon (gear)
|
||||
1. Under **Apps** click Manage.
|
||||
1. Click **Upload private app**
|
||||
1. Give the app a name.
|
||||
1. Browse to the location you saved the .zip release and select it.
|
||||
1. Provide your Visual Studio Team Services name and decide on a work item tag for Zendesk.
|
||||
### Running locally
|
||||
|
||||
See [full instructions](https://www.visualstudio.com/docs/marketplace/integrate/service-hooks/services/zendesk)
|
||||
_Note: The App Scaffold currently depends on zat v1.35.12 or greater._
|
||||
|
||||
### Send updates from Visual Studio Team Services to Zendesk
|
||||
Foreman allows you to easily run multiple processes in one tab. One process is `zat server --path=./dist`, which serves the app in a way that can be run in a supported Zendesk product. The second is `webpack --watch` to rebuild the project whenever you save changes to a source file.
|
||||
|
||||
1. Open the admin page for the team project in Visual Studio Team Services
|
||||
2. On the *Service Hooks* tab, run the subscription wizard
|
||||
3. Select Zendesk from the subscription wizard
|
||||
4. Pick and the Visual Studio Team Services event which will post to Zendesk
|
||||
5. Tell Zendesk what to do when the event occurs
|
||||
6. Test the service hook subscription and finish the wizard
|
||||
To run these processes, run
|
||||
|
||||
```
|
||||
nf start
|
||||
```
|
||||
|
||||
or run the individual commands from the Procfile in separate terminals.
|
||||
|
||||
Note: If you installed the Ruby version of foreman, you'll need to use `foreman start`.
|
||||
|
||||
## But why?
|
||||
The App Scaffold includes many features to help you maintain and scale your app. Some of the features provided by the App Scaffold are listed below. However, you don't need prior experience in any of these to be able to use the scaffold successfully.
|
||||
|
||||
- [ES6 (ES2015)](https://babeljs.io/docs/learn-es2015/)
|
||||
|
||||
ECMAScript 6, also known as ECMAScript 2015, is the latest version of the ECMAScript standard. The App Scaffold includes the [Babel compiler](https://babeljs.io/) to transpile your code to ES5. This allows you to use ES6 features, such as classes, arrow functions and template strings even in browsers that haven't fully implemented these features.
|
||||
|
||||
- [Handlebars](http://handlebarsjs.com/) templates
|
||||
|
||||
Handlebars is a powerful templating library that lets you build semantic templates for your app with minimal logic.
|
||||
|
||||
- [SASS](http://sass-lang.com/) stylesheets
|
||||
|
||||
Sass is an extension of CSS that adds power and elegance to the basic language. It allows you to use variables, nested rules, mixins, inline imports, and more.
|
||||
|
||||
- [Webpack](https://webpack.github.io/) module bundler
|
||||
|
||||
Webpack compiles web browser applications. It allows splitting your source code into modules and re-use them with require and import statements. It also allows splitting your compiled project into separate files that are loaded on demand.
|
||||
|
||||
- [Karma](http://karma-runner.github.io/) test runner
|
||||
|
||||
The main goal for Karma is to bring a productive testing environment to developers with minimal configuration.
|
||||
|
||||
- [Jasmine](https://jasmine.github.io/) testing framework
|
||||
|
||||
Jasmine is a behavior-driven development framework for testing JavaScript code with a clean syntax.
|
||||
|
||||
## Folder structure
|
||||
|
||||
The folder and file structure of the App Scaffold is as follows:
|
||||
|
||||
| Name | Description |
|
||||
|:----------------------------------------|:---------------------------------------------------------------------------------------------|
|
||||
| [`dist/`](#dist) | The folder in which webpack packages the built version of your app |
|
||||
| [`lib/`](#lib) | The folder in which the shims and files that make the scaffold work live |
|
||||
| [`spec/`](#spec) | The folder in which all of your test files live |
|
||||
| [`src/`](#src) | The folder in which all of your source JavaScript, CSS, templates and translation files live |
|
||||
| [`.eslintrc`](#eslintrc) | Configuration file for JavaScript linting |
|
||||
| [`karma.conf.js`](#karmaconfjs) | Configuration file for the test runner |
|
||||
| [`package.json`](#packagejson) | Configuration file for build dependencies |
|
||||
| [`webpack.config.js`](#webpackconfigjs) | Configuration file that webpack uses to build your app |
|
||||
|
||||
#### dist
|
||||
The dist directory is the folder you will need to package when submitting your app to the marketplace. It is also the folder you will have to serve when using [ZAT](https://developer.zendesk.com/apps/docs/apps-v2/getting_started#zendesk-app-tools). It includes your app's manifest.json file, an assets folder with all your compiled JavaScript and CSS as well as HTML and images.
|
||||
|
||||
#### lib
|
||||
The lib directory is where the source code for the app shims and compatibility methods live. While you may modify or remove this code as required for your app, doing so is not recommended for beginners.
|
||||
|
||||
#### spec
|
||||
The spec directory is where all your tests and test helpers live. Tests are not required to submit/upload your app to Zendesk and your test files are not included in your app's package, however it is good practice to write tests to document functionality and prevent bugs.
|
||||
|
||||
#### src
|
||||
The src directory is where your raw source code lives. The App Scaffold includes different directories for JavaScript, stylesheets, templates and translations. Most of your additions will be in here (and spec, of course!).
|
||||
|
||||
#### .eslintrc
|
||||
.eslintrc is a configuration file for [ESLint](http://eslint.org). ESLint is a linting utility for JavaScript. For more information on how to configure ESLint, see [Configuring ESLint](http://eslint.org/docs/user-guide/configuring).
|
||||
|
||||
#### karma.conf.js
|
||||
karma.conf.js is a configuration file for [Karma](http://karma-runner.github.io). Karma is a JavaScript test runner. This file defines where your source and test files live. For more information on how to use this file, see [Karma - Configuration File](http://karma-runner.github.io/1.0/config/configuration-file.html).
|
||||
|
||||
#### package.json
|
||||
package.json is a configuration file for [NPM](https://www.npmjs.com). NPM is a package manager for JavaScript. This file includes information about your project and its dependencies. For more information on how to configure this file, see [package.json](https://docs.npmjs.com/files/package.json).
|
||||
|
||||
#### webpack.config.js
|
||||
webpack.config.js is a configuration file for [webpack](https://webpack.github.io/). Webpack is a JavaScript module bundler. For more information about webpack and how to configure it, see [What is webpack](http://webpack.github.io/docs/what-is-webpack.html).
|
||||
|
||||
## Initialization
|
||||
The App Scaffold's initialization code lives in [`src/index.js`](https://github.com/zendesk/app_scaffold/blob/master/src/javascripts/index.js). For more information, see [inline documentation](https://github.com/zendesk/app_scaffold/blob/master/src/javascripts/index.js).
|
||||
|
||||
## API Reference
|
||||
The App Scaffold provides some classes under `/lib` to help building apps.
|
||||
|
||||
### I18n
|
||||
The I18n (internationalization) module provides a `t` method and Handlebars helper to look up translations based on a key. For more information, see [Using the I18n module](https://github.com/zendesk/app_scaffold/blob/master/doc/i18n.md).
|
||||
|
||||
### Storage
|
||||
The Storage module provides helper methods to interact with `localStorage`. For more information, see [Using the Storage module](https://github.com/zendesk/app_scaffold/blob/master/doc/storage.md).
|
||||
|
||||
### View
|
||||
The View module provides methods to simplify rendering Handlebars templates located under the templates folder. For more information, see [Using the View module](https://github.com/zendesk/app_scaffold/blob/master/doc/view.md).
|
||||
|
||||
## Migrating from v1
|
||||
The master branch of this repo contains modules and sample code to help you migrate from a v1 app. For detailed documentation on how to migrate from a v1 app, see our [Migrating to v2](https://developer.zendesk.com/apps/docs/apps-v2/migrating) guide on the Zendesk Developer Portal.
|
||||
|
||||
## Starting from scratch
|
||||
If you're starting a v2 app from scratch you will need to check out the [from-scratch](https://github.com/zendesk/app_scaffold/tree/from-scratch) branch:
|
||||
|
||||
```
|
||||
git checkout from-scratch
|
||||
npm install
|
||||
```
|
||||
|
||||
The from-scratch branch uses up-to-date versions of the libraries included with the App Scaffold and also removes the shims needed when migrating from v1. It also includes sample code to help you get started on v2.
|
||||
|
||||
Another addition present only in the from-scratch branch, is the [Zendesk Garden](http://garden.zendesk.com/) stylesheet. Zendesk Garden is designed to be a common baseline of styles and components between all Zendesk products. For more information, see [Using the Zendesk Garden styles](https://developer.zendesk.com/apps/docs/apps-v2/setup#using-the-zendesk-garden-styles) in the Zendesk Developer Portal.
|
||||
|
||||
If you want to see the exact differences between the master and from-scratch branches click [here](https://github.com/zendesk/app_scaffold/compare/from-scratch).
|
||||
|
||||
## Parameters and Settings
|
||||
|
||||
If you need to test your app with a `parameters` section in `dist/manifest.json`, foreman might crash with a message like:
|
||||
|
||||
> Would have prompted for a value interactively, but zat is not listening to keyboard input.
|
||||
|
||||
To resolve this problem, set default values for parameters or create a `settings.yml` file in the root directory of your app scaffold-based project, and populate it with your parameter names and test values. For example, using a parameters section like:
|
||||
|
||||
```json
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "myParameter"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
create a `settings.yml` containing:
|
||||
|
||||
```yaml
|
||||
myParameter: 'some value!'
|
||||
```
|
||||
|
||||
If you prefer to manually input settings every time you run foreman, edit the Procfile to remove the `--unattended` option from the server command.
|
||||
|
||||
## Testing
|
||||
|
||||
The App Scaffold is currently setup for testing with [Jasmine](http://jasmine.github.io/) (testing framework) and [Karma](https://karma-runner.github.io) (test runner). To run specs, run
|
||||
|
||||
```
|
||||
karma start
|
||||
```
|
||||
|
||||
Specs live under the `spec` directory and can be configured by editing the `karma.conf.js` file.
|
||||
|
||||
## Deploying
|
||||
|
||||
To check that your app will pass the server-side validation check, run
|
||||
|
||||
```
|
||||
zat validate --path=./dist
|
||||
```
|
||||
|
||||
If validation is successful, you can upload the app into your Zendesk account by running
|
||||
|
||||
```
|
||||
zat create --path=dist
|
||||
```
|
||||
|
||||
To update your app after it has been created in your account, run
|
||||
|
||||
```
|
||||
zat update --path=dist
|
||||
```
|
||||
|
||||
Or, to create a zip archive for manual upload, run
|
||||
|
||||
```
|
||||
zat package --path=dist
|
||||
```
|
||||
|
||||
taking note of the created filename.
|
||||
|
||||
For more information on the Zendesk Apps Tools please see the [documentation](https://developer.zendesk.com/apps/docs/apps-v2/getting_started#zendesk-app-tools).
|
||||
|
||||
## External Dependencies
|
||||
External dependencies are defined in a module, [`lib/external_assets.js`](https://github.com/zendesk/app_scaffold/blob/master/lib/external_assets.js). The export of the module is imported into [`webpack.config.js`](https://github.com/zendesk/app_scaffold/blob/master/webpack.config.js) at build-time. This ensures these dependencies are included on your app's `index.html` as well as in the test suite.
|
||||
|
||||
## Contribute
|
||||
* Put up a PR into the master branch.
|
||||
* CC and get a +1 from @zendesk/vegemite.
|
||||
|
||||
## Bugs
|
||||
Submit Issues via [GitHub](https://github.com/zendesk/app_scaffold/issues/new) or email support@zendesk.com.
|
||||
|
||||
## Useful Links
|
||||
Links to maintaining team, confluence pages, Datadog dashboard, Kibana logs, etc
|
||||
- https://developer.zendesk.com/
|
||||
- https://github.com/zendesk/zendesk_apps_tools
|
||||
- https://webpack.github.io
|
||||
|
||||
## Copyright and license
|
||||
Copyright 2016 Zendesk
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
|
||||
<script type="text/javascript" src="https://assets.zendesk.com/apps/sdk/2.0/zaf_sdk.js"></script>
|
||||
|
||||
<script type="text/javascript" src="main.js"></script></body>
|
||||
<script type="text/javascript" src="app.js"></script></body>
|
||||
</html>
|
До Ширина: | Высота: | Размер: 3.0 KiB После Ширина: | Высота: | Размер: 3.0 KiB |
До Ширина: | Высота: | Размер: 9.0 KiB После Ширина: | Высота: | Размер: 9.0 KiB |
До Ширина: | Высота: | Размер: 705 B После Ширина: | Высота: | Размер: 705 B |
|
@ -0,0 +1,18 @@
|
|||
<!-- AUTOMATICALLY GENERATED FROM ./lib/templates/modal.hdbs - DO NOT MODIFY THIS FILE DIRECTLY -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/bootstrap/2.3.2/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link href="main.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<section data-main></section>
|
||||
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/g/lodash@2.4.2(lodash.underscore.min.js),handlebarsjs@1.3.0,jquery@2.2.4,momentjs@2.9.0,bootstrap@2.3.2"></script>
|
||||
|
||||
<script type="text/javascript" src="https://assets.zendesk.com/apps/sdk/2.0/zaf_sdk.js"></script>
|
||||
|
||||
<script type="text/javascript" src="modal.js"></script></body>
|
||||
</html>
|
|
@ -136,10 +136,10 @@ BaseApp.prototype = {
|
|||
},
|
||||
|
||||
// https://developer.zendesk.com/apps/docs/agent/requests#make-a-request
|
||||
ajax: function(name) {
|
||||
ajax: async function(name) {
|
||||
let req = this.requests[name],
|
||||
options = _.isFunction(req)
|
||||
? req.apply(this, Array.prototype.slice.call(arguments, 1))
|
||||
? await req.apply(this, Array.prototype.slice.call(arguments, 1))
|
||||
: req,
|
||||
dfd = $.Deferred(),
|
||||
app = this;
|
|
@ -0,0 +1,16 @@
|
|||
<!-- {{htmlWebpackPlugin.options.warning}} -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
{{#each htmlWebpackPlugin.options.vendorCss}}
|
||||
<link href="{{this}}" rel="stylesheet">
|
||||
{{/each}}
|
||||
</head>
|
||||
<body>
|
||||
<section data-main></section>
|
||||
{{#each htmlWebpackPlugin.options.vendorJs}}
|
||||
<script type="text/javascript" src="{{this}}"></script>
|
||||
{{/each}}
|
||||
</body>
|
||||
</html>
|
До Ширина: | Высота: | Размер: 5.3 KiB После Ширина: | Высота: | Размер: 5.3 KiB |
До Ширина: | Высота: | Размер: 24 KiB После Ширина: | Высота: | Размер: 24 KiB |
До Ширина: | Высота: | Размер: 6.3 KiB После Ширина: | Высота: | Размер: 6.3 KiB |
|
@ -0,0 +1,986 @@
|
|||
import ZAFClient from "zendesk_app_framework_sdk";
|
||||
import I18n from "i18n";
|
||||
import View from "view";
|
||||
import BaseApp from "base_app";
|
||||
import helpers from "helpers";
|
||||
window.helpers = helpers;
|
||||
import _ from "lodash";
|
||||
|
||||
String.prototype.fmt = function() {
|
||||
return helpers.fmt.apply(this, [this, ...arguments]);
|
||||
};
|
||||
|
||||
// matches polyfill
|
||||
if (!Element.prototype.matches) {
|
||||
Element.prototype.matches =
|
||||
Element.prototype.matchesSelector ||
|
||||
Element.prototype.mozMatchesSelector ||
|
||||
Element.prototype.msMatchesSelector ||
|
||||
Element.prototype.oMatchesSelector ||
|
||||
Element.prototype.webkitMatchesSelector ||
|
||||
function(s) {
|
||||
var matches = (this.document || this.ownerDocument).querySelectorAll(s),
|
||||
i = matches.length;
|
||||
while (--i >= 0 && matches.item(i) !== this);
|
||||
return i > -1;
|
||||
};
|
||||
}
|
||||
|
||||
// closest polyfill
|
||||
if (!Element.prototype.closest)
|
||||
Element.prototype.closest = function(s) {
|
||||
var el = this;
|
||||
if (!document.documentElement.contains(el)) return null;
|
||||
do {
|
||||
if (el.matches(s)) return el;
|
||||
el = el.parentElement;
|
||||
} while (el !== null);
|
||||
return null;
|
||||
};
|
||||
|
||||
const invokeMethods = /^(hide|show|preloadPane|popover|enableSave|disableSave|setIconState|notify)$/;
|
||||
const wrapZafClient = async (client, apiPath, ...rest) => {
|
||||
let method = "get";
|
||||
const isInvoke = invokeMethods.test(apiPath);
|
||||
if (!isInvoke && rest.length) {
|
||||
if (/^(ticket|user|organization)(Fields|\.customField)$/.test(apiPath)) {
|
||||
apiPath = `${apiPath}:${rest.shift()}`;
|
||||
}
|
||||
if (rest.length) method = "set";
|
||||
} else if (isInvoke) {
|
||||
method = "invoke";
|
||||
}
|
||||
try {
|
||||
let result, errors;
|
||||
// Use destructuring to get the value from path on result object
|
||||
({ [apiPath]: result, errors } = await client[method](apiPath, ...rest));
|
||||
if (errors && Object.keys(errors).length) {
|
||||
console.warn(`Some errors were encountered in request ${apiPath}`, errors);
|
||||
}
|
||||
return result;
|
||||
} catch ({ message }) {
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const objGet = function(obj, path) {
|
||||
const npath = path.replace(/\]/g, "");
|
||||
const pieces = npath.split("[");
|
||||
let result = obj;
|
||||
for (const piece of pieces) {
|
||||
result = result[piece];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// @TODO: for some reason trigger is not passing along additional data. Store it in local storage for now.
|
||||
const tempArgKey = "VSTS_ZENDESK_TEMP_ARG";
|
||||
const getMessageArg = function() {
|
||||
const argVal = window.localStorage.getItem(tempArgKey);
|
||||
window.localStorage.removeItem(tempArgKey);
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(argVal);
|
||||
} catch (e) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const setMessageArg = function(data) {
|
||||
window.localStorage.setItem(tempArgKey, JSON.stringify(data));
|
||||
};
|
||||
|
||||
const sharedDataKey = "VSTS_ZENDESK_SHARED_DATA";
|
||||
const replaceVm = function(replacement) {
|
||||
window.localStorage.setItem(sharedDataKey, JSON.stringify(replacement));
|
||||
};
|
||||
const mergeVm = function(toMerge) {
|
||||
const storedVm = JSON.parse(window.localStorage.getItem(sharedDataKey));
|
||||
_.merge(storedVm, toMerge);
|
||||
window.localStorage.setItem(sharedDataKey, JSON.stringify(storedVm));
|
||||
};
|
||||
const assignVm = function(toAssign) {
|
||||
const storedVm = JSON.parse(window.localStorage.getItem(sharedDataKey));
|
||||
Object.assign(storedVm, toAssign);
|
||||
window.localStorage.setItem(sharedDataKey, JSON.stringify(storedVm));
|
||||
};
|
||||
const getVm = function(path) {
|
||||
const storedVm = JSON.parse(window.localStorage.getItem(sharedDataKey));
|
||||
if (path) {
|
||||
return objGet(storedVm, path);
|
||||
}
|
||||
return storedVm;
|
||||
};
|
||||
|
||||
var INSTALLATION_ID = 0,
|
||||
//For dev purposes, when using Zat, set this to your current installation id
|
||||
VSO_URL_FORMAT = "https://%@.visualstudio.com/DefaultCollection",
|
||||
VSO_API_DEFAULT_VERSION = "1.0",
|
||||
VSO_API_RESOURCE_VERSION = {},
|
||||
TAG_PREFIX = "vso_wi_",
|
||||
DEFAULT_FIELD_SETTINGS = JSON.stringify({
|
||||
"System.WorkItemType": {
|
||||
summary: true,
|
||||
details: true,
|
||||
},
|
||||
"System.Title": {
|
||||
summary: false,
|
||||
details: true,
|
||||
},
|
||||
"System.Description": {
|
||||
summary: true,
|
||||
details: true,
|
||||
},
|
||||
}),
|
||||
VSO_ZENDESK_LINK_TO_TICKET_PREFIX = "ZendeskLinkTo_Ticket_",
|
||||
VSO_ZENDESK_LINK_TO_TICKET_ATTACHMENT_PREFIX = "ZendeskLinkTo_Attachment_Ticket_",
|
||||
VSO_WI_TYPES_WHITE_LISTS = ["Bug", "Product Backlog Item", "User Story", "Requirement", "Issue"],
|
||||
VSO_PROJECTS_PAGE_SIZE = 100; //#endregion
|
||||
|
||||
// Create a new ZAFClient
|
||||
var client = ZAFClient.init();
|
||||
|
||||
// add an event listener to detect once your app is registered with the framework
|
||||
client.on("app.registered", function(appData) {
|
||||
client.get("currentUser.locale").then(userData => {
|
||||
// load translations based on the account's current locale
|
||||
I18n.loadTranslations(userData["currentUser.locale"]);
|
||||
new ModalApp(client, appData);
|
||||
});
|
||||
});
|
||||
|
||||
const ModalApp = BaseApp.extend({
|
||||
ajax: function(endpoint) {
|
||||
this.execQueryOnSidebar(["ajax", endpoint]);
|
||||
},
|
||||
onAppActivated: function(data) {
|
||||
const parentGuid = /(?:parentGuid=)(.*?)(?:$|&)/.exec(window.location.hash)[1];
|
||||
const parentClient = this.zafClient.instance(parentGuid);
|
||||
this._parentClient = parentClient;
|
||||
|
||||
setMessageArg(this._context.instanceGuid);
|
||||
parentClient.trigger("registered.done");
|
||||
|
||||
this.zafClient.on("load_template", data => {
|
||||
console.log("loading template: " + data);
|
||||
this.switchTo(data);
|
||||
});
|
||||
this.zafClient.on("execute.action", () => {
|
||||
let args = getMessageArg();
|
||||
console.log("executing: " + JSON.stringify(args));
|
||||
if (typeof args === "string") {
|
||||
args = [args];
|
||||
}
|
||||
this["action_" + args[0]].apply(this, args.slice(1));
|
||||
});
|
||||
this.zafClient.on("execute.query", async () => {
|
||||
let args = getMessageArg();
|
||||
if (typeof args === "string") {
|
||||
args = [args];
|
||||
}
|
||||
setMessageArg(await this["action_" + args[0]](args.slice(1)));
|
||||
parentClient.trigger("execute.response");
|
||||
});
|
||||
this.zafClient.on("execute.response", () => {
|
||||
this.onSidebarResponse(getMessageArg());
|
||||
});
|
||||
|
||||
this.$("[data-main]").on("click", e => {
|
||||
if (e.target.matches("[data-dismiss=modal]")) {
|
||||
this.zafClient.invoke("destroy");
|
||||
}
|
||||
});
|
||||
},
|
||||
onSidebarResponse: function(response) {
|
||||
this._nextSidebarQueryResponseResolver(response);
|
||||
},
|
||||
execQueryOnSidebar: async function(taskName) {
|
||||
this.showBusy();
|
||||
setMessageArg(taskName);
|
||||
this._parentClient.trigger("execute.query");
|
||||
let response;
|
||||
try {
|
||||
response = await new Promise(resolve => {
|
||||
this._nextSidebarQueryResponseResolver = resolve;
|
||||
});
|
||||
} finally {
|
||||
this.hideBusy();
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
action_initNotify: async function() {
|
||||
const $modal = this.$("[data-main]");
|
||||
$modal.find(".modal-body").html(this.renderTemplate("loading"));
|
||||
const data = await this.execQueryOnSidebar(["ajax", "getComments"]);
|
||||
this.lastComment = data.comments[data.comments.length - 1].body;
|
||||
const attachments = _.flatten(
|
||||
_.map(data.comments, function(comment) {
|
||||
return comment.attachments || [];
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
$modal.find(".modal-body").html(
|
||||
this.renderTemplate("notify", {
|
||||
attachments: attachments,
|
||||
}),
|
||||
);
|
||||
|
||||
$modal.find(".modal-footer button").prop("disabled", false);
|
||||
|
||||
$modal.find(".accept").on("click", e => {
|
||||
this.onNotifyAcceptClick(e);
|
||||
});
|
||||
$modal.find(".copyLastComment").on("click", e => {
|
||||
this.onCopyLastCommentClick(e);
|
||||
});
|
||||
this.resize({ width: "28vw", height: "28vh" });
|
||||
},
|
||||
|
||||
action_initUnlinkWorkItem: function(workItem) {
|
||||
var $modal = this.$("[data-main]");
|
||||
$modal.find(".modal-body").html(this.renderTemplate("unlink"));
|
||||
$modal.find(".modal-footer button").removeAttr("disabled");
|
||||
$modal.find(".modal-body .confirm").html(
|
||||
this.I18n.t("modals.unlink.text", {
|
||||
name: workItem.title,
|
||||
}),
|
||||
);
|
||||
$modal.attr("data-id", workItem.id);
|
||||
$modal.find(".accept").on("click", e => {
|
||||
this.onUnlinkAcceptClick(e);
|
||||
});
|
||||
this.resize({ width: "30vw", height: "18vh" });
|
||||
},
|
||||
|
||||
action_initLinkWorkItem: function() {
|
||||
const $modal = this.$("[data-main]");
|
||||
$modal.find(".modal-footer button").removeAttr("disabled");
|
||||
$modal.find(".modal-body").html(this.renderTemplate("link"));
|
||||
$modal.find("button.search").show();
|
||||
const projectCombo = $modal.find(".project");
|
||||
this.fillComboWithProjects(projectCombo);
|
||||
projectCombo.change();
|
||||
this.resize({ width: "30vw", height: "27vh" });
|
||||
|
||||
$modal.find(".search").on("click", () => {
|
||||
this.onLinkSearchClick();
|
||||
});
|
||||
$modal.find(".project").on("change", () => {
|
||||
this.onLinkVsoProjectChange();
|
||||
});
|
||||
$modal.find(".reloadQueriesBtn").on("click", () => {
|
||||
this.onLinkReloadQueriesButtonClick();
|
||||
});
|
||||
$modal.find(".queryBtn").on("click", () => {
|
||||
this.onLinkQueryButtonClick();
|
||||
});
|
||||
$modal.on("click", e => {
|
||||
if (e.target.closest("a.workItemResult") !== null) {
|
||||
this.onLinkResultClick(e);
|
||||
}
|
||||
});
|
||||
$modal.find(".accept").on("click", e => {
|
||||
this.onLinkAcceptClick(e);
|
||||
});
|
||||
},
|
||||
|
||||
action_initWorkItemDetails: async function(workItem) {
|
||||
var $modal = this.$("[data-main]");
|
||||
$modal.find(".modal-header h3").html(this.I18n.t("modals.details.loading"));
|
||||
$modal.find(".modal-body").html(this.renderTemplate("loading"));
|
||||
|
||||
const workItemWithFields = this.attachRestrictedFieldsToWorkItem(workItem, "details");
|
||||
$modal.find(".modal-header h3").html(
|
||||
this.I18n.t("modals.details.title", {
|
||||
name: workItem.title,
|
||||
}),
|
||||
);
|
||||
$modal.find(".modal-body").html(this.renderTemplate("details", workItem));
|
||||
this.resize({ width: "40vw" });
|
||||
},
|
||||
|
||||
action_initNewWorkItem: async function() {
|
||||
const $modal = this.$("[data-main]");
|
||||
$modal.find(".modal-body").html(this.renderTemplate("loading"));
|
||||
const data = await this.execQueryOnSidebar(["ajax", "getComments"]);
|
||||
var attachments = _.flatten(
|
||||
_.map(data.comments, function(comment) {
|
||||
return comment.attachments || [];
|
||||
}),
|
||||
true,
|
||||
); // Check if we have a template for decription
|
||||
|
||||
var templateDefined = !!this.setting("vso_wi_description_template");
|
||||
$modal.find(".modal-body").html(
|
||||
this.renderTemplate("new", {
|
||||
attachments: attachments,
|
||||
templateDefined: templateDefined,
|
||||
}),
|
||||
);
|
||||
$modal.find(".summary").val(getVm("temp[ticket]").subject);
|
||||
var projectCombo = $modal.find(".project");
|
||||
this.fillComboWithProjects(projectCombo);
|
||||
$modal.find(".inputVsoProject").on("change", this.onNewVsoProjectChange.bind(this));
|
||||
$modal.find(".copyDescription").on("click", () => {
|
||||
$modal.find(".description").val(getVm("temp[ticket]").description);
|
||||
});
|
||||
$modal.find(".accept").on("click", () => {
|
||||
this.onNewWorkItemAcceptClick();
|
||||
});
|
||||
projectCombo.change();
|
||||
this.resize({ height: "520px", width: "780px" });
|
||||
},
|
||||
|
||||
showBusy: function() {
|
||||
this.$("[data-main] .busySpinner").show();
|
||||
},
|
||||
|
||||
hideBusy: function() {
|
||||
this.$("[data-main] .busySpinner").hide();
|
||||
},
|
||||
|
||||
onCopyLastCommentClick: function(event) {
|
||||
event.preventDefault();
|
||||
this.$(".notifyModal")
|
||||
.find("textarea")
|
||||
.val(this.lastComment);
|
||||
},
|
||||
|
||||
onNotifyAcceptClick: async function(event) {
|
||||
const _currentUser = await this.zafClient.get("currentUser");
|
||||
|
||||
var $modal = this.$("[data-main]");
|
||||
var text = $modal.find("textarea").val();
|
||||
|
||||
if (!text) {
|
||||
return this.showErrorInModal($modal, this.I18n.t("modals.notify.errCommentRequired"));
|
||||
}
|
||||
|
||||
const workItems = await this.execQueryOnSidebar(["fetchLinkedVsoWorkItems"]);
|
||||
|
||||
// Must do these serially because my execQueryOnSidebar isn't threadsafe
|
||||
try {
|
||||
for (const workItem of workItems) {
|
||||
await this.execQueryOnSidebar([
|
||||
"ajax",
|
||||
"updateVsoWorkItem",
|
||||
workItem.id,
|
||||
[this.buildPatchToAddWorkItemField("System.History", text)],
|
||||
]);
|
||||
}
|
||||
|
||||
const ticketMsg = [this.I18n.t("notify.message", { name: _currentUser.name }), text].join("\r\n\r\n");
|
||||
await this.execQueryOnSidebar(["ajax", "addPrivateCommentToTicket", ticketMsg]);
|
||||
this.zafClient.invoke("notify", this.I18n.t("notify.notification"));
|
||||
this.zafClient.invoke("destroy");
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, e.message);
|
||||
}
|
||||
},
|
||||
|
||||
onUnlinkAcceptClick: async function(event) {
|
||||
const ticket = getVm("temp[ticket]");
|
||||
event.preventDefault();
|
||||
|
||||
const $modal = this.$("[data-main]");
|
||||
const workItemId = $modal.attr("data-id");
|
||||
|
||||
const updateWorkItem = async function(workItem) {
|
||||
// Calculate the positions of links to remove
|
||||
const posOfLinksToRemove = [];
|
||||
|
||||
_.each(
|
||||
workItem.relations,
|
||||
function(link, idx) {
|
||||
if (
|
||||
link.rel.toLowerCase() === "hyperlink" &&
|
||||
(link.attributes.name === VSO_ZENDESK_LINK_TO_TICKET_PREFIX + ticket.id ||
|
||||
link.attributes.name === VSO_ZENDESK_LINK_TO_TICKET_ATTACHMENT_PREFIX + ticket.id)
|
||||
) {
|
||||
posOfLinksToRemove.push(idx - posOfLinksToRemove.length);
|
||||
}
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
const finish = async function() {
|
||||
await this.unlinkTicket(workItem.id);
|
||||
this.zafClient.invoke("notify", this.I18n.t("notify.workItemUnlinked").fmt(workItem.id));
|
||||
|
||||
// close the modal.
|
||||
await this.setDirty();
|
||||
this.zafClient.invoke("destroy");
|
||||
}.bind(this);
|
||||
|
||||
if (posOfLinksToRemove.length === 0) {
|
||||
finish();
|
||||
} else {
|
||||
const operations = [
|
||||
{
|
||||
op: "test",
|
||||
path: "/rev",
|
||||
value: workItem.rev,
|
||||
},
|
||||
].concat(
|
||||
_.map(
|
||||
posOfLinksToRemove,
|
||||
function(pos) {
|
||||
return this.buildPatchToRemoveWorkItemHyperlink(pos);
|
||||
}.bind(this),
|
||||
),
|
||||
);
|
||||
try {
|
||||
await this.execQueryOnSidebar(["ajax", "updateVsoWorkItem", workItemId, operations]);
|
||||
finish();
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, this.I18n.t("modals.unlink.errUnlink") + " - " + e.message);
|
||||
}
|
||||
}
|
||||
}.bind(this); //Get work item to get the last revision and then update
|
||||
|
||||
try {
|
||||
const workItem = await this.execQueryOnSidebar(["ajax", "getVsoWorkItem", workItemId]);
|
||||
updateWorkItem(workItem);
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, e.message);
|
||||
}
|
||||
},
|
||||
|
||||
onLinkAcceptClick: async function(event) {
|
||||
const ticket = getVm("temp[ticket]");
|
||||
const $modal = this.$("[data-main]");
|
||||
const workItemId = $modal.find(".inputVsoWorkItemId").val();
|
||||
|
||||
if (!/^([0-9]+)$/.test(workItemId)) {
|
||||
return this.showErrorInModal($modal, this.I18n.t("modals.link.errWorkItemIdNaN"));
|
||||
}
|
||||
|
||||
if (await this.isAlreadyLinkedToWorkItem(workItemId)) {
|
||||
return this.showErrorInModal($modal, this.I18n.t("modals.link.errAlreadyLinked"));
|
||||
}
|
||||
|
||||
const updateWorkItem = async function(workItem) {
|
||||
//Let's check if there is already a link in the WI returned data
|
||||
const currentLink = _.find(
|
||||
workItem.relations || [],
|
||||
async function(link) {
|
||||
if (
|
||||
link.rel.toLowerCase() === "hyperlink" &&
|
||||
link.attributes.name === VSO_ZENDESK_LINK_TO_TICKET_PREFIX + ticket.id
|
||||
) {
|
||||
return link;
|
||||
}
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
const finish = async function() {
|
||||
await this.linkTicket(workItemId);
|
||||
this.zafClient.invoke("notify", this.I18n.t("notify.workItemLinked").fmt(workItemId));
|
||||
|
||||
// close the modal.
|
||||
await this.setDirty();
|
||||
this.zafClient.invoke("destroy");
|
||||
}.bind(this);
|
||||
|
||||
if (currentLink) {
|
||||
finish();
|
||||
} else {
|
||||
const addLinkOperation = this.buildPatchToAddWorkItemHyperlink(
|
||||
await this.buildTicketLinkUrl(),
|
||||
VSO_ZENDESK_LINK_TO_TICKET_PREFIX + ticket.id,
|
||||
);
|
||||
try {
|
||||
await this.execQueryOnSidebar(["ajax", "updateVsoWorkItem", workItemId, [addLinkOperation]]);
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, this.I18n.t("modals.link.errCannotUpdateWorkItem") + " - " + e.message);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
// Get work item and then update
|
||||
try {
|
||||
const data = await this.execQueryOnSidebar(["ajax", "getVsoWorkItem", workItemId]);
|
||||
await updateWorkItem(data);
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, this.I18n.t("modals.link.errCannotGetWorkItem") + " - " + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
onLinkResultClick: function(event) {
|
||||
event.preventDefault();
|
||||
var $modal = this.$("[data-main]");
|
||||
var id = this.$(event.target)
|
||||
.closest(".workItemResult")
|
||||
.attr("data-id");
|
||||
$modal.find(".inputVsoWorkItemId").val(id);
|
||||
$modal.find(".search-section").hide();
|
||||
this.resize();
|
||||
},
|
||||
|
||||
onLinkQueryButtonClick: async function() {
|
||||
const $modal = this.$("[data-main]");
|
||||
const projId = $modal.find(".project").val();
|
||||
const queryId = $modal.find(".query").val();
|
||||
|
||||
const drawQueryResults = function(results, countQueryItemsResult) {
|
||||
const workItems = _.map(
|
||||
results,
|
||||
function(workItem) {
|
||||
return {
|
||||
id: workItem.id,
|
||||
type: this.getWorkItemFieldValue(workItem, "System.WorkItemType"),
|
||||
title: this.getWorkItemFieldValue(workItem, "System.Title"),
|
||||
};
|
||||
}.bind(this),
|
||||
);
|
||||
|
||||
$modal.find(".results").html(
|
||||
this.renderTemplate("query_results", {
|
||||
workItems: workItems,
|
||||
}),
|
||||
);
|
||||
$modal.find(".alert-success").html(
|
||||
this.I18n.t("queryResults.returnedWorkItems", {
|
||||
count: countQueryItemsResult,
|
||||
}),
|
||||
);
|
||||
this.resize();
|
||||
}.bind(this);
|
||||
|
||||
const [done, proj] = this.getProjectById(projId);
|
||||
|
||||
try {
|
||||
const data = await this.execQueryOnSidebar(["ajax", "getVsoWorkItemQueryResult", proj.name, queryId]);
|
||||
const getWorkItemsIdsFromQueryResult = function(result) {
|
||||
if (result.queryType === "oneHop" || result.queryType === "tree") {
|
||||
return _.map(result.workItemRelations, function(rel) {
|
||||
return rel.target.id;
|
||||
});
|
||||
} else {
|
||||
return _.pluck(result.workItems, "id");
|
||||
}
|
||||
};
|
||||
const ids = getWorkItemsIdsFromQueryResult(data);
|
||||
if (!ids || ids.length === 0) {
|
||||
return drawQueryResults([], 0);
|
||||
}
|
||||
|
||||
const results = await this.execQueryOnSidebar(["ajax", "getVsoWorkItems", _.first(ids, 200).join(",")]);
|
||||
drawQueryResults(results.value, ids.length);
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, this.I18n.t("modals.link.errCannotGetWorkItem. " + e.message));
|
||||
}
|
||||
},
|
||||
|
||||
onLinkVsoProjectChange: function() {
|
||||
this.loadQueriesList();
|
||||
},
|
||||
|
||||
onLinkReloadQueriesButtonClick: function() {
|
||||
this.loadQueriesList(true);
|
||||
},
|
||||
|
||||
onLinkSearchClick: function() {
|
||||
const $modal = this.$("[data-main]");
|
||||
$modal.find(".search-section").show();
|
||||
this.resize({ width: "30vw" });
|
||||
},
|
||||
|
||||
onNewWorkItemAcceptClick: async function() {
|
||||
const ticket = getVm("temp[ticket]");
|
||||
|
||||
const $modal = this.$("[data-main]");
|
||||
|
||||
const [proj, done] = this.getProjectById($modal.find(".project").val());
|
||||
|
||||
if (!proj) {
|
||||
return this.showErrorInModal($modal, this.I18n.t("modals.new.errProjRequired"));
|
||||
}
|
||||
|
||||
// read area id
|
||||
const areaId = $modal.find(".area").val(); //check work item type
|
||||
|
||||
const workItemType = this.getWorkItemTypeByName(proj, $modal.find(".type").val());
|
||||
if (!workItemType) {
|
||||
return this.showErrorInModal($modal, this.I18n.t("modals.new.errWorkItemTypeRequired"));
|
||||
}
|
||||
|
||||
//check summary
|
||||
const summary = $modal.find(".summary").val();
|
||||
if (!summary) {
|
||||
return this.showErrorInModal($modal, this.I18n.t("modals.new.errSummaryRequired"));
|
||||
}
|
||||
|
||||
const description = $modal.find(".description").val();
|
||||
// var attachments = this.getSelectedAttachments($modal);
|
||||
let operations = [].concat(
|
||||
this.buildPatchToAddWorkItemField("System.Title", summary),
|
||||
this.buildPatchToAddWorkItemField("System.Description", description),
|
||||
);
|
||||
|
||||
if (areaId) {
|
||||
operations.push(this.buildPatchToAddWorkItemField("System.AreaId", areaId));
|
||||
}
|
||||
|
||||
if (this.hasFieldDefined(workItemType, "Microsoft.VSTS.Common.Severity") && $modal.find(".severity").val()) {
|
||||
operations.push(this.buildPatchToAddWorkItemField("Microsoft.VSTS.Common.Severity", $modal.find(".severity").val()));
|
||||
}
|
||||
|
||||
if (this.hasFieldDefined(workItemType, "Microsoft.VSTS.TCM.ReproSteps")) {
|
||||
operations.push(this.buildPatchToAddWorkItemField("Microsoft.VSTS.TCM.ReproSteps", description));
|
||||
} //Set tag
|
||||
|
||||
if (this.setting("vso_tag")) {
|
||||
operations.push(this.buildPatchToAddWorkItemField("System.Tags", this.setting("vso_tag")));
|
||||
}
|
||||
|
||||
//Add hyperlink to ticket url
|
||||
operations.push(
|
||||
this.buildPatchToAddWorkItemHyperlink(await this.buildTicketLinkUrl(), VSO_ZENDESK_LINK_TO_TICKET_PREFIX + ticket.id),
|
||||
);
|
||||
|
||||
//Add hyperlinks to attachments
|
||||
//operations = operations.concat(await this.buildPatchToAddWorkItemAttachments(attachments));
|
||||
|
||||
try {
|
||||
const data = await this.execQueryOnSidebar(["ajax", "createVsoWorkItem", proj.id, workItemType.name, operations]);
|
||||
const newWorkItemId = data.id; //sanity check due tfs returning 200 ok but with exception
|
||||
|
||||
if (newWorkItemId > 0) {
|
||||
await this.linkTicket(newWorkItemId); // @TODO
|
||||
}
|
||||
|
||||
this.zafClient.invoke("notify", this.I18n.t("notify.workItemCreated").fmt(newWorkItemId));
|
||||
} catch (exception) {
|
||||
this.showErrorInModal($modal, exception.message);
|
||||
}
|
||||
done();
|
||||
|
||||
await this.setDirty();
|
||||
|
||||
// close the modal.
|
||||
this.zafClient.invoke("destroy");
|
||||
},
|
||||
|
||||
isAlreadyLinkedToWorkItem: async function(id) {
|
||||
return _.contains(await this.getLinkedWorkItemIds(), id);
|
||||
},
|
||||
|
||||
getLinkedWorkItemIds: async function() {
|
||||
return await this.execQueryOnSidebar("getLinkedWorkItemIds");
|
||||
},
|
||||
|
||||
setDirty: async function() {
|
||||
this.execQueryOnSidebar("setDirty");
|
||||
},
|
||||
|
||||
loadQueriesList: async function(reload) {
|
||||
const $modal = this.$("[data-main]");
|
||||
const projId = $modal.find(".project").val();
|
||||
try {
|
||||
await this.loadProjectWorkItemQueries(projId, reload);
|
||||
} catch (e) {
|
||||
this.showErrorInModal($modal, e.message);
|
||||
}
|
||||
this.drawQueriesList($modal.find(".query"), projId);
|
||||
},
|
||||
|
||||
loadProjectWorkItemQueries: async function(projectId, reload) {
|
||||
const [project, doneWithProj] = this.getProjectById(projectId);
|
||||
|
||||
if (project.queries && !reload) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load project queries
|
||||
const data = await this.execQueryOnSidebar(["ajax", "getVsoProjectWorkItemQueries", project.name]);
|
||||
project.queries = data.value;
|
||||
doneWithProj();
|
||||
return data;
|
||||
},
|
||||
|
||||
drawQueriesList: function(select, projId) {
|
||||
const [project, done] = this.getProjectById(projId);
|
||||
|
||||
const drawNode = function(node, prefix) {
|
||||
//It's a folder
|
||||
if (node.isFolder) {
|
||||
return "<optgroup label='%@ %@'>%@</optgroup>".fmt(
|
||||
prefix,
|
||||
node.name,
|
||||
_.reduce(
|
||||
node.children,
|
||||
function(options, childNode, ix) {
|
||||
return "%@%@".fmt(options, drawNode(childNode, prefix + (ix + 1) + "."));
|
||||
},
|
||||
"",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//It's a query
|
||||
return "<option value='%@'>%@ %@</option>".fmt(node.id, prefix, node.name);
|
||||
}.bind(this);
|
||||
|
||||
select.html(
|
||||
_.reduce(
|
||||
project.queries,
|
||||
function(options, query, ix) {
|
||||
return "%@%@".fmt(options, drawNode(query, "" + (ix + 1) + "."));
|
||||
},
|
||||
"",
|
||||
),
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
|
||||
buildTicketLinkUrl: async function() {
|
||||
const ticket = getVm("temp[ticket]");
|
||||
const _currentAccount = await wrapZafClient(this.zafClient, "currentAccount");
|
||||
|
||||
return helpers.fmt("https://%@.zendesk.com/agent/#/tickets/%@", _currentAccount.subdomain, ticket.id);
|
||||
},
|
||||
linkTicket: async function(workItemId) {
|
||||
await this.execQueryOnSidebar(["linkTicket", workItemId]);
|
||||
},
|
||||
unlinkTicket: async function(workItemId) {
|
||||
await this.execQueryOnSidebar(["unlinkTicket", workItemId]);
|
||||
},
|
||||
buildPatchToAddWorkItemField: function(fieldName, value) {
|
||||
// Check if the field type is html to replace newlines by br
|
||||
if (this.isHtmlContentField(fieldName)) {
|
||||
value = value.replace(/\n/g, "<br>");
|
||||
}
|
||||
|
||||
return {
|
||||
op: "add",
|
||||
path: helpers.fmt("/fields/%@", fieldName),
|
||||
value: value,
|
||||
};
|
||||
},
|
||||
buildPatchToAddWorkItemHyperlink: function(url, name, comment) {
|
||||
return {
|
||||
op: "add",
|
||||
path: "/relations/-",
|
||||
value: {
|
||||
rel: "Hyperlink",
|
||||
url: url,
|
||||
attributes: {
|
||||
name: name,
|
||||
comment: comment,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
buildPatchToRemoveWorkItemHyperlink: function(pos) {
|
||||
return {
|
||||
op: "remove",
|
||||
path: helpers.fmt("/relations/%@", pos),
|
||||
};
|
||||
},
|
||||
getFieldByFieldRefName: function(fieldRefName) {
|
||||
const fields = getVm("fields");
|
||||
return _.find(fields, function(f) {
|
||||
return f.refName == fieldRefName;
|
||||
});
|
||||
},
|
||||
isHtmlContentField: function(fieldName) {
|
||||
var field = this.getFieldByFieldRefName(fieldName);
|
||||
|
||||
if (field && field.type) {
|
||||
var fieldType = field.type.toLowerCase();
|
||||
return fieldType === "html" || fieldType === "history";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onNewVsoProjectChange: function() {
|
||||
var $modal = this.$("[data-main]");
|
||||
var projId = $modal.find(".project").val();
|
||||
|
||||
this.showBusy();
|
||||
this.loadProjectMetadata(projId)
|
||||
.then(
|
||||
function() {
|
||||
this.drawAreasList($modal.find(".area"), projId);
|
||||
this.drawTypesList($modal.find(".type"), projId);
|
||||
$modal.find(".type").change();
|
||||
this.hideBusy();
|
||||
}.bind(this),
|
||||
)
|
||||
.catch(
|
||||
function(jqXHR) {
|
||||
this.hideBusy();
|
||||
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR));
|
||||
}.bind(this),
|
||||
);
|
||||
},
|
||||
fillComboWithProjects: function(el) {
|
||||
el.html(
|
||||
_.reduce(
|
||||
getVm("projects"),
|
||||
function(options, project) {
|
||||
return "%@<option value='%@'>%@</option>".fmt(options, project.id, project.name);
|
||||
},
|
||||
"",
|
||||
),
|
||||
);
|
||||
},
|
||||
loadProjectMetadata: async function(projectId) {
|
||||
var [project, done] = this.getProjectById(projectId);
|
||||
|
||||
if (project.metadataLoaded === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workItemData = await this.execQueryOnSidebar(["ajax", "getVsoProjectWorkItemTypes", project.id]);
|
||||
project.workItemTypes = this.restrictToAllowedWorkItems(workItemData.value);
|
||||
|
||||
const areaData = await this.execQueryOnSidebar(["ajax", "getVsoProjectAreas", project.id]);
|
||||
var areas = []; // Flatten areas to format \Area 1\Area 1.1
|
||||
|
||||
const visitArea = function(area, currentPath) {
|
||||
currentPath = currentPath ? currentPath + "\\" : "";
|
||||
currentPath = currentPath + area.name;
|
||||
areas.push({
|
||||
id: area.id,
|
||||
name: currentPath,
|
||||
});
|
||||
|
||||
if (area.children && area.children.length > 0) {
|
||||
_.forEach(area.children, function(child) {
|
||||
visitArea(child, currentPath);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
visitArea(areaData);
|
||||
project.areas = _.sortBy(areas, function(area) {
|
||||
return area.name;
|
||||
});
|
||||
|
||||
project.metadataLoaded = true;
|
||||
done(); // set project back to localstorage
|
||||
},
|
||||
|
||||
/**
|
||||
* @return [proj: Project, done: Function]
|
||||
* Make sure you call done() when you are done editing
|
||||
* the project, or it will not get saved back to storage.
|
||||
*/
|
||||
getProjectById: function(id) {
|
||||
const projects = getVm("projects");
|
||||
return [
|
||||
_.find(projects, function(proj) {
|
||||
return proj.id == id;
|
||||
}),
|
||||
function() {
|
||||
assignVm({ projects: projects });
|
||||
},
|
||||
];
|
||||
},
|
||||
setProject: function(proj) {
|
||||
const projects = getVm("projects");
|
||||
_.find(projects, function(p) {
|
||||
return p.id === proj.id;
|
||||
});
|
||||
},
|
||||
getWorkItemTypeByName: function(project, name) {
|
||||
return _.find(project.workItemTypes, function(wit) {
|
||||
return wit.name == name;
|
||||
});
|
||||
},
|
||||
getWorkItemFieldValue: function(workItem, fieldRefName) {
|
||||
var field = workItem.fields[fieldRefName];
|
||||
return field || "";
|
||||
},
|
||||
hasFieldDefined: function(workItemType, fieldRefName) {
|
||||
return _.some(workItemType.fieldInstances, function(fieldInstance) {
|
||||
return fieldInstance.referenceName === fieldRefName;
|
||||
});
|
||||
},
|
||||
drawTypesList: function(select, projectId) {
|
||||
var [project, done] = this.getProjectById(projectId);
|
||||
select.html(
|
||||
this.renderTemplate("types", {
|
||||
types: project.workItemTypes,
|
||||
}),
|
||||
);
|
||||
done();
|
||||
},
|
||||
drawAreasList: function(select, projectId) {
|
||||
var [project, done] = this.getProjectById(projectId);
|
||||
select.html(
|
||||
this.renderTemplate("areas", {
|
||||
areas: project.areas,
|
||||
}),
|
||||
);
|
||||
done();
|
||||
},
|
||||
showErrorInModal: function($modal, err) {
|
||||
if ($modal.find(".modal-body .errors")) {
|
||||
$modal
|
||||
.find(".modal-body .errors")
|
||||
.text(err)
|
||||
.show();
|
||||
}
|
||||
},
|
||||
resize: function(size = {}) {
|
||||
// Automatically resize the iframe based on document height, if it's not in the "nav_bar" location
|
||||
if (this._context.location !== "nav_bar") {
|
||||
this.zafClient.invoke("resize", { height: size.height || this.$("html").height(), width: size.width || "50vw" });
|
||||
}
|
||||
},
|
||||
restrictToAllowedWorkItems: function(wits) {
|
||||
return _.filter(wits, function(wit) {
|
||||
return _.contains(VSO_WI_TYPES_WHITE_LISTS, wit.name);
|
||||
});
|
||||
},
|
||||
attachRestrictedFieldsToWorkItem: function(workItem, type) {
|
||||
const fieldSettings = getVm("fieldSettings");
|
||||
var fields = _.compact(
|
||||
_.map(
|
||||
fieldSettings,
|
||||
function(value, key) {
|
||||
if (value[type]) {
|
||||
if (_.has(workItem.fields, key)) {
|
||||
return {
|
||||
refName: key,
|
||||
name: _.find(getVm("fields"), function(f) {
|
||||
return f.refName == key;
|
||||
}).name,
|
||||
value: workItem.fields[key],
|
||||
isHtml: this.isHtmlContentField(key),
|
||||
};
|
||||
}
|
||||
}
|
||||
}.bind(this),
|
||||
),
|
||||
);
|
||||
assignVm({ fieldSettings: fieldSettings });
|
||||
|
||||
return _.extend(workItem, {
|
||||
restricted_fields: fields,
|
||||
});
|
||||
},
|
||||
getAjaxErrorMessage: function(jqXHR, errMsg) {
|
||||
errMsg = errMsg || this.I18n.t("errorAjax"); //Let's try get a friendly message based on some cases
|
||||
|
||||
var serverErrMsg;
|
||||
|
||||
if (jqXHR.responseJSON) {
|
||||
serverErrMsg = jqXHR.responseJSON.message || jqXHR.responseJSON.value.Message;
|
||||
} else if (jqXHR.responseText) {
|
||||
serverErrMsg = jqXHR.responseText.substring(0, 50) + "...";
|
||||
}
|
||||
|
||||
var detail = this.I18n.t("errorServer").fmt(jqXHR.status, jqXHR.statusText, serverErrMsg);
|
||||
return errMsg + " " + detail;
|
||||
},
|
||||
events: {
|
||||
"app.activated": "onAppActivated",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
<div class='modal-header'>
|
||||
<h3>{{t "modals.details.loading"}}</h3>
|
||||
<div class="spacer"></div>
|
||||
<div class="busySpinner spinner dotted"></div>
|
||||
</div>
|
||||
<div class='modal-body'>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{t "modals.details.close"}}</button>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
<div class='modal-header'>
|
||||
<h3>{{t "modals.link.title"}}</h3>
|
||||
<div class="spacer"></div>
|
||||
<div class="busySpinner spinner dotted"></div>
|
||||
</div>
|
||||
<div class='modal-body linkModal'>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{t "modals.link.close"}}</button>
|
||||
<!--<button class="btn search">{{t "modals.link.search"}}</button>
|
||||
<button class="btn btn-primary accept">{{t "modals.link.accept"}}</button>-->
|
||||
</div>
|
|
@ -7,30 +7,27 @@
|
|||
<p> </p>
|
||||
<p class="help">{{t "login.help"}}</p>
|
||||
<p> </p>
|
||||
|
||||
<form class="form-horizontal login-form">
|
||||
<p class='alert alert-error errors' style='display:none'>
|
||||
</p>
|
||||
|
||||
<div class="control-group">
|
||||
<p class='alert alert-error errors' style='display:none'>
|
||||
</p>
|
||||
<div class="form">
|
||||
<div class="username form-input">
|
||||
<label class="control-label" for="vso_username">{{t "login.username"}}</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="vso_username" class="vso_username input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="password form-input">
|
||||
<label class="control-label" for="vso_password">{{t "login.password"}}</label>
|
||||
<div class="controls">
|
||||
<input type="password" id="vso_password" class="vso_password input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<div class="submit form-input">
|
||||
<div class="controls">
|
||||
<button class="btn btn-small login-button">{{t "login.button"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<div class="controlButtons">
|
||||
<a class='user'><i class="icon-user"/></a>
|
||||
<a class='cog'><i class='icon-cog'/></a>
|
||||
</div>
|
||||
<ul class='buttons'>
|
||||
<li>
|
||||
<button class='btn btn-mini newWorkItem'>{{t "buttons.create"}}</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class='btn btn-mini link'>{{t "buttons.link"}}</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class='btn btn-mini notify'>{{t "buttons.notify"}}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr class='split'>
|
||||
|
||||
|
||||
<div class='workItems'>
|
||||
<div class="workItemsError alert alert-error" style="display:none">
|
||||
<strong>{{t "errorOoops"}}</strong> {{t "errorLoadingWorkItems"}}
|
||||
<a href="#" class="refreshWorkItemsLink" title="Refresh">
|
||||
<i class="icon-refresh"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
<div class='modal-header'>
|
||||
<h3>{{t "modals.new.title"}}</h3>
|
||||
<div class="spacer"></div>
|
||||
<div class="busySpinner spinner dotted"></div>
|
||||
</div>
|
||||
<div class='modal-body'>
|
||||
</div>
|
||||
<div class='modal-footer'>
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{t "modals.new.close"}}</button>
|
||||
<button class="btn btn-primary accept">{{t "modals.new.accept"}}</button>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
<div class='modal-header'>
|
||||
<h3>{{t "modals.notify.title"}}</h3>
|
||||
<div class="spacer"></div>
|
||||
<div class="busySpinner spinner dotted"></div>
|
||||
</div>
|
||||
<div class='modal-body notifyModal'>
|
||||
</div>
|
||||
<div class='modal-footer'>
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{t "modals.notify.close"}}</button>
|
||||
<button class="btn btn-primary accept">{{t "modals.notify.accept"}}</button>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
<div class='modal-header'>
|
||||
<h3>{{t "modals.unlink.title"}}</h3>
|
||||
<div class="spacer"></div>
|
||||
<div class="busySpinner spinner dotted"></div>
|
||||
</div>
|
||||
<div class='modal-body'>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal" aria-hidden="true">{{t "modals.unlink.close"}}</button>
|
||||
<button class="btn btn-danger accept">{{t "modals.unlink.accept"}}</button>
|
||||
</div>
|
|
@ -70,7 +70,7 @@
|
|||
},
|
||||
"link": {
|
||||
"title": "Link to a work item",
|
||||
"close": "Go back",
|
||||
"close": "Close",
|
||||
"accept": "Link",
|
||||
"search": "Search",
|
||||
"query": "Query",
|
||||
|
@ -87,7 +87,7 @@
|
|||
},
|
||||
"new": {
|
||||
"title": "New Visual Studio Team Services Work Item",
|
||||
"close": "Go back",
|
||||
"close": "Close",
|
||||
"accept": "Create work item",
|
||||
"help": "Fill this form to create a new Visual Studio Team Services work item and link it to this ticket.",
|
||||
"automatic": "Automatic",
|
|
@ -0,0 +1,4 @@
|
|||
*.zip
|
||||
debug.log
|
||||
.editorconfig
|
||||
node_modules
|
|
@ -0,0 +1,41 @@
|
|||
# Visual Studio Team Services App for Zendesk
|
||||
|
||||
> Get the latest version of the app: [Download v0.5.0](https://github.com/Microsoft/vsts-zendesk-app/releases/download/v0.5.0/vsts-zendesk-app-0.5.0.zip)
|
||||
|
||||
Unite your customer support and development teams. Quickly create or link work items to tickets, enable efficient two-way communication, and stop using email to check status.
|
||||
|
||||
### Create work items for your engineers right from Zendesk
|
||||
|
||||
With the Visual Studio Team Services app for Zendesk, users in Zendesk can quickly create a new work item from a Zendesk ticket.
|
||||
|
||||
![img](https://i3-vso.sec.s-msft.com/dynimg/IC729561.png)
|
||||
|
||||
### Get instant access to the status of linked work items
|
||||
|
||||
Give your customer support team easy access to the information they need. See details about work items linked to a Zendesk ticket.
|
||||
|
||||
![img](https://ms-vsts.gallery.vsassets.io/_apis/public/gallery/publisher/ms-vsts/extension/services-zendesk/latest/assetbyname/images/zendesk-linked.png)
|
||||
|
||||
## How to install and setup
|
||||
|
||||
### Install the app to Zendesk
|
||||
|
||||
1. Download the latest release .zip file: **[version 0.5.0](https://github.com/Microsoft/vsts-zendesk-app/releases/download/v0.5.0/vsts-zendesk-app-0.5.0.zip)**
|
||||
1. From Zendesk, click the settings icon (gear)
|
||||
1. Under **Apps** click Manage.
|
||||
1. Click **Upload private app**
|
||||
1. Give the app a name.
|
||||
1. Browse to the location you saved the .zip release and select it.
|
||||
1. Provide your Visual Studio Team Services name and decide on a work item tag for Zendesk.
|
||||
|
||||
See [full instructions](https://www.visualstudio.com/docs/marketplace/integrate/service-hooks/services/zendesk)
|
||||
|
||||
### Send updates from Visual Studio Team Services to Zendesk
|
||||
|
||||
1. Open the admin page for the team project in Visual Studio Team Services
|
||||
2. On the *Service Hooks* tab, run the subscription wizard
|
||||
3. Select Zendesk from the subscription wizard
|
||||
4. Pick and the Visual Studio Team Services event which will post to Zendesk
|
||||
5. Tell Zendesk what to do when the event occurs
|
||||
6. Test the service hook subscription and finish the wizard
|
||||
|
До Ширина: | Высота: | Размер: 5.3 KiB После Ширина: | Высота: | Размер: 5.3 KiB |
До Ширина: | Высота: | Размер: 24 KiB После Ширина: | Высота: | Размер: 24 KiB |
235
v2/README.md
|
@ -1,235 +0,0 @@
|
|||
*Use of this software is subject to important terms and conditions as set forth in the License file*
|
||||
|
||||
# App Scaffold
|
||||
|
||||
## Description
|
||||
This repo contains a scaffold to help developers build [apps for Zendesk products](https://developer.zendesk.com/apps/docs/apps-v2/getting_started).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Dependencies
|
||||
- [Node.js](https://nodejs.org/en/) >= 6.3.x
|
||||
- [Ruby](https://www.ruby-lang.org/) >= 2.0.x
|
||||
|
||||
### Setup
|
||||
1. Clone or fork this repo
|
||||
2. Change (`cd`) into the `app_scaffold` directory
|
||||
3. Run `npm install`
|
||||
|
||||
To run your app locally in Zendesk, you need the [Zendesk Apps Tools (ZAT)](https://github.com/zendesk/zendesk_apps_tools).
|
||||
|
||||
You'll also need to run a couple of command-line Node.js-based tools that are installed using `npm`. For a node module to be available from the command-line, it must be installed globally.
|
||||
|
||||
To setup these and other dependencies, run these commands:
|
||||
|
||||
```
|
||||
gem install zendesk_apps_tools
|
||||
npm install --global webpack foreman karma-cli
|
||||
```
|
||||
|
||||
Note: Foreman was originally created as a Ruby tool. If you prefer, you can install it by `gem install foreman` instead.
|
||||
|
||||
### Running locally
|
||||
|
||||
_Note: The App Scaffold currently depends on zat v1.35.12 or greater._
|
||||
|
||||
Foreman allows you to easily run multiple processes in one tab. One process is `zat server --path=./dist`, which serves the app in a way that can be run in a supported Zendesk product. The second is `webpack --watch` to rebuild the project whenever you save changes to a source file.
|
||||
|
||||
To run these processes, run
|
||||
|
||||
```
|
||||
nf start
|
||||
```
|
||||
|
||||
or run the individual commands from the Procfile in separate terminals.
|
||||
|
||||
Note: If you installed the Ruby version of foreman, you'll need to use `foreman start`.
|
||||
|
||||
## But why?
|
||||
The App Scaffold includes many features to help you maintain and scale your app. Some of the features provided by the App Scaffold are listed below. However, you don't need prior experience in any of these to be able to use the scaffold successfully.
|
||||
|
||||
- [ES6 (ES2015)](https://babeljs.io/docs/learn-es2015/)
|
||||
|
||||
ECMAScript 6, also known as ECMAScript 2015, is the latest version of the ECMAScript standard. The App Scaffold includes the [Babel compiler](https://babeljs.io/) to transpile your code to ES5. This allows you to use ES6 features, such as classes, arrow functions and template strings even in browsers that haven't fully implemented these features.
|
||||
|
||||
- [Handlebars](http://handlebarsjs.com/) templates
|
||||
|
||||
Handlebars is a powerful templating library that lets you build semantic templates for your app with minimal logic.
|
||||
|
||||
- [SASS](http://sass-lang.com/) stylesheets
|
||||
|
||||
Sass is an extension of CSS that adds power and elegance to the basic language. It allows you to use variables, nested rules, mixins, inline imports, and more.
|
||||
|
||||
- [Webpack](https://webpack.github.io/) module bundler
|
||||
|
||||
Webpack compiles web browser applications. It allows splitting your source code into modules and re-use them with require and import statements. It also allows splitting your compiled project into separate files that are loaded on demand.
|
||||
|
||||
- [Karma](http://karma-runner.github.io/) test runner
|
||||
|
||||
The main goal for Karma is to bring a productive testing environment to developers with minimal configuration.
|
||||
|
||||
- [Jasmine](https://jasmine.github.io/) testing framework
|
||||
|
||||
Jasmine is a behavior-driven development framework for testing JavaScript code with a clean syntax.
|
||||
|
||||
## Folder structure
|
||||
|
||||
The folder and file structure of the App Scaffold is as follows:
|
||||
|
||||
| Name | Description |
|
||||
|:----------------------------------------|:---------------------------------------------------------------------------------------------|
|
||||
| [`dist/`](#dist) | The folder in which webpack packages the built version of your app |
|
||||
| [`lib/`](#lib) | The folder in which the shims and files that make the scaffold work live |
|
||||
| [`spec/`](#spec) | The folder in which all of your test files live |
|
||||
| [`src/`](#src) | The folder in which all of your source JavaScript, CSS, templates and translation files live |
|
||||
| [`.eslintrc`](#eslintrc) | Configuration file for JavaScript linting |
|
||||
| [`karma.conf.js`](#karmaconfjs) | Configuration file for the test runner |
|
||||
| [`package.json`](#packagejson) | Configuration file for build dependencies |
|
||||
| [`webpack.config.js`](#webpackconfigjs) | Configuration file that webpack uses to build your app |
|
||||
|
||||
#### dist
|
||||
The dist directory is the folder you will need to package when submitting your app to the marketplace. It is also the folder you will have to serve when using [ZAT](https://developer.zendesk.com/apps/docs/apps-v2/getting_started#zendesk-app-tools). It includes your app's manifest.json file, an assets folder with all your compiled JavaScript and CSS as well as HTML and images.
|
||||
|
||||
#### lib
|
||||
The lib directory is where the source code for the app shims and compatibility methods live. While you may modify or remove this code as required for your app, doing so is not recommended for beginners.
|
||||
|
||||
#### spec
|
||||
The spec directory is where all your tests and test helpers live. Tests are not required to submit/upload your app to Zendesk and your test files are not included in your app's package, however it is good practice to write tests to document functionality and prevent bugs.
|
||||
|
||||
#### src
|
||||
The src directory is where your raw source code lives. The App Scaffold includes different directories for JavaScript, stylesheets, templates and translations. Most of your additions will be in here (and spec, of course!).
|
||||
|
||||
#### .eslintrc
|
||||
.eslintrc is a configuration file for [ESLint](http://eslint.org). ESLint is a linting utility for JavaScript. For more information on how to configure ESLint, see [Configuring ESLint](http://eslint.org/docs/user-guide/configuring).
|
||||
|
||||
#### karma.conf.js
|
||||
karma.conf.js is a configuration file for [Karma](http://karma-runner.github.io). Karma is a JavaScript test runner. This file defines where your source and test files live. For more information on how to use this file, see [Karma - Configuration File](http://karma-runner.github.io/1.0/config/configuration-file.html).
|
||||
|
||||
#### package.json
|
||||
package.json is a configuration file for [NPM](https://www.npmjs.com). NPM is a package manager for JavaScript. This file includes information about your project and its dependencies. For more information on how to configure this file, see [package.json](https://docs.npmjs.com/files/package.json).
|
||||
|
||||
#### webpack.config.js
|
||||
webpack.config.js is a configuration file for [webpack](https://webpack.github.io/). Webpack is a JavaScript module bundler. For more information about webpack and how to configure it, see [What is webpack](http://webpack.github.io/docs/what-is-webpack.html).
|
||||
|
||||
## Initialization
|
||||
The App Scaffold's initialization code lives in [`src/index.js`](https://github.com/zendesk/app_scaffold/blob/master/src/javascripts/index.js). For more information, see [inline documentation](https://github.com/zendesk/app_scaffold/blob/master/src/javascripts/index.js).
|
||||
|
||||
## API Reference
|
||||
The App Scaffold provides some classes under `/lib` to help building apps.
|
||||
|
||||
### I18n
|
||||
The I18n (internationalization) module provides a `t` method and Handlebars helper to look up translations based on a key. For more information, see [Using the I18n module](https://github.com/zendesk/app_scaffold/blob/master/doc/i18n.md).
|
||||
|
||||
### Storage
|
||||
The Storage module provides helper methods to interact with `localStorage`. For more information, see [Using the Storage module](https://github.com/zendesk/app_scaffold/blob/master/doc/storage.md).
|
||||
|
||||
### View
|
||||
The View module provides methods to simplify rendering Handlebars templates located under the templates folder. For more information, see [Using the View module](https://github.com/zendesk/app_scaffold/blob/master/doc/view.md).
|
||||
|
||||
## Migrating from v1
|
||||
The master branch of this repo contains modules and sample code to help you migrate from a v1 app. For detailed documentation on how to migrate from a v1 app, see our [Migrating to v2](https://developer.zendesk.com/apps/docs/apps-v2/migrating) guide on the Zendesk Developer Portal.
|
||||
|
||||
## Starting from scratch
|
||||
If you're starting a v2 app from scratch you will need to check out the [from-scratch](https://github.com/zendesk/app_scaffold/tree/from-scratch) branch:
|
||||
|
||||
```
|
||||
git checkout from-scratch
|
||||
npm install
|
||||
```
|
||||
|
||||
The from-scratch branch uses up-to-date versions of the libraries included with the App Scaffold and also removes the shims needed when migrating from v1. It also includes sample code to help you get started on v2.
|
||||
|
||||
Another addition present only in the from-scratch branch, is the [Zendesk Garden](http://garden.zendesk.com/) stylesheet. Zendesk Garden is designed to be a common baseline of styles and components between all Zendesk products. For more information, see [Using the Zendesk Garden styles](https://developer.zendesk.com/apps/docs/apps-v2/setup#using-the-zendesk-garden-styles) in the Zendesk Developer Portal.
|
||||
|
||||
If you want to see the exact differences between the master and from-scratch branches click [here](https://github.com/zendesk/app_scaffold/compare/from-scratch).
|
||||
|
||||
## Parameters and Settings
|
||||
|
||||
If you need to test your app with a `parameters` section in `dist/manifest.json`, foreman might crash with a message like:
|
||||
|
||||
> Would have prompted for a value interactively, but zat is not listening to keyboard input.
|
||||
|
||||
To resolve this problem, set default values for parameters or create a `settings.yml` file in the root directory of your app scaffold-based project, and populate it with your parameter names and test values. For example, using a parameters section like:
|
||||
|
||||
```json
|
||||
{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "myParameter"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
create a `settings.yml` containing:
|
||||
|
||||
```yaml
|
||||
myParameter: 'some value!'
|
||||
```
|
||||
|
||||
If you prefer to manually input settings every time you run foreman, edit the Procfile to remove the `--unattended` option from the server command.
|
||||
|
||||
## Testing
|
||||
|
||||
The App Scaffold is currently setup for testing with [Jasmine](http://jasmine.github.io/) (testing framework) and [Karma](https://karma-runner.github.io) (test runner). To run specs, run
|
||||
|
||||
```
|
||||
karma start
|
||||
```
|
||||
|
||||
Specs live under the `spec` directory and can be configured by editing the `karma.conf.js` file.
|
||||
|
||||
## Deploying
|
||||
|
||||
To check that your app will pass the server-side validation check, run
|
||||
|
||||
```
|
||||
zat validate --path=./dist
|
||||
```
|
||||
|
||||
If validation is successful, you can upload the app into your Zendesk account by running
|
||||
|
||||
```
|
||||
zat create --path=dist
|
||||
```
|
||||
|
||||
To update your app after it has been created in your account, run
|
||||
|
||||
```
|
||||
zat update --path=dist
|
||||
```
|
||||
|
||||
Or, to create a zip archive for manual upload, run
|
||||
|
||||
```
|
||||
zat package --path=dist
|
||||
```
|
||||
|
||||
taking note of the created filename.
|
||||
|
||||
For more information on the Zendesk Apps Tools please see the [documentation](https://developer.zendesk.com/apps/docs/apps-v2/getting_started#zendesk-app-tools).
|
||||
|
||||
## External Dependencies
|
||||
External dependencies are defined in a module, [`lib/external_assets.js`](https://github.com/zendesk/app_scaffold/blob/master/lib/external_assets.js). The export of the module is imported into [`webpack.config.js`](https://github.com/zendesk/app_scaffold/blob/master/webpack.config.js) at build-time. This ensures these dependencies are included on your app's `index.html` as well as in the test suite.
|
||||
|
||||
## Contribute
|
||||
* Put up a PR into the master branch.
|
||||
* CC and get a +1 from @zendesk/vegemite.
|
||||
|
||||
## Bugs
|
||||
Submit Issues via [GitHub](https://github.com/zendesk/app_scaffold/issues/new) or email support@zendesk.com.
|
||||
|
||||
## Useful Links
|
||||
Links to maintaining team, confluence pages, Datadog dashboard, Kibana logs, etc
|
||||
- https://developer.zendesk.com/
|
||||
- https://github.com/zendesk/zendesk_apps_tools
|
||||
- https://webpack.github.io
|
||||
|
||||
## Copyright and license
|
||||
Copyright 2016 Zendesk
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
@ -1,151 +0,0 @@
|
|||
* {
|
||||
/*
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||
//
|
||||
*/ }
|
||||
* header .logo {
|
||||
background-image: app-asset-url("logo-small.png"); }
|
||||
* .header {
|
||||
margin-bottom: 8px; }
|
||||
* .header h3.app_name {
|
||||
font-size: 16px; }
|
||||
* .cog {
|
||||
margin-top: 6px;
|
||||
margin-right: 8px;
|
||||
opacity: 0.5;
|
||||
float: right;
|
||||
display: none; }
|
||||
* .cog:hover {
|
||||
opacity: 1; }
|
||||
* .user {
|
||||
margin-top: 6px;
|
||||
margin-right: 8px;
|
||||
opacity: 0.5;
|
||||
float: right; }
|
||||
* .user:hover {
|
||||
opacity: 1; }
|
||||
* hr.split {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 10px; }
|
||||
* .linkModal .inputVsoProject {
|
||||
width: 330px; }
|
||||
* .linkModal .results {
|
||||
margin-top: 15px; }
|
||||
* .linkModal .inputVsoWorkItemId {
|
||||
width: calc(100% - 174px);
|
||||
box-sizing: border-box;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
margin-left: 10px; }
|
||||
* .newWorkItemModal {
|
||||
width: 750px;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
margin: 0; }
|
||||
* .newWorkItemModal .modal-body {
|
||||
max-height: 550px; }
|
||||
* .newWorkItemModal .description {
|
||||
height: 100px; }
|
||||
* .newWorkItemModal .summary {
|
||||
width: 530px; }
|
||||
* .newWorkItemModal .attachments ul {
|
||||
margin: 0px; }
|
||||
* .admin .closeAdmin {
|
||||
float: right;
|
||||
opacity: 0.5; }
|
||||
* .admin .closeAdmin:hover {
|
||||
opacity: 1; }
|
||||
* .login .closeLogin {
|
||||
float: right;
|
||||
opacity: 0.5; }
|
||||
* .login .closeLogin:hover {
|
||||
opacity: 1; }
|
||||
* .workItems p.help {
|
||||
font-size: 13px;
|
||||
margin-top: 27px;
|
||||
line-height: 13px;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
color: #d1d1d1; }
|
||||
* .workItems .action {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
opacity: 0.2; }
|
||||
* .workItems .action:hover {
|
||||
opacity: 1; }
|
||||
* .workItems .workItem {
|
||||
padding-left: 30px;
|
||||
border-bottom: 1px solid #dedede;
|
||||
padding-bottom: 17px;
|
||||
margin-top: 18px;
|
||||
position: relative; }
|
||||
* .workItems .workItem .avatar {
|
||||
float: left;
|
||||
position: absolute;
|
||||
left: 0px; }
|
||||
* .workItem-title {
|
||||
line-height: 14px;
|
||||
margin-bottom: 8px; }
|
||||
* .workItem-fields-list {
|
||||
margin-left: 0px;
|
||||
color: #7b7b7b; }
|
||||
* .workItem-field-item {
|
||||
padding-top: 8px;
|
||||
line-height: 18px; }
|
||||
* .workItem-field-name {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase; }
|
||||
* .workItem-field-name-icon {
|
||||
opacity: 0.5; }
|
||||
* .workItems-header {
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
border-bottom: 1px solid #dedede;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 18px; }
|
||||
* .center {
|
||||
text-align: center; }
|
||||
* .buttons {
|
||||
margin: 0px 22px 0px; }
|
||||
* .buttons li {
|
||||
display: inline-block;
|
||||
min-width: 70px; }
|
||||
* .buttons li button {
|
||||
color: inherit;
|
||||
width: 100%; }
|
||||
* .form-horizontal.project-selection .help {
|
||||
font-size: 13px;
|
||||
text-align: center; }
|
||||
* .form-horizontal.project-selection .control-label {
|
||||
float: left;
|
||||
width: 140px;
|
||||
padding-top: 5px;
|
||||
text-align: left;
|
||||
margin-left: 16px; }
|
||||
* .control-group.project-selection {
|
||||
font-size: 13px;
|
||||
padding-left: 140px;
|
||||
padding-bottom: 10px; }
|
||||
* .notifyModal textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 100px; }
|
||||
* .notifyModal .copyLastComment {
|
||||
display: block;
|
||||
margin-bottom: 20px; }
|
||||
* .notifyModal label {
|
||||
margin-bottom: 0.5em; }
|
||||
* .attachments img,
|
||||
* .attachments input {
|
||||
vertical-align: middle; }
|
||||
* .attachments img {
|
||||
width: 30px; }
|
||||
* .settings {
|
||||
table-layout: fixed; }
|
||||
* .settings tbody tr td:first-of-type {
|
||||
word-wrap: break-word; }
|