This commit is contained in:
Trevor Gau 2017-12-06 12:07:36 -05:00
Родитель 1a259958e6
Коммит 995b3a3bc7
105 изменённых файлов: 2997 добавлений и 2247 удалений

Просмотреть файл

Просмотреть файл

@ -1,5 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 2

Просмотреть файл

1
.gitignore поставляемый
Просмотреть файл

@ -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.

1
dist/assets/app.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -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

171
dist/assets/main.css поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

18
dist/assets/modal.html поставляемый Normal file
Просмотреть файл

@ -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>

1
dist/assets/modal.js поставляемый Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -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;

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

16
lib/templates/modal.hdbs Normal file
Просмотреть файл

@ -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>

0
v2/package-lock.json → package-lock.json сгенерированный
Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 5.3 KiB

После

Ширина:  |  Высота:  |  Размер: 5.3 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 24 KiB

После

Ширина:  |  Высота:  |  Размер: 24 KiB

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 6.3 KiB

После

Ширина:  |  Высота:  |  Размер: 6.3 KiB

Просмотреть файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

986
src/javascripts/modal.js Normal file
Просмотреть файл

@ -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",
},
});

272
src/stylesheets/app.scss Normal file

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -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>&nbsp;</p>
<p class="help">{{t "login.help"}}</p>
<p>&nbsp;</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>

27
src/templates/main.hdbs Normal file
Просмотреть файл

@ -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",

4
v1/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,4 @@
*.zip
debug.log
.editorconfig
node_modules

Просмотреть файл

41
v1/README.md Normal file
Просмотреть файл

@ -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

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -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.

151
v2/dist/assets/main.css поставляемый
Просмотреть файл

@ -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; }

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше