vsts-zendesk-app/app.js

1211 строки
42 KiB
JavaScript

//*********************************************************
//
// 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.
//
//*********************************************************
/* global helpers, services, _, Base64 */
(function () {
'use strict';
//#region Constants
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
return {
defaultState: 'loading',
//Global view model shared by all instances
vm: {
accountUrl: null,
projects: [],
fields: [],
fieldSettings: {},
userProfile: {},
isAppLoadedOk: false
},
//#region Events Declaration
events: {
// App
'app.activated': 'onAppActivated',
// Requests
'getVsoProjects.done': 'onGetVsoProjectsDone',
'getVsoFields.done': 'onGetVsoFieldsDone',
//New workitem dialog
'click .newWorkItem': 'onNewWorkItemClick',
'change .newWorkItemModal .inputVsoProject': 'onNewVsoProjectChange',
'change .newWorkItemModal .type': 'onNewVsoWorkItemTypeChange',
'click .newWorkItemModal .copyDescription': 'onNewCopyDescriptionClick',
'click .newWorkItemModal .copyTemplate': 'onNewCopyTemplateClick',
'click .newWorkItemModal .accept': 'onNewWorkItemAcceptClick',
//Admin side pane
'click .cog': 'onCogClick',
'click .closeAdmin': 'onCloseAdminClick',
'change .settings .summary, .settings .details': 'onSettingChange',
//Details dialog
'click .showDetails': 'onShowDetailsClick',
//Link work item dialog
'click .link': 'onLinkClick',
'change .linkModal .project': 'onLinkVsoProjectChange',
'click .linkModal button.queryBtn': 'onLinkQueryButtonClick',
'click .linkModal button.reloadQueriesBtn': 'onLinkReloadQueriesButtonClick',
'click .linkModal button.accept': 'onLinkAcceptClick',
'click .linkModal button.search': 'onLinkSearchClick',
'click .linkModal a.workItemResult': 'onLinkResultClick',
//Unlink click
'click .unlink': 'onUnlinkClick',
'click .unlinkModal .accept': 'onUnlinkAcceptClick',
//Notify dialog
'click .notify': 'onNotifyClick',
'click .notifyModal .accept': 'onNotifyAcceptClick',
'click .notifyModal .copyLastComment': 'onCopyLastCommentClick',
//Refresh work items
'click .refreshWorkItemsLink': 'onRefreshWorkItemClick',
//Login
'click .user,.user-link': 'onUserIconClick',
'click .closeLogin': 'onCloseLoginClick',
'click .login-button': 'onLoginClick'
},
//#endregion
//#region Requests
requests: {
getComments: function () {
return {
url: helpers.fmt('/api/v2/tickets/%@/comments.json', this.ticket().id()),
type: 'GET'
};
},
addTagToTicket: function (tag) {
return {
url: helpers.fmt('/api/v2/tickets/%@/tags.json', this.ticket().id()),
type: 'PUT',
dataType: 'json',
data: {
"tags": [tag]
}
};
},
removeTagFromTicket: function (tag) {
return {
url: helpers.fmt('/api/v2/tickets/%@/tags.json', this.ticket().id()),
type: 'DELETE',
dataType: 'json',
data: {
"tags": [tag]
}
};
},
addPrivateCommentToTicket: function (text) {
return {
url: helpers.fmt('/api/v2/tickets/%@.json', this.ticket().id()),
type: 'PUT',
dataType: 'json',
data: {
"ticket": {
"comment": {
"public": false,
"body": text
}
}
}
};
},
saveSettings: function (data) {
return {
type: 'PUT',
url: helpers.fmt("/api/v2/apps/installations/%@.json", this.installationId() || INSTALLATION_ID),
dataType: 'json',
data: {
enabled: true,
settings: data
}
};
},
getVsoProjects: function (skip) { return this.vsoRequest('/_apis/projects', { $top: VSO_PROJECTS_PAGE_SIZE, $skip: skip || 0 } );},
getVsoProjectWorkItemTypes: function (projectId) { return this.vsoRequest(helpers.fmt('/%@/_apis/wit/workitemtypes', projectId)); },
getVsoProjectAreas: function (projectId) { return this.vsoRequest(helpers.fmt('/%@/_apis/wit/classificationnodes/areas', projectId), { $depth: 9999 } ); },
getVsoProjectWorkItemQueries: function (projectName) { return this.vsoRequest(helpers.fmt('/%@/_apis/wit/queries', projectName), { $depth: 2 }); },
getVsoFields: function () { return this.vsoRequest('/_apis/wit/fields'); },
getVsoWorkItems: function (ids) { return this.vsoRequest('/_apis/wit/workItems', { ids: ids, '$expand': 'relations' }); },
getVsoWorkItem: function (workItemId) { return this.vsoRequest(helpers.fmt('/_apis/wit/workItems/%@', workItemId), { '$expand': 'relations' }); },
getVsoWorkItemQueryResult: function (projectName, queryId) { return this.vsoRequest(helpers.fmt('/%@/_apis/wit/wiql/%@', projectName, queryId)); },
createVsoWorkItem: function (projectId, witName, data) {
return this.vsoRequest(helpers.fmt('/%@/_apis/wit/workitems/$%@', projectId, witName), undefined, {
type: 'PUT',
contentType: 'application/json-patch+json',
data: JSON.stringify(data),
headers: {
'X-HTTP-Method-Override': 'PATCH',
}
});
},
updateVsoWorkItem: function (workItemId, data) {
return this.vsoRequest(helpers.fmt('/_apis/wit/workItems/%@', workItemId), undefined, {
type: 'PUT',
contentType: 'application/json-patch+json',
data: JSON.stringify(data),
headers: {
'X-HTTP-Method-Override': 'PATCH',
},
});
},
updateMultipleVsoWorkItem: function (data) {
return this.vsoRequest('/_apis/wit/workItems', undefined, {
type: 'PUT',
contentType: 'application/json',
data: JSON.stringify(data),
headers: {
'X-HTTP-Method-Override': 'PATCH',
},
});
}
},
onGetVsoProjectsDone: function (projects) {
this.vm.projects = _.sortBy(this.vm.projects.concat(_.map(projects.value, function (project) {
return {
id: project.id,
name: project.name,
workItemTypes: []
};
})), function (project) {
return project.name.toLowerCase();
});
},
onGetVsoFieldsDone: function (data) {
this.vm.fields = _.map(data.value, function (field) {
return {
refName: field.referenceName,
name: field.name,
type: field.type
};
});
},
getLinkedVsoWorkItems: function (func) {
var vsoLinkedIds = this.getLinkedWorkItemIds();
var finish = function (workItems) {
if (func && _.isFunction(func)) { func(workItems); } else { this.displayMain(); }
this.onGetLinkedVsoWorkItemsDone(workItems);
}.bind(this);
if (!vsoLinkedIds || vsoLinkedIds.length === 0) {
finish([]);
return;
}
//make a call for each linked wi to get the data we need (web URL is not returned from the getVsoWorkItems)
var requests = _.map(vsoLinkedIds, function (workItemId) {
return this.ajax('getVsoWorkItem', workItemId);
}.bind(this));
//wait for all requests to complete
this.when.apply(this, requests)
.done(function () {
var linkedWorkItems = [];
if (vsoLinkedIds.length === 1) {
//just one wi: arguments is [data, status, jqXhr]
linkedWorkItems.push(arguments[0]);
} else {
//more than 1 wi: arguments is [[data1, status1, jqXhr1],...]
for (var i = 0; i < arguments.length; i++) {
linkedWorkItems.push(arguments[i][0]);
}
}
finish(linkedWorkItems);
}.bind(this))
.fail(function (jqXHR) {
this.displayMain(this.getAjaxErrorMessage(jqXHR));
}.bind(this));
//this.ajax('getVsoWorkItems', vsoLinkedIds.join(','))
// .done(function (data) { finish(data.value); })
// .fail(function (jqXHR) { this.displayMain(this.getAjaxErrorMessage(jqXHR)); }.bind(this));
},
onGetLinkedVsoWorkItemsDone: function (data) {
this.vmLocal.workItems = data;
_.each(this.vmLocal.workItems, function (workItem) {
workItem.title = helpers.fmt("%@: %@", workItem.id, this.getWorkItemFieldValue(workItem, "System.Title"));
}.bind(this));
this.drawWorkItems();
},
//#endregion
//#region Events Implementation
// App
onAppActivated: function (data) {
if (data.firstLoad) {
//Check if everything is ok to continue
if (!(this.setting('vso_account'))) {
return this.switchTo('finish_setup');
}
//set account url
this.vm.accountUrl = this.buildAccountUrl();
if (!this.store("auth_token_for_" + this.setting('vso_account'))) {
return this.switchTo('login');
}
//Private instance view model
this.vmLocal = { workItems: [] };
if (!this.vm.isAppLoadedOk) {
//Initialize global data
try {
this.vm.fieldSettings = JSON.parse(this.setting('vso_field_settings') || DEFAULT_FIELD_SETTINGS);
} catch (ex) {
services.notify(this.I18n.t('errorReadingFieldSettings'), 'alert');
this.vm.fieldSettings = JSON.parse(DEFAULT_FIELD_SETTINGS);
}
// Function to get all VSTS projects paginated if needed
var getAllVsoProjects = function() {
return this.promise(function(done, fail) {
var getPage = function (page) {
var skip = page * VSO_PROJECTS_PAGE_SIZE;
this.ajax('getVsoProjects', skip).done(function (data) {
// If the page is full, get a new page
if (data.count === VSO_PROJECTS_PAGE_SIZE) {
getPage(page + 1);
} else {
done();
}
}).fail(function (xhr, status, err) {
fail(xhr, status, err);
});
}.bind(this);
// Get First page
getPage(0);
}.bind(this));
}.bind(this);
this.when(
getAllVsoProjects(),
this.ajax('getVsoFields')
).done(function () {
this.vm.isAppLoadedOk = true;
this.getLinkedVsoWorkItems();
}.bind(this))
.fail(function (jqXHR, textStatus, err) {
this.switchTo('error_loading_app', {
invalidAccount: jqXHR.status === 404,
accountName: this.setting('vso_account')
});
}.bind(this));
} else {
this.getLinkedVsoWorkItems();
}
}
},
// UI
onNewWorkItemClick: function () {
var $modal = this.$('.newWorkItemModal').modal();
$modal.find('.modal-body').html(this.renderTemplate('loading'));
this.ajax('getComments').done(function (data) {
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(this.ticket().subject());
var projectCombo = $modal.find('.project');
this.fillComboWithProjects(projectCombo);
projectCombo.change();
}.bind(this));
},
onNewVsoProjectChange: function () {
var $modal = this.$('.newWorkItemModal');
var projId = $modal.find('.project').val();
this.showSpinnerInModal($modal);
this.loadProjectMetadata(projId)
.done(function () {
this.drawAreasList($modal.find('.area'), projId);
this.drawTypesList($modal.find('.type'), projId);
$modal.find('.type').change();
this.hideSpinnerInModal($modal);
}.bind(this))
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR));
}.bind(this));
},
onNewVsoWorkItemTypeChange: function () {
var $modal = this.$('.newWorkItemModal');
var project = this.getProjectById($modal.find('.project').val());
var workItemType = this.getWorkItemTypeByName(project, $modal.find('.type').val());
//Check if we have severity
if (this.hasFieldDefined(workItemType, "Microsoft.VSTS.Common.Severity")) {
$modal.find('.severityInput').show();
} else {
$modal.find('.severityInput').hide();
}
},
onNewCopyDescriptionClick: function (event) {
event.preventDefault();
this.$('.newWorkItemModal .description').val(this.ticket().description());
},
onNewCopyTemplateClick: function (event) {
event.preventDefault();
this.$('.newWorkItemModal .description').val(this.setting('vso_wi_description_template'));
},
onNewWorkItemAcceptClick: function () {
var $modal = this.$('.newWorkItemModal').modal();
//check project
var proj = this.getProjectById($modal.find('.project').val());
if (!proj) { return this.showErrorInModal($modal, this.I18n.t("modals.new.errProjRequired")); }
// read area id
var areaId = $modal.find('.area').val();
//check work item type
var workItemType = this.getWorkItemTypeByName(proj, $modal.find('.type').val());
if (!workItemType) { return this.showErrorInModal($modal, this.I18n.t("modals.new.errWorkItemTypeRequired")); }
//check summary
var summary = $modal.find(".summary").val();
if (!summary) { return this.showErrorInModal($modal, this.I18n.t("modals.new.errSummaryRequired")); }
var description = $modal.find(".description").val();
var attachments = this.getSelectedAttachments($modal);
var 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(
this.buildTicketLinkUrl(),
VSO_ZENDESK_LINK_TO_TICKET_PREFIX + this.ticket().id()));
//Add hyperlinks to attachments
operations = operations.concat(this.buildPatchToAddWorkItemAttachments(attachments));
this.showSpinnerInModal($modal);
this.ajax('createVsoWorkItem', proj.id, workItemType.name, operations)
.done(function (data) {
var newWorkItemId = data.id;
//sanity check due tfs returning 200 ok but with exception
if (newWorkItemId > 0) { this.linkTicket(newWorkItemId); }
services.notify(this.I18n.t('notify.workItemCreated').fmt(newWorkItemId));
this.getLinkedVsoWorkItems(function () { this.closeModal($modal); }.bind(this));
}.bind(this))
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR));
}.bind(this));
},
onCogClick: function () {
this.switchTo('admin');
this.drawSettings();
},
onCloseAdminClick: function () {
this.displayMain();
},
onSettingChange: function () {
var self = this;
var fieldSettings = {};
this.$('tr').each(function () {
var line = self.$(this);
var fieldName = line.attr('data-refName');
if (!fieldName) return true; //continue
var inSummary = line.find('.summary').is(':checked');
var inDetails = line.find('.details').is(':checked');
if (inSummary || inDetails) {
fieldSettings[fieldName] = {
summary: inSummary,
details: inDetails
};
} else if (fieldSettings[fieldName]) {
delete fieldName[fieldName];
}
});
this.vm.fieldSettings = fieldSettings;
this.ajax('saveSettings', { vso_field_settings: JSON.stringify(fieldSettings) })
.done(function () {
services.notify(this.I18n.t('admin.settingsSaved'));
}.bind(this));
},
onShowDetailsClick: function (event) {
var $modal = this.$('.detailsModal').modal();
$modal.find('.modal-header h3').html(this.I18n.t('modals.details.loading'));
$modal.find('.modal-body').html(this.renderTemplate('loading'));
var id = this.$(event.target).closest('.workItem').attr('data-id');
var workItem = this.getWorkItemById(id);
workItem = 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));
},
onLinkClick: function () {
var $modal = this.$('.linkModal').modal();
$modal.find('.modal-footer button').removeAttr('disabled');
$modal.find('.modal-body').html(this.renderTemplate('link'));
$modal.find("button.search").show();
var projectCombo = $modal.find('.project');
this.fillComboWithProjects(projectCombo);
projectCombo.change();
},
onLinkSearchClick: function () {
var $modal = this.$('.linkModal');
$modal.find(".search-section").show();
},
onLinkResultClick: function (event) {
event.preventDefault();
var $modal = this.$('.linkModal');
var id = this.$(event.target).closest('.workItemResult').attr('data-id');
$modal.find('.inputVsoWorkItemId').val(id);
$modal.find('.search-section').hide();
},
onLinkVsoProjectChange: function () {
this.loadQueriesList();
},
onLinkReloadQueriesButtonClick: function () {
this.loadQueriesList(true);
},
loadQueriesList: function (reload) {
var $modal = this.$('.linkModal');
var projId = $modal.find('.project').val();
this.showSpinnerInModal($modal);
this.loadProjectWorkItemQueries(projId, reload)
.done(function () {
this.drawQueriesList($modal.find('.query'), projId);
this.hideSpinnerInModal($modal);
}.bind(this))
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR));
}.bind(this));
},
onLinkQueryButtonClick: function () {
var $modal = this.$('.linkModal');
var projId = $modal.find('.project').val();
var queryId = $modal.find('.query').val();
var drawQueryResults = function (results, countQueryItemsResult) {
var 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.hideSpinnerInModal($modal);
}.bind(this);
this.showSpinnerInModal($modal);
this.ajax('getVsoWorkItemQueryResult', this.getProjectById(projId).name, queryId)
.done(function (data) {
var 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');
}
};
var ids = getWorkItemsIdsFromQueryResult(data);
if (!ids || ids.length === 0) {
return drawQueryResults([], 0);
}
this.ajax('getVsoWorkItems', _.first(ids, 200).join(',')).done(function (results) {
drawQueryResults(results.value, ids.length);
});
}.bind(this))
.fail(function (jqXHR, textStatus, errorThrown) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR, this.I18n.t('modals.link.errCannotGetWorkItem')));
}.bind(this));
},
onLinkAcceptClick: function (event) {
var $modal = this.$('.linkModal');
var workItemId = $modal.find('.inputVsoWorkItemId').val();
if (!/^([0-9]+)$/.test(workItemId)) {
return this.showErrorInModal($modal, this.I18n.t('modals.link.errWorkItemIdNaN'));
}
if (this.isAlreadyLinkedToWorkItem(workItemId)) {
return this.showErrorInModal($modal, this.I18n.t('modals.link.errAlreadyLinked'));
}
this.showSpinnerInModal($modal);
var updateWorkItem = function (workItem) {
//Let's check if there is already a link in the WI returned data
var currentLink = _.find(workItem.relations || [], function (link) {
if (link.rel.toLowerCase() === "hyperlink" && link.attributes.name === (VSO_ZENDESK_LINK_TO_TICKET_PREFIX + this.ticket().id())) {
return link;
}
}.bind(this));
var finish = function () {
this.linkTicket(workItemId);
services.notify(this.I18n.t('notify.workItemLinked').fmt(workItemId));
this.getLinkedVsoWorkItems(function () { this.closeModal($modal); }.bind(this));
}.bind(this);
if (currentLink) {
finish();
} else {
var addLinkOperation = this.buildPatchToAddWorkItemHyperlink(
this.buildTicketLinkUrl(),
VSO_ZENDESK_LINK_TO_TICKET_PREFIX + this.ticket().id());
this.ajax('updateVsoWorkItem', workItemId, [addLinkOperation])
.done(function () {
finish();
}.bind(this))
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR, this.I18n.t('modals.link.errCannotUpdateWorkItem')));
}.bind(this));
}
}.bind(this);
//Get work item and then update
this.ajax('getVsoWorkItem', workItemId)
.done(function (data) {
updateWorkItem(data);
}.bind(this))
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR, this.I18n.t('modals.link.errCannotGetWorkItem')));
}.bind(this));
},
onUnlinkClick: function (event) {
var id = this.$(event.target).closest('.workItem').attr('data-id');
var workItem = this.getWorkItemById(id);
var $modal = this.$('.unlinkModal').modal();
$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', id);
},
onUnlinkAcceptClick: function (event) {
event.preventDefault();
var $modal = this.$(event.target).closest('.unlinkModal');
this.showSpinnerInModal($modal);
var workItemId = $modal.attr('data-id');
var updateWorkItem = function (workItem) {
//Calculate the positions of links to remove
var posOfLinksToRemove = [];
_.each(workItem.relations, function (link, idx) {
if (link.rel.toLowerCase() === 'hyperlink' &&
(link.attributes.name === VSO_ZENDESK_LINK_TO_TICKET_PREFIX + this.ticket().id() ||
link.attributes.name === VSO_ZENDESK_LINK_TO_TICKET_ATTACHMENT_PREFIX + this.ticket().id())) {
posOfLinksToRemove.push(idx - posOfLinksToRemove.length);
}
}.bind(this));
var finish = function () {
this.unlinkTicket(workItem.id);
services.notify(this.I18n.t('notify.workItemUnlinked').fmt(workItem.id));
this.getLinkedVsoWorkItems(function () { this.closeModal($modal); }.bind(this));
}.bind(this);
if (posOfLinksToRemove.length === 0) {
finish();
} else {
var operations = [{ op: "test", path: "/rev", value: workItem.rev }]
.concat(_.map(posOfLinksToRemove, function (pos) {
return this.buildPatchToRemoveWorkItemHyperlink(pos);
}.bind(this)));
this.ajax('updateVsoWorkItem', workItemId, operations)
.done(function () { finish(); })
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR, this.I18n.t('modals.unlink.errUnlink')));
}.bind(this));
}
}.bind(this);
//Get work item to get the last revision and then update
this.ajax('getVsoWorkItem', workItemId)
.done(function (workItem) { updateWorkItem(workItem); }.bind(this))
.fail(function (jqXHR) { this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR)); }.bind(this));
},
onNotifyClick: function () {
var $modal = this.$('.notifyModal');
$modal.find('.modal-body').html(this.renderTemplate('loading'));
$modal.modal();
this.ajax('getComments').done(function (data) {
this.lastComment = data.comments[data.comments.length - 1].body;
var 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);
}.bind(this));
},
onNotifyAcceptClick: function () {
var $modal = this.$('.notifyModal');
var text = $modal.find('textarea').val();
if (!text) { return this.showErrorInModal($modal, this.I18n.t("modals.notify.errCommentRequired")); }
var attachments = this.getSelectedAttachments($modal);
this.showSpinnerInModal($modal);
//Refresh linked VSO work items
this.getLinkedVsoWorkItems(function (workItems) {
//create an array of promises with individual request
var requests = _.map(workItems, function (workItem) {
//exclude selected attachments that are already in the work item
var newAttachments = _.reject(attachments, function (att) {
return _.some(workItem.relations || [], function (rel) {
return rel.url === att.url;
});
});
var operations = [this.buildPatchToAddWorkItemField("System.History", text)].concat(
this.buildPatchToAddWorkItemAttachments(newAttachments));
return this.ajax('updateVsoWorkItem', workItem.id, operations);
}.bind(this));
//wait for all requests to complete
this.when.apply(this, requests)
//this.ajax('updateMultipleVsoWorkItem', updatePayload)
.done(function () {
var ticketMsg = [this.I18n.t('notify.message', { name: this.currentUser().name() }), text].join("\n\r\n\r");
this.ajax('addPrivateCommentToTicket', ticketMsg);
services.notify(this.I18n.t('notify.notification'));
this.closeModal($modal);
}.bind(this))
.fail(function (jqXHR) {
this.showErrorInModal($modal, this.getAjaxErrorMessage(jqXHR));
}.bind(this));
}.bind(this));
},
onCopyLastCommentClick: function (event) {
event.preventDefault();
this.$('.notifyModal').find('textarea').val(this.lastComment);
},
onRefreshWorkItemClick: function (event) {
event.preventDefault();
this.$('.workItemsError').hide();
this.switchTo('loading');
this.getLinkedVsoWorkItems();
},
onLoginClick: function (event) {
event.preventDefault();
var vso_username = this.$('.vso_username').val();
var vso_password = this.$('.vso_password').val();
if (!vso_username || !vso_password) {
this.$(".login-form").find('.errors').text(this.I18n.t("login.errRequiredFields")).show();
return;
}
this.authString(vso_username, vso_password);
services.notify(this.I18n.t('notify.credentialsSaved'));
this.switchTo('loading');
if (!this.vm.isAppLoadedOk) {
this.onAppActivated({ firstLoad: true });
} else {
this.getLinkedVsoWorkItems();
}
},
onCloseLoginClick: function () {
this.displayMain();
},
onUserIconClick: function () {
this.switchTo('login');
},
//#endregion
//#region Drawing
displayMain: function (err) {
if (this.vm.isAppLoadedOk) {
this.$('.cog').toggle(this.isAdmin());
this.switchTo('main');
if (!err) {
this.drawWorkItems();
} else {
this.$('.workItemsError').show();
}
} else {
this.$('.cog').toggle(false);
this.switchTo('error_loading_app');
}
},
drawWorkItems: function (data) {
var workItems = _.map(data || this.vmLocal.workItems, function (workItem) {
var tmp = this.attachRestrictedFieldsToWorkItem(workItem, 'summary');
return tmp;
}.bind(this));
this.$('.workItems').html(this.renderTemplate('workItems', { workItems: workItems }));
this.$('.buttons .notify').prop('disabled', !workItems.length);
},
drawTypesList: function (select, projectId) {
var project = this.getProjectById(projectId);
select.html(this.renderTemplate('types', { types: project.workItemTypes }));
},
drawAreasList: function (select, projectId) {
var project = this.getProjectById(projectId);
select.html(this.renderTemplate('areas', { areas: project.areas }));
},
drawQueriesList: function (select, projectId) {
var project = this.getProjectById(projectId);
var 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) + "."));
}, ""));
},
drawSettings: function () {
var settings = _.sortBy(
_.map(this.vm.fields, function (field) {
var current = this.vm.fieldSettings[field.refName];
if (current) { field = _.extend(field, current); }
return field;
}.bind(this)), function (f) { return f.name; });
var html = this.renderTemplate('settings', { settings: settings });
this.$('.content').html(html);
},
showSpinnerInModal: function ($modal) {
if ($modal.find('.modal-body form')) { $modal.find('.modal-body form').hide(); }
if ($modal.find('.modal-body .loading')) { $modal.find('.modal-body .loading').show(); }
if ($modal.find('.modal-footer button')) { $modal.find('.modal-footer button').attr('disabled', 'disabled'); }
},
hideSpinnerInModal: function ($modal) {
if ($modal.find('.modal-body form')) { $modal.find('.modal-body form').show(); }
if ($modal.find('.modal-body .loading')) { $modal.find('.modal-body .loading').hide(); }
if ($modal.find('.modal-footer button')) { $modal.find('.modal-footer button').prop('disabled', false); }
},
showErrorInModal: function ($modal, err) {
this.hideSpinnerInModal($modal);
if ($modal.find('.modal-body .errors')) { $modal.find('.modal-body .errors').text(err).show(); }
},
closeModal: function ($modal) {
$modal.find('#loading').hide();
$modal.modal('hide').find('.modal-footer button').attr('disabled', '');
},
fillComboWithProjects: function (el) {
el.html(_.reduce(this.vm.projects, function (options, project) {
return "%@<option value='%@'>%@</option>".fmt(options, project.id, project.name);
}, ""));
},
//#endregion
//#region Helpers
isAdmin: function () {
return this.currentUser().role() === 'admin';
},
vsoUrl: function (url, parameters) {
url = (url[0] === '/') ? url.slice(1) : url;
var full = [this.vm.accountUrl, url].join('/');
if (parameters) {
full += '?' + _.map(parameters, function (value, key) {
return [key, value].join('=');
}).join('&');
}
return full;
},
authString: function (vso_username, vso_password) {
if (vso_username && vso_password) {
var b64 = Base64.encode([vso_username, vso_password].join(':'));
this.store('auth_token_for_' + this.setting('vso_account'), b64);
}
return helpers.fmt("Basic %@", this.store('auth_token_for_' + this.setting('vso_account')));
},
vsoRequest: function (url, parameters, options) {
var requestOptions = _.extend({
url: this.vsoUrl(url, parameters),
dataType: 'json',
}, options);
var fixedHeaders = {
'Authorization': this.authString(),
'Accept': helpers.fmt("application/json;api-version=%@", this.getVsoResourceVersion(url))
};
requestOptions.headers = _.extend(fixedHeaders, options ? options.headers : {});
return requestOptions;
},
getVsoResourceVersion: function (url) {
var resource = url.split("/_apis/")[1].split("/")[0];
return VSO_API_RESOURCE_VERSION[resource] || VSO_API_DEFAULT_VERSION;
},
attachRestrictedFieldsToWorkItem: function (workItem, type) {
var fields = _.compact(_.map(this.vm.fieldSettings, function (value, key) {
if (value[type]) {
if (_.has(workItem.fields, key)) {
return {
refName: key,
name: _.find(this.vm.fields, function (f) { return f.refName == key; }).name,
value: workItem.fields[key],
isHtml: this.isHtmlContentField(key)
};
}
}
}.bind(this)));
return _.extend(workItem, { restricted_fields: fields });
},
getWorkItemById: function (id) {
return _.find(this.vmLocal.workItems, function (workItem) { return workItem.id == id; });
},
getProjectById: function (id) {
return _.find(this.vm.projects, function (proj) { return proj.id == id; });
},
getWorkItemTypeByName: function (project, name) {
return _.find(project.workItemTypes, function (wit) { return wit.name == name; });
},
getFieldByFieldRefName: function (fieldRefName) {
return _.find(this.vm.fields, function (f) { return f.refName == fieldRefName; });
},
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;
});
},
linkTicket: function (workItemId) {
var linkVsoTag = TAG_PREFIX + workItemId;
this.ticket().tags().add(linkVsoTag);
this.ajax('addTagToTicket', linkVsoTag);
},
unlinkTicket: function (workItemId) {
var linkVsoTag = TAG_PREFIX + workItemId;
this.ticket().tags().remove(linkVsoTag);
this.ajax('removeTagFromTicket', linkVsoTag);
},
buildTicketLinkUrl: function () {
return helpers.fmt("https://%@.zendesk.com/agent/#/tickets/%@", this.currentAccount().subdomain(), this.ticket().id());
},
getLinkedWorkItemIds: function () {
return _.compact(this.ticket().tags().map(function (t) {
var p = t.indexOf(TAG_PREFIX);
if (p === 0) { return t.slice(TAG_PREFIX.length); }
}));
},
isAlreadyLinkedToWorkItem: function (id) { return _.contains(this.getLinkedWorkItemIds(), id); },
loadProjectMetadata: function (projectId) {
var project = this.getProjectById(projectId);
if (project.metadataLoaded === true) { return this.promise(function (done) { done(); }); }
var loadWorkItemTypes = this.ajax('getVsoProjectWorkItemTypes', project.id).done(function (data) {
project.workItemTypes = this.restrictToAllowedWorkItems(data.value);
}.bind(this));
var loadAreas = this.ajax('getVsoProjectAreas', project.id).done(function (rootArea) {
var areas = [];
// Flatten areas to format \Area 1\Area 1.1
var 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(rootArea);
project.areas = _.sortBy(areas, function(area) { return area.name; } );
}.bind(this));
return this.when(
loadWorkItemTypes,
loadAreas
).done(function () {
project.metadataLoaded = true;
});
},
loadProjectWorkItemQueries: function (projectId, reload) {
var project = this.getProjectById(projectId);
if (project.queries && !reload) { return this.promise(function (done) { done(); }); }
//Let's load project queries
return this.ajax('getVsoProjectWorkItemQueries', project.name).done(function (data) {
project.queries = data.value;
}.bind(this));
},
restrictToAllowedWorkItems: function (wits) {
return _.filter(wits, function (wit) { return _.contains(VSO_WI_TYPES_WHITE_LISTS, wit.name); });
},
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
};
},
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;
}
},
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)
};
},
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 {
serverErrMsg = jqXHR.responseText.substring(0, 50) + "...";
}
var detail = this.I18n.t("errorServer").fmt(jqXHR.status, jqXHR.statusText, serverErrMsg);
return errMsg + " " + detail;
},
buildPatchToAddWorkItemAttachments: function (attachments) {
return _.map(attachments, function (att) {
return this.buildPatchToAddWorkItemHyperlink(
att.url,
VSO_ZENDESK_LINK_TO_TICKET_ATTACHMENT_PREFIX + this.ticket().id(),
att.name);
}.bind(this));
},
getSelectedAttachments: function ($modal) {
var attachments = [];
$modal.find('.attachments input').each(function (ix, el) {
var $el = this.$(el);
if ($el.is(':checked')) {
attachments.push({
url: $el.val(),
name: $el.data('fileName')
});
}
}.bind(this));
return attachments;
},
buildAccountUrl: function () {
var baseUrl;
var setting = this.setting('vso_account');
var loweredSetting = setting.toLowerCase();
if (loweredSetting.indexOf('http://') === 0 || loweredSetting.indexOf('https://') === 0) {
baseUrl = setting;
} else {
baseUrl = helpers.fmt(VSO_URL_FORMAT, setting);
}
baseUrl = (baseUrl[baseUrl.length - 1] === '/') ? baseUrl.slice(0, -1) : baseUrl;
//check if collection defined
if (baseUrl.lastIndexOf('/') <= 'https://'.length) {
baseUrl = baseUrl + '/DefaultCollection';
}
return baseUrl;
}
//#endregion
};
}());