Merge pull request #2219 from 0xF013/feature/17013-status-editable

pull/2260/head
Hagen Schink 10 years ago
commit fb8ffee2cb
  1. 9
      app/assets/stylesheets/content/_in_place_editing.sass
  2. 1
      app/assets/stylesheets/content/work_package_details/_overview_tab.sass
  3. 6
      frontend/app/routing.js
  4. 28
      frontend/app/services/work-package-service.js
  5. 9
      frontend/app/ui_components/index.js
  6. 75
      frontend/app/ui_components/inplace-editor-directive.js
  7. 127
      frontend/app/ui_components/inplace-editor-dispatcher.js
  8. 1
      frontend/app/work_packages/controllers/index.js
  9. 57
      frontend/app/work_packages/controllers/work-package-details-controller.js
  10. 2
      frontend/tests/integration/index.html
  11. 13
      frontend/tests/integration/mocks/work-packages.js
  12. 1659
      frontend/tests/integration/mocks/work-packages/819.json
  13. 219
      frontend/tests/integration/mocks/work-packages/819_form.json
  14. 2157
      frontend/tests/integration/mocks/work-packages/819_patch.json
  15. 4
      frontend/tests/integration/mocks/work-packages/819_textile.html
  16. 1666
      frontend/tests/integration/mocks/work-packages/820.json
  17. 219
      frontend/tests/integration/mocks/work-packages/820_form.json
  18. 1664
      frontend/tests/integration/mocks/work-packages/821.json
  19. 219
      frontend/tests/integration/mocks/work-packages/821_form.json
  20. 4
      frontend/tests/integration/pages/work-package-details-pane.js
  21. 42
      frontend/tests/integration/work-package-details-spec.js
  22. 19
      frontend/tests/unit/tests/work_packages/directives/inplace-editor-directive-test.js
  23. 13
      public/templates/components/inplace_editor.html
  24. 8
      public/templates/work_packages/tabs/overview.html

@ -122,4 +122,13 @@
padding-left: 0px padding-left: 0px
.jstb_strong, .jstb_em, .jstb_ins, .jstb_del, .jstb_ul, .jstb_ol .jstb_strong, .jstb_em, .jstb_ins, .jstb_del, .jstb_ul, .jstb_ol
display: inline display: inline
&.type-select
.ined-controls
right: auto
left: 0px
bottom: -23px
.ined-errors
bottom: -44px
left: 119px

@ -65,7 +65,6 @@
.work-package-attributes--value-container .work-package-attributes--value-container
@include work-package-attribute-margin @include work-package-attribute-margin
padding: 0 4px 0 0 padding: 0 4px 0 0
overflow: hidden
.work-package-attributes--value .work-package-attributes--value
&.-user &.-user

@ -65,7 +65,11 @@ angular.module('openproject')
controller: 'WorkPackageDetailsController', controller: 'WorkPackageDetailsController',
resolve: { resolve: {
workPackage: function(WorkPackageService, $stateParams) { workPackage: function(WorkPackageService, $stateParams) {
return WorkPackageService.getWorkPackage($stateParams.workPackageId); return WorkPackageService.getWorkPackage($stateParams.workPackageId).then(function(wp) {
return WorkPackageService.loadWorkPackageForm(wp).then(function() {
return wp;
});
});
} }
} }
}) })

@ -31,7 +31,7 @@ module.exports = function($http, PathHelper, WorkPackagesHelper, HALAPIResource,
var WorkPackageService = { var WorkPackageService = {
getWorkPackage: function(id) { getWorkPackage: function(id) {
var resource = HALAPIResource.setup("work_packages/" + id); var resource = HALAPIResource.setup('work_packages/' + id);
return resource.fetch().then(function (wp) { return resource.fetch().then(function (wp) {
workPackage = wp; workPackage = wp;
return workPackage; return workPackage;
@ -125,14 +125,28 @@ module.exports = function($http, PathHelper, WorkPackagesHelper, HALAPIResource,
}); });
}, },
loadWorkPackageForm: function(workPackage) {
var options = { ajax: {
method: 'POST',
headers: {
Accept: 'application/hal+json'
},
contentType: 'application/json; charset=utf-8'
}, force: true};
return workPackage.links.update.fetch(options).then(function(form) {
workPackage.form = form;
return form;
});
},
updateWorkPackage: function(workPackage, data) { updateWorkPackage: function(workPackage, data) {
var options = { ajax: { var options = { ajax: {
method: "PATCH", method: 'PATCH',
headers: { headers: {
Accept: "application/hal+json" Accept: 'application/hal+json'
}, },
data: JSON.stringify(data), data: JSON.stringify(data),
contentType: "application/json; charset=utf-8" contentType: 'application/json; charset=utf-8'
}, force: true}; }, force: true};
return workPackage.links.updateImmediately.fetch(options).then(function(workPackage) { return workPackage.links.updateImmediately.fetch(options).then(function(workPackage) {
return workPackage; return workPackage;
@ -141,12 +155,12 @@ module.exports = function($http, PathHelper, WorkPackagesHelper, HALAPIResource,
addWorkPackageRelation: function(workPackage, toId, relationType) { addWorkPackageRelation: function(workPackage, toId, relationType) {
var options = { ajax: { var options = { ajax: {
method: "POST", method: 'POST',
data: JSON.stringify({ data: JSON.stringify({
to_id: toId, to_id: toId,
relation_type: relationType relation_type: relationType
}), }),
contentType: "application/json; charset=utf-8" contentType: 'application/json; charset=utf-8'
} }; } };
return workPackage.links.addRelation.fetch(options).then(function(relation){ return workPackage.links.addRelation.fetch(options).then(function(relation){
return relation; return relation;
@ -154,7 +168,7 @@ module.exports = function($http, PathHelper, WorkPackagesHelper, HALAPIResource,
}, },
removeWorkPackageRelation: function(relation) { removeWorkPackageRelation: function(relation) {
var options = { ajax: { method: "DELETE" } }; var options = { ajax: { method: 'DELETE' } };
return relation.links.remove.fetch(options).then(function(response){ return relation.links.remove.fetch(options).then(function(response){
return response; return response;
}); });

@ -71,11 +71,14 @@ angular.module('openproject.uiComponents')
.directive('inaccessibleByTab', [require('./inaccessible-by-tab-directive')]) .directive('inaccessibleByTab', [require('./inaccessible-by-tab-directive')])
.directive('inplaceEditor', [ .directive('inplaceEditor', [
'$timeout', '$timeout',
'$sce', 'InplaceEditorDispatcher',
'TextileService',
'AutoCompleteHelper',
require('./inplace-editor-directive') require('./inplace-editor-directive')
]) ])
.service('InplaceEditorDispatcher', [
'$sce',
'AutoCompleteHelper',
'TextileService',
require('./inplace-editor-dispatcher')])
.directive('modal', [require('./modal-directive')]) .directive('modal', [require('./modal-directive')])
.directive('modalLoading', ['I18n', require('./modal-loading-directive')]) .directive('modalLoading', ['I18n', require('./modal-loading-directive')])
.directive('progressBar', ['I18n', require('./progress-bar-directive')]) .directive('progressBar', ['I18n', require('./progress-bar-directive')])

@ -26,7 +26,7 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) { module.exports = function($timeout, InplaceEditorDispatcher) {
return { return {
restrict: 'A', restrict: 'A',
transclude: false, transclude: false,
@ -60,18 +60,8 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
}); });
scope.$on('startEditing', function() { scope.$on('startEditing', function() {
$timeout(function() { $timeout(function() {
var textarea = element.find('.ined-input-wrapper input, .ined-input-wrapper textarea'); element.find('.ined-input-wrapper-inner .focus-input').focus().triggerHandler('keyup');
InplaceEditorDispatcher.dispatchHook(scope, 'link', element);
AutoCompleteHelper.enableTextareaAutoCompletion(textarea);
textarea.focus().triggerHandler('keyup');
// TODO: move this to a textarea-specific strategy
if (scope.type == 'wiki_textarea' || scope.type == 'textarea') {
var textarea = element.find('.ined-input-wrapper textarea'),
lines = textarea.val().split('\n');
textarea.attr('rows', lines.length + 1);
}
}); });
}); });
@ -85,14 +75,15 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
Controller.$inject = ['$scope', 'WorkPackageService', 'ApiHelper']; Controller.$inject = ['$scope', 'WorkPackageService', 'ApiHelper'];
function Controller($scope, WorkPackageService, ApiHelper) { function Controller($scope, WorkPackageService, ApiHelper) {
$scope.isEditing = false; $scope.isEditing = false;
$scope.isEditable = !!$scope.entity.links.updateImmediately;
$scope.isBusy = false; $scope.isBusy = false;
$scope.isPreview = false;
$scope.readValue = ''; $scope.readValue = '';
$scope.editTitle = I18n.t('js.inplace.button_edit'); $scope.editTitle = I18n.t('js.inplace.button_edit');
$scope.saveTitle = I18n.t('js.inplace.button_save'); $scope.saveTitle = I18n.t('js.inplace.button_save');
$scope.saveAndSendTitle = I18n.t('js.inplace.button_save_and_send'); $scope.saveAndSendTitle = I18n.t('js.inplace.button_save_and_send');
$scope.cancelTitle = I18n.t('js.inplace.button_cancel'); $scope.cancelTitle = I18n.t('js.inplace.button_cancel');
$scope.error = null; $scope.error = null;
$scope.options = [];
$scope.startEditing = startEditing; $scope.startEditing = startEditing;
$scope.discardEditing = discardEditing; $scope.discardEditing = discardEditing;
@ -100,31 +91,32 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
$scope.onSuccess = onSuccess; $scope.onSuccess = onSuccess;
$scope.onFail = onFail; $scope.onFail = onFail;
$scope.onFinally = onFinally; $scope.onFinally = onFinally;
$scope.togglePreview = togglePreview;
activate(); activate();
function activate() { function activate() {
// ng-model works weird with isolated scope InplaceEditorDispatcher.dispatchHook($scope, 'activate');
// also it's better to make an intermediate container
// to avoid live editing
setWriteValue(); setWriteValue();
setReadValue(); setReadValue();
} }
function setWriteValue() {
InplaceEditorDispatcher.dispatchHook($scope, 'setWriteValue');
}
function startEditing() { function startEditing() {
setWriteValue(); setWriteValue();
$scope.isEditing = true; $scope.isEditing = true;
$scope.error = null; $scope.error = null;
$scope.isBusy = false; $scope.isBusy = false;
$scope.isPreview = false; InplaceEditorDispatcher.dispatchHook($scope, 'startEditing');
$scope.$broadcast('startEditing'); $scope.$broadcast('startEditing');
} }
function submit(withEmail) { function submit(withEmail) {
var data = {}; // angular.copy here to make a new object instead of a reference
data[$scope.attribute] = $scope.dataObject.value; var data = angular.copy($scope.entity.form.embedded.payload.props);
data.lockVersion = $scope.entity.props.lockVersion; InplaceEditorDispatcher.dispatchHook($scope, 'submit', data);
$scope.isBusy = true; $scope.isBusy = true;
var result = WorkPackageService.updateWorkPackage($scope.entity, data); var result = WorkPackageService.updateWorkPackage($scope.entity, data);
result.then(function(workPackage) { result.then(function(workPackage) {
@ -138,9 +130,10 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
}); });
} }
function onSuccess(workPackage) { function onSuccess(entity) {
angular.extend($scope.entity, workPackage); // is it copying the other way around in documentation?
$scope.dataObject.value = $scope.entity.props[$scope.attribute]; // https://docs.angularjs.org/api/ng/function/angular.copy
angular.extend($scope.entity, entity);
$scope.error = null; $scope.error = null;
setReadValue(); setReadValue();
finishEditing(); finishEditing();
@ -149,7 +142,7 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
function onFail(e) { function onFail(e) {
$scope.error = ApiHelper.getErrorMessage(e); $scope.error = ApiHelper.getErrorMessage(e);
$scope.isPreview = false; InplaceEditorDispatcher.dispatchHook($scope, 'onFail');
} }
function onFinally() { function onFinally() {
@ -165,21 +158,8 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
$scope.$broadcast('finishEditing'); $scope.$broadcast('finishEditing');
} }
function setWriteValue() {
$scope.dataObject = {
value: $scope.entity.props[$scope.attribute]
};
}
function setReadValue() { function setReadValue() {
// this part should be refactored into a service that sets the read value InplaceEditorDispatcher.dispatchHook($scope, 'setReadValue');
// by attribute name, maybe some strategies or whatever
if ($scope.attribute == 'rawDescription') {
$scope.readValue = $sce.trustAsHtml($scope.entity.props.description);
} else {
$scope.readValue = $scope.entity.props[$scope.attribute];
}
if ((!$scope.readValue || $scope.readValue.length === 0) && $scope.placeholder) { if ((!$scope.readValue || $scope.readValue.length === 0) && $scope.placeholder) {
$scope.readValue = $scope.placeholder; $scope.readValue = $scope.placeholder;
$scope.placeholderSet = true; $scope.placeholderSet = true;
@ -188,20 +168,5 @@ module.exports = function($timeout, $sce, TextileService, AutoCompleteHelper) {
} }
} }
function togglePreview() {
$scope.isPreview = !$scope.isPreview;
$scope.error = null;
if (!$scope.isPreview) {
return;
}
$scope.isBusy = true;
TextileService.renderWithWorkPackageContext($scope.entity.props.id, $scope.dataObject.value).then(function(r) {
$scope.onFinally();
$scope.previewHtml = $sce.trustAsHtml(r.data);
}, function(e) {
$scope.onFinally();
$scope.onFail(e);
});
}
} }
}; };

@ -0,0 +1,127 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// 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 is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// 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.
//++
module.exports = function($sce, AutoCompleteHelper, TextileService) {
function enableAutoCompletion(element) {
var textarea = element.find('.ined-input-wrapper input, .ined-input-wrapper textarea');
AutoCompleteHelper.enableTextareaAutoCompletion(textarea);
}
function disablePreview($scope) {
$scope.isPreview = false;
}
function setOptions($scope) {
$scope.options = $scope
.entity.form.embedded.schema
.props[$scope.attribute]._links.allowedValues;
if (!$scope.options.length) {
$scope.isEditable = false;
}
}
var hooks = {
_fallback: {
submit: function($scope, data) {
data[$scope.attribute] = $scope.dataObject.value;
},
setWriteValue: function($scope) {
$scope.dataObject = {
value: $scope.entity.props[$scope.attribute]
};
},
setReadValue: function($scope) {
$scope.readValue = $scope.entity.props[$scope.attribute];
}
},
text: {
link: function(scope, element) {
enableAutoCompletion(element);
}
},
'wiki_textarea': {
link: function(scope, element) {
enableAutoCompletion(element);
var textarea = element.find('.ined-input-wrapper textarea'),
lines = textarea.val().split('\n');
textarea.attr('rows', lines.length + 1);
},
startEditing: disablePreview,
activate: function($scope) {
disablePreview($scope);
$scope.togglePreview = function() {
$scope.isPreview = !$scope.isPreview;
$scope.error = null;
if (!$scope.isPreview) {
return;
}
$scope.isBusy = true;
TextileService
.renderWithWorkPackageContext($scope.entity.props.id, $scope.dataObject.value)
.then(function(r) {
$scope.onFinally();
$scope.previewHtml = $sce.trustAsHtml(r.data);
}, function(e) {
$scope.onFinally();
$scope.onFail(e);
});
};
},
onFail: disablePreview,
setReadValue: function($scope) {
if ($scope.attribute == 'rawDescription') {
$scope.readValue = $sce.trustAsHtml($scope.entity.props.description);
} else {
$scope.readValue = $scope.entity.props[$scope.attribute];
}
}
},
select: {
activate: setOptions,
startEditing: setOptions,
submit: function($scope, data) {
data._links = { };
data._links[$scope.attribute] = { href: $scope.dataObject.value };
},
setWriteValue: function($scope) {
$scope.dataObject = {
value: $scope.entity.form.embedded.payload.links[$scope.attribute].href
};
}
}
};
this.dispatchHook = function($scope, action, data) {
var actionFunction = hooks[$scope.type][action] || hooks._fallback[action] || angular.noop;
return actionFunction($scope, data);
};
};

@ -92,6 +92,7 @@ angular.module('openproject.workPackages.controllers')
'PathHelper', 'PathHelper',
'UsersHelper', 'UsersHelper',
'ConfigurationService', 'ConfigurationService',
'WorkPackageService',
'CommonRelationsHandler', 'CommonRelationsHandler',
'ChildrenRelationsHandler', 'ChildrenRelationsHandler',
'ParentRelationsHandler', 'ParentRelationsHandler',

@ -26,12 +26,18 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
//++ //++
module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION_TYPES, RELATION_IDENTIFIERS, $q, WorkPackagesHelper, PathHelper, UsersHelper, ConfigurationService, CommonRelationsHandler, ChildrenRelationsHandler, ParentRelationsHandler) { module.exports = function(
$scope, $state, latestTab, workPackage, I18n,
RELATION_TYPES, RELATION_IDENTIFIERS, $q,
WorkPackagesHelper, PathHelper, UsersHelper,
ConfigurationService, WorkPackageService,
CommonRelationsHandler, ChildrenRelationsHandler, ParentRelationsHandler
) {
$scope.$on('$stateChangeSuccess', function(event, toState){ $scope.$on('$stateChangeSuccess', function(event, toState){
latestTab.registerState(toState.name); latestTab.registerState(toState.name);
}); });
$scope.$on('workPackageRefreshRequired', function(event, toState){ $scope.$on('workPackageRefreshRequired', function() {
refreshWorkPackage(); refreshWorkPackage();
}); });
@ -45,7 +51,10 @@ module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION
function refreshWorkPackage() { function refreshWorkPackage() {
workPackage.links.self workPackage.links.self
.fetch({force: true}) .fetch({force: true})
.then(setWorkPackageScopeProperties); .then(function() {
WorkPackageService.loadWorkPackageForm(workPackage);
setWorkPackageScopeProperties(workPackage);
});
} }
$scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers $scope.refreshWorkPackage = refreshWorkPackage; // expose to child controllers
@ -70,7 +79,13 @@ module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION
$scope.workPackage = workPackage; $scope.workPackage = workPackage;
$scope.isWatched = !!workPackage.links.unwatchChanges; $scope.isWatched = !!workPackage.links.unwatchChanges;
$scope.toggleWatchLink = workPackage.links.watchChanges === undefined ? workPackage.links.unwatchChanges : workPackage.links.watchChanges;
if (workPackage.links.watchChanges === undefined) {
$scope.toggleWatchLink = workPackage.links.unwatchChanges;
} else {
$scope.toggleWatchLink = workPackage.links.watchChanges;
}
$scope.watchers = workPackage.embedded.watchers; $scope.watchers = workPackage.embedded.watchers;
// autocomplete path // autocomplete path
@ -94,7 +109,7 @@ module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION
// relations // relations
$q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parents) { $q.all(WorkPackagesHelper.getParent(workPackage)).then(function(parents) {
var relationsHandler = new ParentRelationsHandler(workPackage, parents, "parent"); var relationsHandler = new ParentRelationsHandler(workPackage, parents, 'parent');
$scope.wpParent = relationsHandler; $scope.wpParent = relationsHandler;
}); });
@ -103,16 +118,21 @@ module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION
$scope.wpChildren = relationsHandler; $scope.wpChildren = relationsHandler;
}); });
for (var key in RELATION_TYPES) { function relationTypeIterator(key) {
if (RELATION_TYPES.hasOwnProperty(key)) { $q.all(WorkPackagesHelper.getRelationsOfType(
(function(key) { workPackage,
$q.all(WorkPackagesHelper.getRelationsOfType(workPackage, RELATION_TYPES[key])).then(function(relations) { RELATION_TYPES[key])
).then(function(relations) {
var relationsHandler = new CommonRelationsHandler(workPackage, var relationsHandler = new CommonRelationsHandler(workPackage,
relations, relations,
RELATION_IDENTIFIERS[key]); RELATION_IDENTIFIERS[key]);
$scope[key] = relationsHandler; $scope[key] = relationsHandler;
}); });
})(key); }
for (var key in RELATION_TYPES) {
if (RELATION_TYPES.hasOwnProperty(key)) {
relationTypeIterator(key);
} }
} }
@ -132,7 +152,8 @@ module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION
function displayedActivities(workPackage) { function displayedActivities(workPackage) {
var activities = workPackage.embedded.activities; var activities = workPackage.embedded.activities;
activities.splice(0, 1); // remove first activity (assumes activities are sorted chronologically) // remove first activity (assumes activities are sorted chronologically)
activities.splice(0, 1);
if ($scope.activitiesSortedInDescendingOrder) { if ($scope.activitiesSortedInDescendingOrder) {
activities.reverse(); activities.reverse();
} }
@ -147,12 +168,18 @@ module.exports = function($scope, $state, latestTab, workPackage, I18n, RELATION
}; };
function getFocusAnchorLabel(tab, workPackage) { function getFocusAnchorLabel(tab, workPackage) {
var tabLabel = I18n.t('js.work_packages.tabs.' + tab); var tabLabel = I18n.t('js.work_packages.tabs.' + tab),
var params = { tab: tabLabel, type: workPackage.props.type, subject: workPackage.props.subject }; params = {
tab: tabLabel,
type: workPackage.props.type,
subject: workPackage.props.subject
};
return I18n.t('js.label_work_package_details_you_are_here', params); return I18n.t('js.label_work_package_details_you_are_here', params);
} }
$scope.focusAnchorLabel = getFocusAnchorLabel($state.current.url.replace(/\//, ''), $scope.workPackage); $scope.focusAnchorLabel = getFocusAnchorLabel(
$state.current.url.replace(/\//, ''),
$scope.workPackage
);
}; };

@ -23,6 +23,8 @@
<script src="/bower_components/jquery/dist/jquery.js"></script> <script src="/bower_components/jquery/dist/jquery.js"></script>
<script src="/bower_components/momentjs/moment.js"></script> <script src="/bower_components/momentjs/moment.js"></script>
<script src="/bower_components/moment-timezone/moment-timezone.js"></script> <script src="/bower_components/moment-timezone/moment-timezone.js"></script>
<script src="/bower_components/jquery.atwho/dist/js/jquery.atwho.js"></script>
<script src="/bower_components/Caret.js/src/jquery.caret.js"></script>
<script src="/bower_components/select2/select2.js"></script> <script src="/bower_components/select2/select2.js"></script>
<!-- <script src="vendor/assets/javascripts/moment-timezone/moment-timezone-data.js"></script> --> <!-- <script src="vendor/assets/javascripts/moment-timezone/moment-timezone-data.js"></script> -->

@ -38,6 +38,14 @@ module.exports = function(app) {
res.send(text); res.send(text);
}); });
}); });
workPackagesRouter.post('/:id/form', function(req, res) {
fs.readFile(
__dirname + '/work-packages/' + req.params.id + '_form.json',
'utf8',
function(err, text) {
res.send(text);
});
});
workPackagesRouter.patch('/821', function(req, res) { workPackagesRouter.patch('/821', function(req, res) {
fs.readFile(__dirname + '/work-packages/821_patch.json', 'utf8', function(err, text) { fs.readFile(__dirname + '/work-packages/821_patch.json', 'utf8', function(err, text) {
res.status(409); res.status(409);
@ -56,4 +64,9 @@ module.exports = function(app) {
}); });
app.use('/api/v3/render/textile', textileRouter); app.use('/api/v3/render/textile', textileRouter);
app.get('/work_packages/auto_complete.json*', function(req, res) {
res.send('[]');
});
}; };

File diff suppressed because it is too large Load Diff

@ -0,0 +1,219 @@
{
"_type": "Form",
"_links": {
"self": {
"href": "/api/v3/work_packages/819/form"
},
"validate": {
"href": "/api/v3/work_packages/819/form",
"method": "post"
},
"previewMarkup": {
"href": "/api/v3/render/textile?/api/v3/work_packages/819",
"method": "post"
},
"commit": {
"href": "/api/v3/work_packages/819",
"method": "patch"
}
},
"_embedded": {
"payload": {
"_type": "WorkPackage",
"_links": {
"status": {
"href": "/api/v3/statuses/2"
}
},
"lockVersion": 119,
"subject": "66666",
"rawDescription": "#820\n2",
"parentId": 54,
"projectId": 1,
"startDate": "2014-10-23T00:00:00+00:00",
"dueDate": "2014-12-27T00:00:00+00:00",
"versionId": null,
"createdAt": "2014-11-05T15:56:53Z",
"updatedAt": "2014-11-21T08:48:57Z"
},
"schema": {
"_type": {
"type": "MetaType",
"required": true,
"writable": false
},
"lockVersion": {
"type": "Integer",
"required": true,
"writable": false
},
"subject": {
"type": "String"
},
"status": {
"_links": {
"allowedValues": [
{
"href": "/api/v3/statuses/1",
"title": "new"
},
{
"href": "/api/v3/statuses/2",
"title": "specified"
},
{
"href": "/api/v3/statuses/3",
"title": "confirmed"
},
{
"href": "/api/v3/statuses/6",
"title": "in progress"
},
{
"href": "/api/v3/statuses/7",
"title": "tested"
},
{
"href": "/api/v3/statuses/8",
"title": "on hold"
},
{
"href": "/api/v3/statuses/9",
"title": "rejected"
},
{
"href": "/api/v3/statuses/10",
"title": "closed"
}
]
},
"type": "Status",
"_embedded": {
"allowedValues": [
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/1",
"title": "new"
}
},
"id": 1,
"name": "new",
"isClosed": false,
"isDefault": true,
"defaultDoneRatio": null,
"position": 1
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/2",
"title": "specified"
}
},
"id": 2,
"name": "specified",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 2
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/3",
"title": "confirmed"
}
},
"id": 3,
"name": "confirmed",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 3
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/6",
"title": "in progress"
}
},
"id": 6,
"name": "in progress",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 6
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/7",
"title": "tested"
}
},
"id": 7,
"name": "tested",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 7
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/8",
"title": "on hold"
}
},
"id": 8,
"name": "on hold",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 8
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/9",
"title": "rejected"
}
},
"id": 9,
"name": "rejected",
"isClosed": true,
"isDefault": false,
"defaultDoneRatio": null,
"position": 9
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/10",
"title": "closed"
}
},
"id": 10,
"name": "closed",
"isClosed": true,
"isDefault": false,
"defaultDoneRatio": null,
"position": 10
}
]
}
}
},
"validationErrors": {}
}
}

@ -1,2 +1,2 @@
<p><a href="/work_packages/54" class="issue work_package status-4 priority-2 parent" <p><a href="/work_packages/820" class="issue work_package status-4 priority-2 parent"
title="harum temporibus sit sit autem atque optio vitaelasd22ss2 (to be scheduled)">#54</a></p> title="harum temporibus sit sit autem atque optio vitaelasd22ss2 (to be scheduled)">#820</a></p>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,219 @@
{
"_type": "Form",
"_links": {
"self": {
"href": "/api/v3/work_packages/820/form"
},
"validate": {
"href": "/api/v3/work_packages/820/form",
"method": "post"
},
"previewMarkup": {
"href": "/api/v3/render/textile?/api/v3/work_packages/820",
"method": "post"
},
"commit": {
"href": "/api/v3/work_packages/820",
"method": "patch"
}
},
"_embedded": {
"payload": {
"_type": "WorkPackage",
"_links": {
"status": {
"href": "/api/v3/statuses/9"
}
},
"lockVersion": 119,
"subject": "66666",
"rawDescription": "#54\n2",
"parentId": 54,
"projectId": 1,
"startDate": "2014-10-23T00:00:00+00:00",
"dueDate": "2014-12-27T00:00:00+00:00",
"versionId": null,
"createdAt": "2014-11-05T15:56:53Z",
"updatedAt": "2014-11-21T08:48:57Z"
},
"schema": {
"_type": {
"type": "MetaType",
"required": true,
"writable": false
},
"lockVersion": {
"type": "Integer",
"required": true,
"writable": false
},
"subject": {
"type": "String"
},
"status": {
"_links": {
"allowedValues": [
{
"href": "/api/v3/statuses/1",
"title": "new"
},
{
"href": "/api/v3/statuses/2",
"title": "specified"
},
{
"href": "/api/v3/statuses/3",
"title": "confirmed"
},
{
"href": "/api/v3/statuses/6",
"title": "in progress"
},
{
"href": "/api/v3/statuses/7",
"title": "tested"
},
{
"href": "/api/v3/statuses/8",
"title": "on hold"
},
{
"href": "/api/v3/statuses/9",
"title": "rejected"
},
{
"href": "/api/v3/statuses/10",
"title": "closed"
}
]
},
"type": "Status",
"_embedded": {
"allowedValues": [
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/1",
"title": "new"
}
},
"id": 1,
"name": "new",
"isClosed": false,
"isDefault": true,
"defaultDoneRatio": null,
"position": 1
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/2",
"title": "specified"
}
},
"id": 2,
"name": "specified",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 2
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/3",
"title": "confirmed"
}
},
"id": 3,
"name": "confirmed",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 3
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/6",
"title": "in progress"
}
},
"id": 6,
"name": "in progress",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 6
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/7",
"title": "tested"
}
},
"id": 7,
"name": "tested",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 7
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/8",
"title": "on hold"
}
},
"id": 8,
"name": "on hold",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 8
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/9",
"title": "rejected"
}
},
"id": 9,
"name": "rejected",
"isClosed": true,
"isDefault": false,
"defaultDoneRatio": null,
"position": 9
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/10",
"title": "closed"
}
},
"id": 10,
"name": "closed",
"isClosed": true,
"isDefault": false,
"defaultDoneRatio": null,
"position": 10
}
]
}
}
},
"validationErrors": {}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,219 @@
{
"_type": "Form",
"_links": {
"self": {
"href": "/api/v3/work_packages/821/form"
},
"validate": {
"href": "/api/v3/work_packages/821/form",
"method": "post"
},
"previewMarkup": {
"href": "/api/v3/render/textile?/api/v3/work_packages/821",
"method": "post"
},
"commit": {
"href": "/api/v3/work_packages/821",
"method": "patch"
}
},
"_embedded": {
"payload": {
"_type": "WorkPackage",
"_links": {
"status": {
"href": "/api/v3/statuses/9"
}
},
"lockVersion": 119,
"subject": "66666",
"rawDescription": "#54\n2",
"parentId": 54,
"projectId": 1,
"startDate": "2014-10-23T00:00:00+00:00",
"dueDate": "2014-12-27T00:00:00+00:00",
"versionId": null,
"createdAt": "2014-11-05T15:56:53Z",
"updatedAt": "2014-11-21T08:48:57Z"
},
"schema": {
"_type": {
"type": "MetaType",
"required": true,
"writable": false
},
"lockVersion": {
"type": "Integer",
"required": true,
"writable": false
},
"subject": {
"type": "String"
},
"status": {
"_links": {
"allowedValues": [
{
"href": "/api/v3/statuses/1",
"title": "new"
},
{
"href": "/api/v3/statuses/2",
"title": "specified"
},
{
"href": "/api/v3/statuses/3",
"title": "confirmed"
},
{
"href": "/api/v3/statuses/6",
"title": "in progress"
},
{
"href": "/api/v3/statuses/7",
"title": "tested"
},
{
"href": "/api/v3/statuses/8",
"title": "on hold"
},
{
"href": "/api/v3/statuses/9",
"title": "rejected"
},
{
"href": "/api/v3/statuses/10",
"title": "closed"
}
]
},
"type": "Status",
"_embedded": {
"allowedValues": [
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/1",
"title": "new"
}
},
"id": 1,
"name": "new",
"isClosed": false,
"isDefault": true,
"defaultDoneRatio": null,
"position": 1
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/2",
"title": "specified"
}
},
"id": 2,
"name": "specified",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 2
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/3",
"title": "confirmed"
}
},
"id": 3,
"name": "confirmed",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 3
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/6",
"title": "in progress"
}
},
"id": 6,
"name": "in progress",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 6
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/7",
"title": "tested"
}
},
"id": 7,
"name": "tested",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 7
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/8",
"title": "on hold"
}
},
"id": 8,
"name": "on hold",
"isClosed": false,
"isDefault": false,
"defaultDoneRatio": null,
"position": 8
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/9",
"title": "rejected"
}
},
"id": 9,
"name": "rejected",
"isClosed": true,
"isDefault": false,
"defaultDoneRatio": null,
"position": 9
},
{
"_type": "Status",
"_links": {
"self": {
"href": "/api/v3/statuses/10",
"title": "closed"
}
},
"id": 10,
"name": "closed",
"isClosed": true,
"isDefault": false,
"defaultDoneRatio": null,
"position": 10
}
]
}
}
},
"validationErrors": {}
}
}

@ -27,13 +27,9 @@
//++ //++
function WorkPackageDetailsPane(id, tab) { function WorkPackageDetailsPane(id, tab) {
this.pane = $(".work-packages--details");
this.get = function() { this.get = function() {
browser.get('/work_packages/' + id + '/' + tab); browser.get('/work_packages/' + id + '/' + tab);
}; };
} }
module.exports = WorkPackageDetailsPane; module.exports = WorkPackageDetailsPane;

@ -41,12 +41,10 @@ describe('OpenProject', function () {
it('should show work packages details pane', function() { it('should show work packages details pane', function() {
page.get(); page.get();
expect(page.pane.isPresent()).to.eventually.be.true; expect($('.work-packages--details').isPresent()).to.eventually.be.true;
}); });
describe('editable', function() { describe('editable', function() {
var page;
context('subject', function() { context('subject', function() {
context('work package with updateImmediately link', function() { context('work package with updateImmediately link', function() {
beforeEach(function() { beforeEach(function() {
@ -89,21 +87,21 @@ describe('OpenProject', function () {
it('should render the link to another work package', function() { it('should render the link to another work package', function() {
expect($('.detail-panel-description .inplace-editor .ined-read-value a.work_package').isDisplayed()).to.eventually.be.true; expect($('.detail-panel-description .inplace-editor .ined-read-value a.work_package').isDisplayed()).to.eventually.be.true;
}); });
context('click', function() {
it('should render the textarea', function() { it('should render the textarea', function() {
$('.detail-panel-description .inplace-editor .ined-read-value').then(function(e) { $('.detail-panel-description .inplace-editor .ined-read-value').then(function(e) {
e.click(); e.click();
expect($('.detail-panel-description .ined-edit textarea').isDisplayed()).to.eventually.be.true; expect($('.detail-panel-description .ined-edit textarea').isDisplayed())
.to.eventually.be.true;
}); });
}); });
it('should not render the textarea if click is on the link', function() { it('should not render the textarea if click is on the link', function() {
$('.detail-panel-description .inplace-editor .ined-read-value a.work_package').then(function (e) { $('.detail-panel-description .inplace-editor .ined-read-value a.work_package')
.then(function(e) {
e.click(); e.click();
expect($('.detail-panel-description .ined-edit textarea').isPresent()).to.eventually.be.false; expect($('.detail-panel-description .ined-edit textarea').isPresent()).to.eventually.be.false;
}); });
}); });
}); });
});
describe('preview', function() { describe('preview', function() {
beforeEach(function() { beforeEach(function() {
$('.detail-panel-description .inplace-editor .ined-read-value').then(function(e) { $('.detail-panel-description .inplace-editor .ined-read-value').then(function(e) {
@ -121,5 +119,35 @@ describe('OpenProject', function () {
}); });
}); });
}); });
context('status', function() {
beforeEach(function() {
page = new WorkPackageDetailsPane(819, 'overview');
page.get();
});
describe('read state', function() {
it('should render a span with value', function() {
expect($('.status-inline-editor .inplace-editor span.read-value-wrapper').getText())
.to.eventually.equal('specified');
});
});
describe('edit state', function() {
beforeEach(function() {
$('.status-inline-editor .inplace-editor .ined-read-value').then(function(e) {
e.click();
});
});
context('dropdown', function() {
it('should be rendered', function() {
expect($('.status-inline-editor select.focus-input').isDisplayed())
.to.eventually.be.true;
});
it('should have the correct value', function() {
expect(
$('.status-inline-editor select.focus-input option:checked').getAttribute('value')
).to.eventually.equal('1');
});
});
});
});
}); });
}); });

@ -31,7 +31,15 @@
describe('inplaceEditor Directive', function() { describe('inplaceEditor Directive', function() {
var compile, element, rootScope, scope, elementScope, $timeout, html, var compile, element, rootScope, scope, elementScope, $timeout, html,
submitStub, updateWorkPackageStub, onSuccessSpy, onFailSpy, onFinallySpy, submitStub, updateWorkPackageStub, onSuccessSpy, onFailSpy, onFinallySpy,
WorkPackageService; WorkPackageService, form;
form = {
embedded: {
payload: {
props: {}
}
}
};
function triggerKey(element, keyCode) { function triggerKey(element, keyCode) {
var e = jQuery.Event("keypress"); var e = jQuery.Event("keypress");
@ -82,7 +90,8 @@ describe('inplaceEditor Directive', function() {
updateImmediately: { updateImmediately: {
fetch: function() { } fetch: function() { }
} }
} },
form: form
}; };
compile(); compile();
element.appendTo(document.body); element.appendTo(document.body);
@ -115,7 +124,8 @@ describe('inplaceEditor Directive', function() {
updateImmediately: { updateImmediately: {
fetch: function() { } fetch: function() { }
} }
} },
form: form
}; };
html = html =
'<h2 ' + '<h2 ' +
@ -156,7 +166,8 @@ describe('inplaceEditor Directive', function() {
subject: 'Some subject', subject: 'Some subject',
lockVersion: '1' lockVersion: '1'
}, },
links: { } links: { },
form: form
}; };
compile(); compile();
}); });

@ -1,5 +1,5 @@
<span ng-if="!entity.links.updateImmediately">{{ entity.props[attribute] }}</span> <span ng-if="!isEditable">{{ entity.props[attribute] }}</span>
<div ng-if="entity.links.updateImmediately" class="inplace-editor type-{{type}}" ng-class="{busy: isBusy, preview: isPreview}"> <div ng-if="isEditable" class="inplace-editor type-{{type}}" ng-class="{busy: isBusy, preview: isPreview}">
<div class="ined-read-value" ng-class="{ default: placeholderSet }" ng-hide="isEditing" ng-switch="type"> <div class="ined-read-value" ng-class="{ default: placeholderSet }" ng-hide="isEditing" ng-switch="type">
<span class="read-value-wrapper" ng-switch-when="wiki_textarea" ng-bind-html="readValue"></span> <span class="read-value-wrapper" ng-switch-when="wiki_textarea" ng-bind-html="readValue"></span>
<span class="read-value-wrapper" ng-switch-default ng-bind="readValue"></span> <span class="read-value-wrapper" ng-switch-default ng-bind="readValue"></span>
@ -15,16 +15,19 @@
<div class="ined-input-wrapper" ng-switch="type"> <div class="ined-input-wrapper" ng-switch="type">
<div class="ined-input-wrapper-inner"> <div class="ined-input-wrapper-inner">
<input ng-switch-when="text" <input ng-switch-when="text"
class="focus-input"
name="value" name="value"
type="text" type="text"
ng-disabled="isBusy" ng-disabled="isBusy"
ng-model="dataObject.value" /> ng-model="dataObject.value" />
<textarea ng-switch-when="textarea" <textarea ng-switch-when="textarea"
class="focus-input"
name="value" name="value"
ng-disabled="isBusy" ng-disabled="isBusy"
ng-model="dataObject.value"> ng-model="dataObject.value">
</textarea> </textarea>
<textarea wiki-toolbar ng-switch-when="wiki_textarea" <textarea wiki-toolbar ng-switch-when="wiki_textarea"
class="focus-input"
ng-show="!isPreview || isBusy" ng-show="!isPreview || isBusy"
preview-toggle="togglePreview()" preview-toggle="togglePreview()"
name="value" name="value"
@ -32,6 +35,12 @@
ng-model="dataObject.value" ng-model="dataObject.value"
data-wp_autocomplete_url="{{ autocompletePath }}"> data-wp_autocomplete_url="{{ autocompletePath }}">
</textarea> </textarea>
<select ng-switch-when="select"
class="focus-input"
name="value"
ng-disabled="isBusy"
ng-model="dataObject.value"
ng-options="item.href as item.title for item in options"></select>
</div> </div>
<div class="preview-wrapper" ng-if="isPreview && !isBusy"> <div class="preview-wrapper" ng-if="isPreview && !isBusy">
<span ng-bind-html="previewHtml"></span> <span ng-bind-html="previewHtml"></span>

@ -31,7 +31,13 @@
<div ng-switch-when="null" class="work-package-attributes--value"> <div ng-switch-when="null" class="work-package-attributes--value">
<empty-element></empty-element> <empty-element></empty-element>
</div> </div>
<div ng-switch-default ng-switch="propertyData.format"> <div class="status-inline-editor" ng-if="propertyData.property == 'status'"
inplace-editor
ined-type="select"
ined-entity="workPackage"
ined-attribute="status"
></div>
<div ng-if="propertyData.property != 'status'" ng-switch-default ng-switch="propertyData.format">
<div ng-class="['work-package-attributes--value', '-' + propertyData.format]"> <div ng-class="['work-package-attributes--value', '-' + propertyData.format]">
<user-field ng-switch-when="user" user="propertyData.value"></user-field> <user-field ng-switch-when="user" user="propertyData.value"></user-field>
<span ng-switch-when="dynamic" work-package-dynamic-attribute <span ng-switch-when="dynamic" work-package-dynamic-attribute

Loading…
Cancel
Save