kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
497 lines
15 KiB
497 lines
15 KiB
//-- copyright
|
|
// OpenProject Backlogs Plugin
|
|
//
|
|
// Copyright (C)2013-2014 the OpenProject Foundation (OPF)
|
|
// Copyright (C)2011 Stephan Eckardt, Tim Felgentreff, Marnen Laibow-Koser, Sandro Munda
|
|
// Copyright (C)2010-2011 friflaj
|
|
// Copyright (C)2010 Maxime Guilbot, Andrew Vit, Joakim Kolsjö, ibussieres, Daniel Passos, Jason Vasquez, jpic, Emiliano Heyns
|
|
// Copyright (C)2009-2010 Mark Maglana
|
|
// Copyright (C)2009 Joe Heck, Nate Lowrie
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify it under
|
|
// the terms of the GNU General Public License version 3.
|
|
//
|
|
// OpenProject Backlogs is a derivative work based on ChiliProject Backlogs.
|
|
// The copyright follows:
|
|
// Copyright (C) 2010-2011 - Emiliano Heyns, Mark Maglana, friflaj
|
|
// Copyright (C) 2011 - Jens Ulferts, Gregor Schmidt - Finn GmbH - Berlin, Germany
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
//
|
|
// See doc/COPYRIGHT.rdoc for more details.
|
|
//++
|
|
|
|
/***************************************
|
|
MODEL
|
|
Common methods for sprint, work_package,
|
|
story, task, and impediment
|
|
***************************************/
|
|
|
|
RB.Model = (function ($) {
|
|
return RB.Object.create({
|
|
|
|
initialize: function (el) {
|
|
this.$ = $(el);
|
|
this.el = el;
|
|
},
|
|
|
|
afterCreate: function (data, textStatus, xhr) {
|
|
// Do nothing. Child objects may optionally override this
|
|
},
|
|
|
|
afterSave: function (data, textStatus, xhr) {
|
|
var isNew, result;
|
|
|
|
isNew = this.isNew();
|
|
result = RB.Factory.initialize(RB.Model, data);
|
|
|
|
this.unmarkSaving();
|
|
this.refresh(result);
|
|
|
|
if (isNew) {
|
|
this.$.attr('id', result.$.attr('id'));
|
|
this.afterCreate(data, textStatus, xhr);
|
|
}
|
|
else {
|
|
this.afterUpdate(data, textStatus, xhr);
|
|
}
|
|
},
|
|
|
|
afterUpdate: function (data, textStatus, xhr) {
|
|
// Do nothing. Child objects may optionally override this
|
|
},
|
|
|
|
beforeSave: function () {
|
|
// Do nothing. Child objects may or may not override this method
|
|
},
|
|
|
|
cancelEdit: function () {
|
|
this.endEdit();
|
|
if (this.isNew()) {
|
|
this.$.hide('blind');
|
|
}
|
|
},
|
|
|
|
close: function () {
|
|
this.$.addClass('closed');
|
|
},
|
|
|
|
copyFromDialog: function () {
|
|
var editors;
|
|
|
|
if (this.$.find(".editors").length === 0) {
|
|
editors = $("<div class='editors'></div>").appendTo(this.$);
|
|
}
|
|
else {
|
|
editors = this.$.find(".editors").first();
|
|
}
|
|
editors.html("");
|
|
editors.append($("#" + this.getType().toLowerCase() + "_editor").children(".editor"));
|
|
this.saveEdits();
|
|
},
|
|
|
|
displayEditor: function (editor) {
|
|
var pos = this.$.offset(),
|
|
self = this,
|
|
baseClasses;
|
|
|
|
baseClasses = 'ui-button ui-widget ui-state-default ui-corner-all';
|
|
|
|
editor.dialog({
|
|
buttons: [
|
|
{
|
|
text: 'OK',
|
|
class: 'button -highlight',
|
|
click: function () {
|
|
self.copyFromDialog();
|
|
$(this).dialog("close");
|
|
}
|
|
},
|
|
{
|
|
text: 'Cancel',
|
|
class: 'button',
|
|
click: function () {
|
|
self.cancelEdit();
|
|
$(this).dialog("close");
|
|
}
|
|
},
|
|
],
|
|
close: function (e, ui) {
|
|
if (e.which === 1 || e.which === 27) {
|
|
self.cancelEdit();
|
|
}
|
|
},
|
|
dialogClass: this.getType().toLowerCase() + '_editor_dialog',
|
|
modal: true,
|
|
position: [pos.left - $(document).scrollLeft(), pos.top - $(document).scrollTop()],
|
|
resizable: false,
|
|
title: (this.isNew() ? this.newDialogTitle() : this.editDialogTitle())
|
|
});
|
|
editor.find(".editor").first().focus();
|
|
$('.button').removeClass(baseClasses);
|
|
$('.ui-icon-closethick').prop('title', 'close');
|
|
},
|
|
|
|
edit: function () {
|
|
var editor = this.getEditor(),
|
|
self = this,
|
|
maxTabIndex = 0;
|
|
|
|
$('.stories .editors .editor').each(function (index) {
|
|
var value;
|
|
|
|
value = parseInt($(this).attr('tabindex'), 10);
|
|
|
|
if (maxTabIndex < value) {
|
|
maxTabIndex = value;
|
|
}
|
|
});
|
|
|
|
this.$.find('.editable').each(function (index) {
|
|
var field, fieldType, fieldLabel, fieldName, fieldOrder, input, newInput,
|
|
typeId, statusId ;
|
|
|
|
field = $(this);
|
|
fieldId = field.attr('field_id');
|
|
fieldName = field.attr('fieldname');
|
|
fieldLabel = field.attr('fieldlabel');
|
|
fieldOrder = parseInt(field.attr('fieldorder'), 10);
|
|
fieldType = field.attr('fieldtype') || 'input';
|
|
|
|
if (!fieldLabel) {
|
|
fieldLabel = fieldName.replace(/_/ig, " ").replace(/ id$/ig, "");
|
|
}
|
|
|
|
if (fieldType === 'select') {
|
|
// Special handling for status_id => they are dependent of type_id
|
|
if (fieldName === 'status_id') {
|
|
typeId = $.trim(self.$.find('.type_id .v').html());
|
|
// when creating stories we need to query the select directly
|
|
if (typeId == '') {
|
|
typeId = $('#type_id_options').val();
|
|
}
|
|
statusId = $.trim(self.$.find('.status_id .v').html());
|
|
input = self.findFactory(typeId, statusId, fieldName);
|
|
}
|
|
else if (fieldName === 'type_id'){
|
|
input = $('#' + fieldName + '_options').clone(true);
|
|
// if the type changes the status dropdown has to be modified
|
|
input.change(function(){
|
|
typeId = $(this).val();
|
|
statusId = $.trim(self.$.find('.status_id .v').html());
|
|
newInput = self.findFactory(typeId, statusId, 'status_id');
|
|
newInput = self.prepareInputFromFactory(newInput,fieldId,'status_id',fieldOrder,maxTabIndex);
|
|
newInput = self.replaceStatusForNewType(input, newInput, $(this).parent().find('.status_id').val(), editor);
|
|
});
|
|
}
|
|
else {
|
|
input = $('#' + fieldName + '_options').clone(true);
|
|
}
|
|
}
|
|
else {
|
|
input = $(document.createElement(fieldType));
|
|
}
|
|
|
|
input = self.prepareInputFromFactory(input, fieldId, fieldName, fieldOrder, maxTabIndex);
|
|
|
|
// Copy the value in the field to the input element
|
|
input.val(fieldType === 'select' ? field.children('.v').first().text() : field.text());
|
|
|
|
|
|
// Add a date picker if field is a date field
|
|
if (field.hasClass("date")) {
|
|
input.datepicker({
|
|
changeMonth: true,
|
|
changeYear: true,
|
|
closeText: 'Close',
|
|
dateFormat: 'yy-mm-dd',
|
|
firstDay: 1,
|
|
showOn: 'button',
|
|
onClose: function () {
|
|
$(this).focus();
|
|
},
|
|
selectOtherMonths: true,
|
|
showAnim: '',
|
|
showButtonPanel: true,
|
|
showOtherMonths: true
|
|
});
|
|
|
|
// Remove click-bindings from div - since leaving the edit modus removes the input
|
|
// and creates a new one
|
|
// Open the datepicker when you click on the div (before in edit-mode)
|
|
field.unbind("click");
|
|
field.click(function(){input.datepicker("show");});
|
|
|
|
// So that we won't need a datepicker button to re-show it
|
|
input.mouseup(function () {
|
|
$(this).datepicker("show");
|
|
});
|
|
}
|
|
|
|
// Record in the model's root element which input field had the last focus. We will
|
|
// use this information inside RB.Model.refresh() to determine where to return the
|
|
// focus after the element has been refreshed with info from the server.
|
|
input.focus(function () {
|
|
self.$.data('focus', $(this).attr('name'));
|
|
});
|
|
|
|
input.blur(function () {
|
|
self.$.data('focus', '');
|
|
});
|
|
|
|
$("<label />").attr({
|
|
for: input.attr('id'),
|
|
}).text(fieldLabel).appendTo(editor);
|
|
input.appendTo(editor);
|
|
});
|
|
|
|
this.displayEditor(editor);
|
|
this.editorDisplayed(editor);
|
|
return editor;
|
|
},
|
|
|
|
findFactory: function (typeId, statusId, fieldName){
|
|
// Find a factory
|
|
newInput = $('#' + fieldName + '_options_' + typeId + '_' + statusId);
|
|
if (newInput.length === 0) {
|
|
// when no list found, only offer the default status
|
|
// no list = combination is not valid / user has no rights -> workflow
|
|
newInput = $('#status_id_options_default_' + statusId);
|
|
}
|
|
newInput = newInput.clone(true);
|
|
return newInput;
|
|
},
|
|
|
|
prepareInputFromFactory: function (input,fieldId,fieldName,fieldOrder, maxTabIndex) {
|
|
input.attr('id', fieldName + '_' + fieldId);
|
|
input.attr('name', fieldName);
|
|
input.attr('tabindex', fieldOrder + maxTabIndex);
|
|
input.addClass(fieldName);
|
|
input.addClass('editor');
|
|
input.removeClass('template');
|
|
input.removeClass('helper');
|
|
return input;
|
|
},
|
|
|
|
replaceStatusForNewType: function (input,newInput, statusId, editor) {
|
|
// Append an empty field and select it in case the old status is not available
|
|
newInput.val(statusId); // try to set the status
|
|
if (newInput.val() !== statusId){
|
|
newInput.append(new Option('',''));
|
|
newInput.val('');
|
|
}
|
|
newInput.focus(function () {
|
|
self.$.data('focus', $(this).attr('name'));
|
|
});
|
|
|
|
newInput.blur(function () {
|
|
self.$.data('focus', '');
|
|
});
|
|
// Find the old status dropdown and replace it with the new one
|
|
input.parent().find('.status_id').replaceWith(newInput);
|
|
},
|
|
|
|
// Override this method to change the dialog title
|
|
editDialogTitle: function () {
|
|
return "Edit " + this.getType();
|
|
},
|
|
|
|
editorDisplayed: function (editor) {
|
|
// Do nothing. Child objects may override this.
|
|
},
|
|
|
|
endEdit: function () {
|
|
this.$.removeClass('editing');
|
|
},
|
|
|
|
error: function (xhr, textStatus, error) {
|
|
this.markError();
|
|
RB.Dialog.msg($(xhr.responseText).find('.errors').html());
|
|
this.processError(xhr, textStatus, error);
|
|
},
|
|
|
|
getEditor: function () {
|
|
var editorId, editor;
|
|
// Create the model editor if it does not yet exist
|
|
editorId = this.getType().toLowerCase() + "_editor";
|
|
|
|
editor = $("#" + editorId).html("");
|
|
|
|
if (editor.length === 0) {
|
|
editor = $("<div id='" + editorId + "'></div>").appendTo("body");
|
|
}
|
|
return editor;
|
|
},
|
|
|
|
getID: function () {
|
|
return this.$.children('.id').children('.v').text();
|
|
},
|
|
|
|
getType: function () {
|
|
throw "Child objects must override getType()";
|
|
},
|
|
|
|
handleClick: function (e) {
|
|
var field, model, j, editor;
|
|
|
|
field = $(this);
|
|
model = field.parents('.model').first().data('this');
|
|
j = model.$;
|
|
|
|
if (!j.hasClass('editing') && !j.hasClass('dragging') && !j.hasClass('prevent_edit') && !$(e.target).hasClass('prevent_edit')) {
|
|
editor = model.edit();
|
|
editor.find('.' + $(e.currentTarget).attr('fieldname') + '.editor').focus();
|
|
}
|
|
},
|
|
|
|
handleSelect: function (e) {
|
|
var j = $(this),
|
|
self = j.data('this');
|
|
|
|
if (!$(e.target).hasClass('editable') &&
|
|
!$(e.target).hasClass('checkbox') &&
|
|
!j.hasClass('editing') &&
|
|
e.target.tagName !== 'A' &&
|
|
!j.hasClass('dragging')) {
|
|
|
|
self.setSelection(!self.isSelected());
|
|
}
|
|
},
|
|
|
|
isClosed: function () {
|
|
return this.$.hasClass('closed');
|
|
},
|
|
|
|
isNew: function () {
|
|
return this.getID() === "";
|
|
},
|
|
|
|
markError: function () {
|
|
this.$.addClass('error icon icon-bug');
|
|
},
|
|
|
|
markIfClosed: function () {
|
|
throw "Child objects must override markIfClosed()";
|
|
},
|
|
|
|
markSaving: function () {
|
|
this.$.addClass('saving');
|
|
},
|
|
|
|
// Override this method to change the dialog title
|
|
newDialogTitle: function () {
|
|
return "New " + this.getType();
|
|
},
|
|
|
|
open: function () {
|
|
this.$.removeClass('closed');
|
|
},
|
|
|
|
processError: function (x, t, e) {
|
|
// Override as needed
|
|
},
|
|
|
|
refresh: function (obj) {
|
|
this.$.html(obj.$.html());
|
|
|
|
if (obj.$.length > 1) {
|
|
// execute script tags, that were attached to the sources
|
|
obj.$.filter('script').each(function () {
|
|
try {
|
|
$.globalEval($(this).html());
|
|
}
|
|
catch (e) {
|
|
}
|
|
});
|
|
}
|
|
|
|
if (obj.isClosed()) {
|
|
this.close();
|
|
} else {
|
|
this.open();
|
|
}
|
|
this.refreshed();
|
|
},
|
|
|
|
refreshed: function () {
|
|
// Override as needed
|
|
},
|
|
|
|
saveDirectives: function () {
|
|
throw "Child object must implement saveDirectives()";
|
|
},
|
|
|
|
saveEdits: function () {
|
|
var j = this.$,
|
|
self = this,
|
|
editors = j.find('.editor'),
|
|
saveDir;
|
|
|
|
// Copy the values from the fields to the proper html elements
|
|
editors.each(function (index) {
|
|
var editor, fieldName;
|
|
|
|
editor = $(this);
|
|
fieldName = editor.attr('name');
|
|
if (this.type.match(/select/)) {
|
|
// if the user changes the type and that type does not offer the status
|
|
// of the current story, the status field is set to blank
|
|
// if the user saves this edit we will receive a validation error
|
|
// the following 3 lines will prevent the override of the status id
|
|
// otherwise we would loose the status id of the current ticket
|
|
if (!(editor.val() === '' && fieldName === 'status_id')){
|
|
j.children('div.' + fieldName).children('.v').text(editor.val());
|
|
}
|
|
|
|
j.children('div.' + fieldName).children('.t').text(editor.children(':selected').text());
|
|
|
|
} else {
|
|
j.children('div.' + fieldName).text(editor.val());
|
|
}
|
|
});
|
|
|
|
// Mark the work_package as closed if so
|
|
self.markIfClosed();
|
|
|
|
// Get the save directives.
|
|
saveDir = self.saveDirectives();
|
|
|
|
self.beforeSave();
|
|
|
|
self.unmarkError();
|
|
self.markSaving();
|
|
RB.ajax({
|
|
type: "POST",
|
|
url: saveDir.url,
|
|
data: saveDir.data,
|
|
success : function (d, t, x) {
|
|
self.afterSave(d, t, x);
|
|
},
|
|
error : function (x, t, e) {
|
|
self.error(x, t, e);
|
|
}
|
|
});
|
|
self.endEdit();
|
|
},
|
|
|
|
unmarkError: function () {
|
|
this.$.removeClass('error icon icon-bug');
|
|
},
|
|
|
|
unmarkSaving: function () {
|
|
this.$.removeClass('saving');
|
|
}
|
|
});
|
|
}(jQuery));
|
|
|