Merge branch 'release/4.1' into dev

pull/2859/merge
Alex Coles 10 years ago
commit c146f65749
  1. 1
      app/assets/stylesheets/content/_in_place_editing.sass
  2. 1
      config/locales/js-de.yml
  3. 1
      config/locales/js-en.yml
  4. 2
      doc/CONFIGURATION.md
  5. 49
      frontend/app/services/work-package-service.js
  6. 3
      frontend/app/templates/work_packages.list.details.html
  7. 16
      frontend/app/templates/work_packages/modals/columns.html
  8. 10
      frontend/app/templates/work_packages/modals/export.html
  9. 14
      frontend/app/templates/work_packages/modals/group_by.html
  10. 10
      frontend/app/templates/work_packages/modals/save.html
  11. 10
      frontend/app/templates/work_packages/modals/settings.html
  12. 12
      frontend/app/templates/work_packages/modals/share.html
  13. 14
      frontend/app/templates/work_packages/modals/sorting.html
  14. 6
      frontend/app/templates/work_packages/tabs/overview.html
  15. 36
      frontend/app/work_packages/controllers/details-tab-overview-controller.js
  16. 3
      frontend/app/work_packages/directives/inplace_editor/custom/editable/inplace-editor-dropdown-directive.js
  17. 9
      frontend/app/work_packages/directives/inplace_editor/inplace-editor-display-pane-directive.js
  18. 75
      frontend/app/work_packages/directives/inplace_editor/inplace-editor-edit-pane-directive.js
  19. 2
      frontend/app/work_packages/services/editable-fields-state.js
  20. 5
      frontend/app/work_packages/services/index.js
  21. 72
      frontend/app/work_packages/services/work-package-field-service.js
  22. 2
      lib/redcloth3.rb

@ -84,6 +84,7 @@ $inplace-edit--color--very-dark: #cacaca
background: white
border: 1px solid $inplace-edit--color--very-dark
min-height: 42px
white-space: pre-line
.inplace-edit--controls
display: inline-block

@ -31,6 +31,7 @@ de:
ajax:
hide: "Verbergen"
loading: "Lädt ..."
close_popup_title: "Dialog schließen"
button_add_watcher: "Beobachter hinzufügen"
button_cancel: "Abbrechen"
button_check_all: "Alles auswählen"

@ -31,6 +31,7 @@ en:
ajax:
hide: "Hide"
loading: "Loading ..."
close_popup_title: "Close popup"
button_add_watcher: "Add watcher"
button_cancel: "Cancel"
button_check_all: "Check all"

@ -78,7 +78,7 @@ storage config above like this:
* `autologin_cookie_secure` (default: false)
* `database_cipher_key` (default: nil)
* `scm_git_command` (default: 'git')
* `scm_subversion_command` (default: 'git')
* `scm_subversion_command` (default: 'svn')
* `session_store`: `active_record_store`, `cache_store`, or `cookie_store` (default: cache_store)
* [`omniauth_direct_login_provider`](#omniauth-direct-login-provider) (default: nil)
* [`disable_password_login`](#disable-password-login) (default: false)

@ -41,6 +41,35 @@ module.exports = function($http,
) {
var workPackage;
function getPendingChanges(workPackage) {
var data = {
// _links: {}
};
if (workPackage.form) {
_.forEach(workPackage.form.pendingChanges, function(value, field) {
if (WorkPackageFieldService.isSpecified(workPackage, field)) {
if(field == 'date') {
data['startDate'] = value['startDate'];
data['dueDate'] = value['dueDate'];
return;
}
if (WorkPackageFieldService.isSavedAsLink(workPackage, field)) {
data._links = data._links || {};
data._links[field] = value ? value.links.self.props : { href: null };
} else {
data[field] = value;
}
}
});
}
if (_.isEmpty(data)) {
return null;
} else {
return JSON.stringify(data);
}
}
var WorkPackageService = {
getWorkPackage: function(id) {
var resource = HALAPIResource.setup('work_packages/' + id);
@ -53,6 +82,7 @@ module.exports = function($http,
wp.schema = result[1];
workPackage = wp;
EditableFieldsState.workPackage = wp;
EditableFieldsState.errors = null;
return wp;
});
});
@ -146,13 +176,13 @@ module.exports = function($http,
},
loadWorkPackageForm: function(workPackage) {
if (this.authorizedFor(workPackage, 'update')) {
var options = { ajax: {
method: 'POST',
headers: {
Accept: 'application/hal+json'
},
data:getPendingChanges(workPackage),
contentType: 'application/json; charset=utf-8'
}, force: true};
@ -174,28 +204,13 @@ module.exports = function($http,
},
updateWorkPackage: function(workPackage, notify) {
var data = {
_links: {}
};
_.forEach(workPackage.form.pendingChanges, function(value, field) {
if(field == 'date') {
data['startDate'] = value['startDate'];
data['dueDate'] = value['dueDate'];
return;
}
if (WorkPackageFieldService.isSavedAsLink(workPackage, field)) {
data._links[field] = value ? value.links.self.props : { href: null };
} else {
data[field] = value;
}
});
var options = { ajax: {
method: 'PATCH',
url: URI(workPackage.links.updateImmediately.href).addSearch('notify', notify).toString(),
headers: {
Accept: 'application/hal+json'
},
data: JSON.stringify(data),
data: getPendingChanges(workPackage),
contentType: 'application/json; charset=utf-8'
}, force: true};
return workPackage.links.updateImmediately.fetch(options);

@ -31,7 +31,6 @@
</span>
<div class="work-packages--details--title">
<div class="work-packages--details--type">{{ type.props.name }}:</div>
<work-package-field field="'subject'"></work-package-field>
</div>
@ -46,7 +45,7 @@
</span>
</span>
</accessible-by-keyboard>
<a href="{{ showStaticPagePath }}">#{{ workPackage.props.id }}</a>
<a href="{{ showStaticPagePath }}">{{ type.props.name }} #{{ workPackage.props.id }}</a>
<span ng-bind="I18n.t('js.label_added_by')"/>
<a ng-if="authorActive" ng-href="{{ authorPath }}" ng-bind="author.props.name"/>
<span ng-if="!authorActive">{{ author.props.name }} </span>

@ -1,12 +1,12 @@
<div class="ng-modal-window">
<div class="ng-modal-inner">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.label_columns') }}</h3>
<h3>{{ ::I18n.t('js.label_columns') }}</h3>
<div cg-busy="vm.promise" class="columns-modal-content">
<label for="selected_columns" class="hidden-for-sighted">{{ I18n.t('js.description_selected_columns') }}</label>
<ui-select multiple sortable="true" ng-model="vm.selectedColumns" theme="select2" id="selected_columns" focus aria-describedby="column_multiselect_description">
<label for="selected_columns" class="hidden-for-sighted">{{ ::I18n.t('js.description_selected_columns') }}</label>
<ui-select multiple sortable="true" ng-model="vm.selectedColumns" theme="select2" id="selected_columns" focus aria-labelledby="column_multiselect_description">
<ui-select-match>
{{$item.title}}
</ui-select-match>
@ -14,20 +14,20 @@
<div ng-bind-html="column.title | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<span class="tooltip--right -multiline" tabindex="0" data-tooltip="{{ I18n.t('js.work_packages.label_column_multiselect') }}">
<span class="tooltip--right -multiline" tabindex="0" title data-tooltip="{{ ::I18n.t('js.work_packages.label_column_multiselect') }}" aria-labelledby="column_multiselect_description">
<i class="icon icon-help1"></i>
</span>
<div class="hidden-for-sighted" id="column_multiselect_description">
<p>{{ I18n.t('js.work_packages.label_column_multiselect') }}</p>
{{ ::I18n.t('js.work_packages.label_column_multiselect') }}
</div>
</div>
<div>
<button class="button -highlight" ng-click="updateSelectedColumns()">
{{ I18n.t('js.modals.button_apply') }}
{{ ::I18n.t('js.modals.button_apply') }}
</button>
<button class="button" ng-click="modal.closeMe()">
{{ I18n.t('js.modals.button_cancel') }}
{{ ::I18n.t('js.modals.button_cancel') }}
</button>
</div>

@ -1,14 +1,14 @@
<div class="ng-modal-window">
<div class="ng-modal-inner">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.label_export') }}</h3>
<h3>{{ ::I18n.t('js.label_export') }}</h3>
<ul class="export-options">
<li ng-repeat="option in modal.exportOptions">
<a ng-href="{{ option.url }}" focus="$first">
<i class="icon-page-{{ option.identifier }} icon-big"></i>
<span class="export-label">{{ option.label }}</span>
<a ng-href="{{ ::option.url }}" focus="$first">
<i class="icon-page-{{ ::option.identifier }} icon-big"></i>
<span class="export-label">{{ ::option.label }}</span>
</a>
</li>
</ul>

@ -1,29 +1,29 @@
<div class="ng-modal-window">
<div class="ng-modal-inner">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.label_group_by') }}</h3>
<h3>{{ ::I18n.t('js.label_group_by') }}</h3>
<div>
<ui-select ng-model="vm.selectedColumnName" theme="select2" focus id="selected_columns_new" aria-describedby="column_select_description">
<ui-select ng-model="vm.selectedColumnName" theme="select2" focus id="selected_columns_new" aria-labelledby="column_select_description">
<ui-select-match>{{$select.selected.title}}</ui-select-match>
<ui-select-choices repeat="column.name as column in vm.groupableColumns | filter: { title: $select.search }">
<div ng-bind-html="column.title | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<span class="tooltip--right -multiline" tabindex="0" data-tooltip="{{ I18n.t('js.work_packages.label_column_select') }}">
<span class="tooltip--right -multiline" tabindex="0" data-tooltip="{{ ::I18n.t('js.work_packages.label_column_select') }}" aria-labelledby="column_select_description">
<i class="icon icon-help1"></i>
</span>
<div id="column_select_description" class="hidden-for-sighted">
<p>{{ I18n.t('js.work_packages.label_column_select') }}</p>
{{ ::I18n.t('js.work_packages.label_column_select') }}
</div>
</div>
<div>
<button class="button -highlight" ng-click="updateGroupBy()">
{{ I18n.t('js.modals.button_apply') }}
{{ ::I18n.t('js.modals.button_apply') }}
</button>
<button class="button" ng-click="modal.closeMe()">
{{ I18n.t('js.modals.button_cancel') }}
{{ ::I18n.t('js.modals.button_cancel') }}
</button>
</div>

@ -1,13 +1,13 @@
<div class="ng-modal-window">
<div class="ng-modal-inner">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.label_save_as') }}</h3>
<h3>{{ ::I18n.t('js.label_save_as') }}</h3>
<form name="modalSaveForm" class="form">
<div class="form--field -required">
<label class="form--label">
{{ I18n.t('js.modals.label_name') }}
{{ ::I18n.t('js.modals.label_name') }}
</label>
<div class="form--field-container">
<div class="form--text-field-container">
@ -20,10 +20,10 @@
<button class="button -highlight"
ng-click="saveQueryAs(queryName)"
ng-disabled="modalSaveForm.$invalid">
{{ I18n.t('js.modals.button_save') }}
{{ ::I18n.t('js.modals.button_save') }}
</button>
<button class="button" ng-click="modal.closeMe()">
{{ I18n.t('js.modals.button_cancel') }}
{{ ::I18n.t('js.modals.button_cancel') }}
</button>
</div>
</form>

@ -1,13 +1,13 @@
<div class="ng-modal-window">
<div class="ng-modal-inner">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.modals.label_settings') }}</h3>
<h3>{{ ::I18n.t('js.modals.label_settings') }}</h3>
<form name="modalSettingsForm" class="form">
<div class="form--field -required">
<label class="form--label" for="query_name">
{{ I18n.t('js.modals.label_name') }}
{{ ::I18n.t('js.modals.label_name') }}
</label>
<div class="form--field-container">
<div class="form--text-field-container">
@ -19,8 +19,8 @@
<div class="form--space">
<button class="button -highlight"
ng-click="updateQuery()"
ng-disabled="modalSettingsForm.$invalid">{{ I18n.t('js.modals.button_submit') }}</button>
<button class="button" ng-click="modal.closeMe()">{{ I18n.t('js.modals.button_cancel') }}</button>
ng-disabled="modalSettingsForm.$invalid">{{ ::I18n.t('js.modals.button_submit') }}</button>
<button class="button" ng-click="modal.closeMe()">{{ ::I18n.t('js.modals.button_cancel') }}</button>
</div>
</form>

@ -1,8 +1,8 @@
<div class="ng-modal-window">
<div class="ng-modal-inner">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.label_share') }}</h3>
<h3>{{ ::I18n.t('js.label_share') }}</h3>
<div>
<label class="checkbox-label">
@ -12,7 +12,7 @@
ng-disabled="cannot('query', 'publicize') && cannot('query', 'depublicize')"
focus></input>
<div class="styled-checkbox"></div>
{{ I18n.t('js.label_visible_for_others') }}
{{ ::I18n.t('js.label_visible_for_others') }}
</label>
</div>
<div>
@ -22,15 +22,15 @@
ng-model="shareSettings.starred"
ng-disabled="query.isGlobal() || cannot('query', 'star')"></input>
<div class="styled-checkbox"></div>
{{ I18n.t('js.label_show_in_menu') }}
{{ ::I18n.t('js.label_show_in_menu') }}
</label>
</div>
<div>
<button class="button -highlight" ng-click="saveQuery()">
{{ I18n.t('js.modals.button_save') }}
{{ ::I18n.t('js.modals.button_save') }}
</button>
<button class="button" ng-click="modal.closeMe()">
{{ I18n.t('js.modals.button_cancel') }}
{{ ::I18n.t('js.modals.button_cancel') }}
</button>
</div>

@ -1,22 +1,22 @@
<div class="ng-modal-window">
<div class="ng-modal-inner modal-content">
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()"></i></div>
<div class="modal-header"><i class="icon-close" ng-click="modal.closeMe()" title="{{ ::I18n.t('js.close_popup_title') }}"></i></div>
<h3>{{ I18n.t('js.label_sorting') }}</h3>
<h3>{{ ::I18n.t('js.label_sorting') }}</h3>
<form name="modalSortingForm">
<div id="modal-sorting" class="modal-content-container" cg-busy="promise">
<div class="form--row" ng-repeat="element in sortElements">
<div class="form--field -full-width">
<div class="form--field-container">
<ui-select ng-model="element[0]" theme="select2" focus="!$index" aria-describedby="sorting_select_description">
<ui-select ng-model="element[0]" theme="select2" focus="!$index" aria-labelledby="sorting_select_description">
<ui-select-match>{{$select.selected.label}}</ui-select-match>
<ui-select-choices repeat="column as column in getRemainingAvailableColumnsData() | filter: { title: $select.search }">
<div ng-bind-html="column.label | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<div class="form--tooltip-container">
<span class="tooltip--right -multiline" tabindex="0" data-tooltip="{{ I18n.t('js.work_packages.label_column_select') }}">
<span class="tooltip--right -multiline" tabindex="0" data-tooltip="{{ ::I18n.t('js.work_packages.label_column_select') }}" aria-labelledby="sorting_select_description">
<i class="icon icon-help1"></i>
</span>
</div>
@ -46,16 +46,16 @@
</div>
</div>
<div id="sorting_select_description" class="hidden-for-sighted">
<p>{{ I18n.t('js.work_packages.label_column_select') }}</p>
{{ ::I18n.t('js.work_packages.label_column_select') }}
</div>
</div>
<button class="button -highlight"
ng-disabled="modalSortingForm.$invalid"
ng-click="updateSortation()">
{{ I18n.t('js.modals.button_apply') }}
{{ ::I18n.t('js.modals.button_apply') }}
</button>
<button class="button" ng-click="modal.closeMe()">
{{ I18n.t('js.modals.button_cancel') }}
{{ ::I18n.t('js.modals.button_cancel') }}
</button>
</form>

@ -13,7 +13,7 @@
</div>
</div>
<div ng-repeat="group in vm.groupedFields" ng-hide="vm.hideEmptyFields && vm.isGroupEmpty(group.groupName)" class="attributes-group">
<div ng-repeat="group in vm.groupedFields" ng-hide="vm.hideEmptyFields && vm.isGroupHideable(group.groupName)" class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
@ -31,13 +31,13 @@
<dl class="attributes-key-value">
<dt
ng-hide="vm.hideEmptyFields && vm.isFieldEmpty(field)"
ng-hide="vm.hideEmptyFields && vm.isFieldHideable(field)"
ng-if="vm.isSpecified(field)"
ng-repeat-start="field in group.attributes" class="attributes-key-value--key"
ng-bind="vm.getLabel(field)">
</dt>
<dd
ng-hide="vm.hideEmptyFields && vm.isFieldEmpty(field)"
ng-hide="vm.hideEmptyFields && vm.isFieldHideable(field)"
ng-if="vm.isSpecified(field)"
ng-repeat-end
class="attributes-key-value--value-container">

@ -38,8 +38,8 @@ module.exports = function(
vm.hideEmptyFields = true;
vm.workPackage = $scope.workPackage;
vm.isGroupEmpty = isGroupEmpty;
vm.isFieldEmpty = isFieldEmpty;
vm.isGroupHideable = isGroupHideable;
vm.isFieldHideable = isFieldHideable;
vm.getLabel = getLabel;
vm.isSpecified = isSpecified;
vm.showToggleButton = showToggleButton;
@ -53,26 +53,32 @@ module.exports = function(
vm.workPackage = $scope.workPackage;
}
});
vm.groupedFields = WorkPackagesOverviewService.getGroupedWorkPackageOverviewAttributes();
var otherGroup = _.find(vm.groupedFields, {groupName: 'other'});
_.forEach(vm.workPackage.schema.props, function(prop, propName) {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
otherGroup.attributes.sort(function(a, b) {
return getLabel(a).toLowerCase().localeCompare(getLabel(b).toLowerCase());
$scope.$watchCollection('vm.workPackage.form', function(form) {
var schema = WorkPackageFieldService.getSchema(vm.workPackage);
var otherGroup = _.find(vm.groupedFields, {groupName: 'other'});
otherGroup.attributes = [];
_.forEach(schema.props, function(prop, propName) {
if (propName.match(/^customField/)) {
otherGroup.attributes.push(propName);
}
});
otherGroup.attributes.sort(function(a, b) {
return getLabel(a).toLowerCase().localeCompare(getLabel(b).toLowerCase());
});
});
}
function isGroupEmpty(groupName) {
function isGroupHideable(groupName) {
var group = _.find(vm.groupedFields, {groupName: groupName});
return _.every(group.attributes, isFieldEmpty);
return _.every(group.attributes, isFieldHideable);
}
function isFieldEmpty(field) {
return WorkPackageFieldService.isEmpty(vm.workPackage, field);
function isFieldHideable(field) {
return WorkPackageFieldService.isHideable(vm.workPackage, field);
}
function isSpecified(field) {

@ -44,7 +44,8 @@ module.exports = function(WorkPackageFieldService, EditableFieldsState, I18n, $t
scope.fieldController.isBusy = true;
WorkPackageFieldService.getAllowedValues(
EditableFieldsState.workPackage,
fieldController.field).then(function(values) {
fieldController.field
).then(function(values) {
scope.customEditorController.allowedValues = values;
scope.fieldController.isBusy = false;
$timeout(function() {

@ -91,6 +91,15 @@ module.exports = function(
scope.displayPaneController.placeholder = I18n.t('js.label_click_to_enter_description');
}
scope.editableFieldsState = EditableFieldsState;
scope.$watch('editableFieldsState.errors', function(errors) {
if (errors) {
if (errors[scope.fieldController.field]) {
scope.displayPaneController.startEditing();
}
}
}, true);
scope.$watch('fieldController.isEditing', function(isEditing, oldIsEditing) {
if (!isEditing) {
$timeout(function() {

@ -45,23 +45,33 @@ module.exports = function(
fieldController.isBusy = true;
var pendingFormChanges = getPendingFormChanges();
pendingFormChanges[fieldController.field] = fieldController.writeValue;
var result = WorkPackageService.updateWorkPackage(EditableFieldsState.workPackage, notify);
result.then(angular.bind(this, function() {
$scope.$emit(
'workPackageRefreshRequired',
function() {
fieldController.isBusy = false;
fieldController.isEditing = false;
fieldController.updateWriteValue();
EditableFieldsState.error = null;
WorkPackageService.loadWorkPackageForm(EditableFieldsState.workPackage).then(
function(form) {
if (_.isEmpty(form.embedded.validationErrors.props)) {
var result = WorkPackageService.updateWorkPackage(
EditableFieldsState.workPackage,
notify
);
result.then(angular.bind(this, function() {
$scope.$emit(
'workPackageRefreshRequired',
function() {
fieldController.isBusy = false;
fieldController.isEditing = false;
fieldController.updateWriteValue();
EditableFieldsState.errors = null;
}
);
})).catch(setFailure);
} else {
afterError();
EditableFieldsState.errors = {};
_.forEach(form.embedded.validationErrors.props, function(error, field) {
EditableFieldsState.errors[field] = error.message;
});
}
);
}));
result.catch(angular.bind(this, function(e) {
fieldController.isBusy = false;
EditableFieldsState.error = ApiHelper.getErrorMessage(e);
$scope.focusInput();
}));
}).catch(setFailure);
};
@ -69,6 +79,12 @@ module.exports = function(
$scope.fieldController.isEditing = false;
delete getPendingFormChanges()[$scope.fieldController.field];
$scope.fieldController.updateWriteValue();
if (
EditableFieldsState.errors &&
EditableFieldsState.errors.hasOwnProperty($scope.fieldController.field)
) {
delete EditableFieldsState.errors[$scope.fieldController.field];
}
};
this.getPendingFormChanges = getPendingFormChanges;
@ -78,6 +94,17 @@ module.exports = function(
form.pendingChanges = form.pendingChanges || angular.copy(form.embedded.payload.props);
return form.pendingChanges;
}
function afterError() {
$scope.fieldController.isBusy = false;
$scope.focusInput();
}
function setFailure(e) {
afterError();
EditableFieldsState.errors = {
'_common': ApiHelper.getErrorMessage(e)
};
}
},
link: function(scope, element, attrs, fieldController) {
scope.fieldController = fieldController;
@ -116,13 +143,21 @@ module.exports = function(
scope.editPaneController.discardEditing();
});
scope.editableFieldsState = EditableFieldsState;
scope.$watch('editableFieldsState.error', function(error) {
scope.editPaneController.error = error;
});
scope.$watch('editableFieldsState.errors', function(errors) {
scope.editPaneController.error = null;
if (!_.isEmpty(errors)) {
// uncomment when we are sure we can bind every message to every field
// scope
// .editPaneController
// .error = errors[scope.fieldController.field] || errors['_common'];
scope.editPaneController.error = _.map(errors, function(error) {
return error;
}).join('\n');
}
}, true);
scope.$watch('fieldController.isEditing', function(isEditing) {
if (isEditing) {
EditableFieldsState.error = null;
scope.focusInput();
}
});

@ -29,7 +29,7 @@
module.exports = function() {
return {
workPackage: null,
error: null,
errors: null,
isBusy: false
};
};

@ -36,7 +36,7 @@ angular.module('openproject.workPackages.services')
.constant('WORK_PACKAGE_ATTRIBUTES', [
{
groupName: 'details',
attributes: ['status', 'percentageDone', 'date', 'priority', 'version', 'category']
attributes: ['type', 'status', 'percentageDone', 'date', 'priority', 'version', 'category']
},
{
groupName: 'people',
@ -65,8 +65,9 @@ angular.module('openproject.workPackages.services')
'$q',
'$http',
'HookService',
'EditableFieldsState',
require('./work-package-field-service')
])
.service('EditableFieldsState',
require('./work-package-field-service')
require('./editable-fields-state')
);

@ -32,13 +32,24 @@ module.exports = function(
WorkPackagesHelper,
$q,
$http,
HookService) {
/* global moment */
HookService,
EditableFieldsState
) {
function getSchema(workPackage) {
if (workPackage.form) {
return workPackage.form.embedded.schema;
} else {
return workPackage.schema;
}
}
function isEditable(workPackage, field) {
// no form - no editing
if (!workPackage.form) {
return false;
}
var schema = getSchema(workPackage);
// TODO: extract to strategy if new cases arise
if (field === 'date') {
// nope
@ -46,14 +57,13 @@ module.exports = function(
//return workPackage.schema.props.startDate.writable
// && workPackage.schema.props.dueDate.writable;
}
if(workPackage.schema.props[field].type === 'Date') {
if(schema.props[field].type === 'Date') {
return false;
}
var isWritable = workPackage.schema.props[field].writable;
var isWritable = schema.props[field].writable;
// not writable if no embedded allowed values
if (workPackage.form && workPackage.form.embedded.schema
.props[field]._links && allowedValuesEmbedded(workPackage, field)) {
if (schema.props[field]._links && allowedValuesEmbedded(workPackage, field)) {
if (getEmbeddedAllowedValues(workPackage, field).length === 0) {
return false;
}
@ -62,12 +72,23 @@ module.exports = function(
}
function isSpecified(workPackage, field) {
var schema = getSchema(workPackage);
if (field === 'date') {
// kind of specified
return true;
}
return !_.isUndefined(workPackage.schema
.props[field]);
return !_.isUndefined(schema.props[field]);
}
// under special conditions fields will be shown
// irregardless if they are empty or not
// e.g. when an error should trigger the editing state
// of an empty field after type change
function isHideable(workPackage, field) {
if (EditableFieldsState.errors && EditableFieldsState.errors[field]) {
return false;
}
return isEmpty(workPackage, field);
}
function getValue(workPackage, field) {
@ -91,14 +112,14 @@ module.exports = function(
}
function allowedValuesEmbedded(workPackage, field) {
return _.isArray(workPackage.form.embedded.schema
.props[field]._links.allowedValues);
var schema = getSchema(workPackage);
return _.isArray(schema.props[field]._links.allowedValues);
}
function getEmbeddedAllowedValues(workPackage, field) {
var options = [];
var allowedValues = workPackage.form.embedded.schema
.props[field]._links.allowedValues;
var schema = getSchema(workPackage);
var allowedValues = schema.props[field]._links.allowedValues;
options = _.map(allowedValues, function(item) {
return _.extend({}, item, { name: item.title });
});
@ -112,8 +133,8 @@ module.exports = function(
}
function getLinkedAllowedValues(workPackage, field) {
var href = workPackage.form.embedded.schema
.props[field]._links.allowedValues.href;
var schema = getSchema(workPackage);
var href = schema.props[field]._links.allowedValues.href;
return $http.get(href).then(function(r) {
var options = [];
options = _.map(r.data._embedded.elements, function(item) {
@ -138,8 +159,8 @@ module.exports = function(
}
function isRequired(workPackage, field) {
return workPackage.form.embedded.schema
.props[field].required;
var schema = getSchema(workPackage);
return schema.props[field].required;
}
function isEmbedded(workPackage, field) {
@ -151,11 +172,12 @@ module.exports = function(
}
function getLabel(workPackage, field) {
var schema = getSchema(workPackage);
if (field === 'date') {
// special case
return I18n.t('js.work_packages.properties.date');
}
return workPackage.schema.props[field].name;
return schema.props[field].name;
}
function isEmpty(workPackage, field) {
@ -182,13 +204,14 @@ module.exports = function(
}
function getInplaceEditStrategy(workPackage, field) {
var schema = getSchema(workPackage);
var fieldType = null,
inplaceType = 'text';
if (field === 'date') {
fieldType = 'DateRange';
} else {
fieldType = workPackage.form.embedded.schema.props[field].type;
fieldType = schema.props[field].type;
}
switch(fieldType) {
case 'DateRange':
@ -219,6 +242,7 @@ module.exports = function(
case 'Status':
case 'Priority':
case 'Category':
case 'Type':
inplaceType = 'dropdown';
break;
}
@ -234,6 +258,7 @@ module.exports = function(
}
function getInplaceDisplayStrategy(workPackage, field) {
var schema = getSchema(workPackage);
var fieldType = null,
displayStrategy = 'embedded';
if (field === 'date') {
@ -241,7 +266,7 @@ module.exports = function(
} else if (field === 'spentTime') {
fieldType = 'SpentTime';
} else {
fieldType = workPackage.schema.props[field].type;
fieldType = schema.props[field].type;
}
switch(fieldType) {
case 'String':
@ -283,6 +308,7 @@ module.exports = function(
}
function format(workPackage, field) {
var schema = getSchema(workPackage);
if (field === 'date') {
return {
startDate: workPackage.props.startDate,
@ -309,13 +335,13 @@ module.exports = function(
updatedAt: 'datetime'
};
if (workPackage.schema.props[field]) {
if (workPackage.schema.props[field].type === 'Duration') {
if (schema.props[field]) {
if (schema.props[field].type === 'Duration') {
var hours = moment.duration(value).asHours();
return I18n.t('js.units.hour', { count: hours.toFixed(2) });
}
if (workPackage.schema.props[field].type === 'Boolean') {
if (schema.props[field].type === 'Boolean') {
return value ? I18n.t('js.general_text_yes') : I18n.t('js.general_text_no');
}
}
@ -324,10 +350,12 @@ module.exports = function(
}
var WorkPackageFieldService = {
getSchema: getSchema,
isEditable: isEditable,
isRequired: isRequired,
isSpecified: isSpecified,
isEmpty: isEmpty,
isHideable: isHideable,
isEmbedded: isEmbedded,
isSavedAsLink: isSavedAsLink,
getValue: getValue,

@ -134,7 +134,7 @@
#
# == Adding Tables
#
# In Textile, simple tables can be added by seperating each column by
# In Textile, simple tables can be added by separating each column by
# a pipe.
#
# |a|simple|table|row|

Loading…
Cancel
Save