Convert user directive to edit in markdown mode

pull/6015/head
Oliver Günther 7 years ago
parent bff8926898
commit 1da55e4095
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 2
      frontend/app/components/work-packages/work-package-comment/work-package-comment.directive.ts
  2. 284
      frontend/app/components/wp-activity/user/user-activity-directive.ts
  3. 32
      frontend/app/components/wp-edit/field-types/wp-edit-wiki-textarea-field.module.ts
  4. 96
      frontend/app/templates/work_packages/activities/_user.html
  5. 6
      spec/features/work_packages/details/inplace_editor/shared_examples.rb
  6. 199
      spec/features/work_packages/details/markdown/activity_comments_spec.rb
  7. 0
      spec/features/work_packages/details/markdown/description_editor_spec.rb
  8. 4
      spec/features/work_packages/details/textile/activity_comments_spec.rb
  9. 130
      spec/features/work_packages/details/textile/description_editor_spec.rb
  10. 1
      spec/support/pages/abstract_work_package.rb
  11. 1
      spec/support/pages/page.rb

@ -98,9 +98,7 @@ export class CommentFieldDirectiveController {
this._forceFocus = true;
this.editing = true;
if (!this.field) {
this.resetField(withText);
}
this.waitForField()
.then(() => {

@ -30,156 +30,194 @@ import {UserResource} from '../../api/api-v3/hal-resources/user-resource.service
import {WorkPackageCacheService} from '../../work-packages/work-package-cache.service';
import {TextileService} from './../../common/textile/textile-service';
import {ActivityService} from './../activity-service';
angular
.module('openproject.workPackages.activities')
.directive('userActivity', userActivity);
function userActivity($uiViewScroll:any,
$timeout:ng.ITimeoutService,
$location:ng.ILocationService,
$sce:ng.ISCEService,
I18n:op.I18n,
PathHelper:any,
wpActivityService:ActivityService,
wpCacheService:WorkPackageCacheService,
ConfigurationService:any,
AutoCompleteHelper:any,
textileService:TextileService) {
return {
restrict: 'E',
replace: true,
templateUrl: '/templates/work_packages/activities/_user.html',
scope: {
workPackage: '=',
activity: '=',
activityNo: '=',
activityLabel: '=',
isInitial: '='
},
link: function (scope:any, element:ng.IAugmentedJQuery) {
scope.$watch('inEdit', function (newVal:boolean, oldVal:boolean) {
var textarea = element.find('.edit-comment-text');
if (newVal) {
$timeout(function () {
AutoCompleteHelper.enableTextareaAutoCompletion(textarea);
textarea.focus();
textarea.on('keydown keypress', function (e) {
if (e.keyCode === 27) {
scope.inEdit = false;
import {PathHelperService} from 'core-components/common/path-helper/path-helper.service';
import {ConfigurationService} from 'core-components/common/config/configuration.service';
import {WorkPackageResourceInterface} from 'core-components/api/api-v3/hal-resources/work-package-resource.service';
import {ActivityEntryInfo} from 'core-components/wp-single-view-tabs/activity-panel/activity-entry-info';
import {HalResource} from 'core-components/api/api-v3/hal-resources/hal-resource.service';
import {WorkPackageCommentField} from 'core-components/work-packages/work-package-comment/wp-comment-field.module';
export class UserActivityController {
public workPackage:WorkPackageResourceInterface;
public activity:HalResource;
public activityNo:string;
public activityLabel:string;
public isInitial:boolean;
public inEdit = false;
public inEditMode = false;
public userCanEdit= false;
public userCanQuote = false;
public userId:string|number;
public userName:string;
public userAvatar:string;
public userActive:boolean;
public userPath:string|null;
public userLabel:string;
public postedComment:string;
public activityLabelWithComment?:string;
public details:any[] = [];
public field:WorkPackageCommentField;
public focused = false;
public accessibilityModeEnabled = this.ConfigurationService.accessibilityModeEnabled();
constructor(readonly $uiViewScroll:any,
readonly $scope:ng.IScope,
readonly $timeout:ng.ITimeoutService,
readonly $q:ng.IQService,
readonly $element:ng.IAugmentedJQuery,
readonly $location:ng.ILocationService,
readonly $sce:ng.ISCEService,
readonly I18n:op.I18n,
readonly PathHelper:PathHelperService,
readonly wpActivityService:ActivityService,
readonly wpCacheService:WorkPackageCacheService,
readonly ConfigurationService:ConfigurationService,
readonly AutoCompleteHelper:any,
readonly textileService:TextileService) {
}
public $onInit() {
this.resetField();
this.userCanEdit = !!this.activity.update;
this.userCanQuote = !!this.workPackage.addComment;
this.postedComment = this.$sce.trustAsHtml(this.activity.comment.html);
if (this.postedComment) {
this.activityLabelWithComment = I18n.t('js.label_activity_with_comment_no', {
activityNo: this.activityNo
});
});
} else {
textarea.off('keydown keypress');
}
});
scope.I18n = I18n;
scope.inEdit = false;
scope.inPreview = false;
scope.userCanEdit = !!scope.activity.update;
scope.userCanQuote = !!scope.workPackage.addComment;
scope.accessibilityModeEnabled = ConfigurationService.accessibilityModeEnabled();
scope.activity.user.$load().then((user:UserResource) => {
scope.userId = user.id;
scope.userName = user.name;
scope.userAvatar = user.avatar;
scope.userActive = user.isActive;
scope.userPath = user.showUser.href;
scope.userLabel = I18n.t('js.label_author', {user: scope.userName});
this.$element.bind('focusin', this.focus.bind(this));
this.$element.bind('focusout', this.blur.bind(this));
angular.forEach(this.activity.details, (detail:any) => {
this.details.push(this.$sce.trustAsHtml(detail.html));
});
scope.postedComment = $sce.trustAsHtml(scope.activity.comment.html);
if (scope.postedComment) {
scope.activityLabelWithComment = I18n.t('js.label_activity_with_comment_no', {
activityNo: scope.activityNo
if (this.$location.hash() === 'activity-' + this.activityNo) {
this.$uiViewScroll(this.$element);
}
this.activity.user.$load().then((user:UserResource) => {
this.userId = user.id;
this.userName = user.name;
this.userAvatar = user.avatar;
this.userActive = user.isActive;
this.userPath = user.showUser.href;
this.userLabel = this.I18n.t('js.label_author', {user: this.userName});
});
}
scope.details = [];
angular.forEach(scope.activity.details, function (this:any[], detail) {
this.push($sce.trustAsHtml(detail.html));
}, scope.details);
public resetField(withText?:string) {
this.field = new WorkPackageCommentField(this.workPackage, I18n);
this.field.initializeFieldValue(withText);
}
$timeout(function () {
if ($location.hash() === 'activity-' + scope.activityNo) {
$uiViewScroll(element);
public handleUserSubmit() {
this.field.onSubmit();
if (this.field.isBusy || this.field.isEmpty()) {
return;
}
this.updateComment();
}
});
scope.editComment = function () {
scope.activity.editedComment = scope.activity.comment.raw;
scope.isPreview = false;
scope.inEdit = true;
};
public handleUserCancel() {
this.inEdit = false;
this.focusEditIcon();
}
scope.cancelEdit = function () {
scope.inEdit = false;
scope.focusEditIcon();
};
public get active() {
return this.inEdit;
}
scope.quoteComment = function () {
wpActivityService.quoteEvents.putValue(quotedText(scope.activity.comment.raw));
};
public editComment() {
this.inEdit = true;
scope.updateComment = function () {
wpActivityService.updateComment(scope.activity, scope.activity.editedComment || '').then(function () {
scope.workPackage.updateActivities();
scope.inEdit = false;
this.resetField(this.activity.comment.raw);
this.waitForField()
.then(() => {
this.field.$onInit(this.$element);
});
scope.focusEditIcon();
};
}
scope.focusEditIcon = function () {
// Find the according edit icon and focus it
jQuery('.edit-activity--' + scope.activityNo + ' a').focus();
};
// Ensure the nested ng-include has rendered
private waitForField():Promise<JQuery> {
const deferred = this.$q.defer<JQuery>();
scope.toggleCommentPreview = function () {
scope.isPreview = !scope.isPreview;
scope.previewHtml = '';
if (scope.isPreview) {
textileService.renderWithWorkPackageContext(
scope.workPackage,
scope.activity.editedComment
).then(function (r:any) {
scope.previewHtml = $sce.trustAsHtml(r.data);
}).catch(() => {
scope.isPreview = false;
});
const interval = setInterval(() => {
const container = this.$element.find('.op-ckeditor-element');
if (container.length > 0) {
clearInterval(interval);
deferred.resolve(container);
}
};
}, 100);
var focused = false;
scope.focus = function () {
$timeout(function () {
focused = true;
});
};
return deferred.promise;
}
public quoteComment() {
this.wpActivityService.quoteEvents.putValue(this.quotedText(this.activity.comment.raw));
}
scope.blur = function () {
$timeout(function () {
focused = false;
public updateComment() {
this.wpActivityService.updateComment(this.activity, this.field.rawValue || '')
.then(() => {
this.workPackage.updateActivities();
this.inEdit = false;
});
};
this.focusEditIcon();
}
scope.focussing = function () {
return focused;
};
public focusEditIcon() {
// Find the according edit icon and focus it
jQuery('.edit-activity--' + this.activityNo + ' a').focus();
}
public focus() {
this.$timeout(() => this.focused = true);
}
public blur() {
this.$timeout(() => this.focused = false);
}
element.bind('focusin', scope.focus);
element.bind('focusout', scope.blur);
public focussing() {
return this.focused;
}
function quotedText(rawComment:string) {
var quoted = rawComment.split("\n")
public quotedText(rawComment:string) {
var quoted = rawComment.split('\n')
.map(function (line:string) {
return "\n> " + line;
return '\n> ' + line;
})
.join('');
return scope.userName + " wrote:\n" + quoted;
return this.userName + ' wrote:\n' + quoted;
}
}
};
}
angular
.module('openproject.workPackages.activities')
.directive('userActivity', function() {
return {
restrict: 'E',
templateUrl: '/templates/work_packages/activities/_user.html',
scope: {
workPackage: '=',
activity: '=',
activityNo: '=',
activityLabel: '=',
isInitial: '='
},
controller: UserActivityController,
bindToController: true,
controllerAs: 'vm'
};
});

@ -52,6 +52,8 @@ export class WikiTextareaEditField extends EditField {
public isPreview:boolean = false;
public previewHtml:string;
public text:Object;
public wysiwig:boolean;
// CKEditor instance
public ckeditor:any;
@ -59,6 +61,7 @@ export class WikiTextareaEditField extends EditField {
protected initialize() {
$injectFields(this, '$sce', '$http', 'textileService', '$timeout', 'AutoCompleteHelper', 'I18n', 'ConfigurationService');
this.wysiwig = this.ConfigurationService.textFormat() === 'markdown';
this.setupTemplate();
this.text = {
@ -69,27 +72,28 @@ export class WikiTextareaEditField extends EditField {
}
public setupTemplate() {
switch (this.ConfigurationService.textFormat()) {
case 'markdown':
if (this.wysiwig) {
this.template = '/components/wp-edit/field-types/wp-edit-markdown-field.directive.html';
break;
default:
} else {
this.template = '/components/wp-edit/field-types/wp-edit-wiki-textarea-field.directive.html';
break;
}
}
public onSubmit() {
if (this.ckeditor) {
if (this.wysiwig && this.ckeditor) {
this.rawValue = this.ckeditor.getData();
}
}
public get isInitialized() {
return !!this.ckeditor;
public $onInit(container:JQuery) {
if (this.wysiwig) {
this.setupMarkdownEditor(container);
} else {
jQuery('body').css('background', 'red !important');
}
}
public $onInit(container:JQuery) {
public setupMarkdownEditor(container:JQuery) {
const element = container.find('.op-ckeditor-element');
(window as any).BalloonEditor
.create(element[0])
@ -104,7 +108,7 @@ export class WikiTextareaEditField extends EditField {
this.reset();
}
this.setupAutocompletion(element);
this.AutoCompleteHelper.enableTextareaAutoCompletion(element, this.resource.project.id);
setTimeout(() => editor.editing.view.focus());
})
@ -113,13 +117,11 @@ export class WikiTextareaEditField extends EditField {
});
}
public setupAutocompletion(element:JQuery) {
this.AutoCompleteHelper.enableTextareaAutoCompletion(element, this.resource.project.id);
}
public reset() {
if (this.wysiwig) {
this.ckeditor.setData(this.rawValue);
}
}
public get rawValue() {
if (this.value && this.value.raw) {
@ -138,7 +140,7 @@ export class WikiTextareaEditField extends EditField {
}
public isEmpty():boolean {
if (this.isInitialized) {
if (this.wysiwig && this.ckeditor) {
return this.ckeditor.getData() === '';
} else {
return !(this.value && this.value.raw);

@ -1,77 +1,61 @@
<div class="work-package-details-activities-activity-contents"
tabindex="0"
aria-label="{{ activityLabelWithComment || activityLabel }}"
ng-mouseenter="focus()"
ng-mouseleave="blur()">
<div ng-if="userAvatar">
<img class="avatar" ng-src="{{ userAvatar }}" alt="Avatar" title="{{userName}}" />
</div>
<span class="user" ng-if="userActive">
<a ng-href="{{ userPath }}"
aria-label="{{ userLabel }}"
ng-bind="userName">
aria-label="{{ vm.activityLabelWithComment || vm.activityLabel }}"
ng-mouseenter="vm.focus()"
ng-mouseleave="vm.blur()">
<div ng-if="vm.userAvatar">
<img class="avatar" ng-src="{{ vm.userAvatar }}" alt="Avatar" title="{{vm.userName}}" />
</div>
<span class="user" ng-if="vm.userActive">
<a ng-href="{{ vm.userPath }}"
aria-label="{{ vm.userLabel }}"
ng-bind="vm.userName">
</a>
</span>
<span class="user" ng-if="!userActive">{{ userName }}</span>
<span class="date">{{ isInitial ? I18n.t('js.label_created_on') : I18n.t('js.label_updated_on') }} <op-date-time date-time-value="activity.createdAt" /></span>
<span class="user" ng-if="!vm.userActive">{{ vm.userName }}</span>
<span class="date">
{{ vm.isInitial ? I18n.t('js.label_created_on') : I18n.t('js.label_updated_on') }}
<op-date-time date-time-value="vm.activity.createdAt" /></span>
<div class="comments-number">
<activity-link work-package="workPackage"
activity-no="activityNo"
<activity-link work-package="vm.workPackage"
activity-no="vm.activityNo"
></activity-link>
<div class="comments-icons"
ng-show="activity._type == 'Activity::Comment' && (focussing() || accessibilityModeEnabled)">
<accessible-by-keyboard ng-if="userCanQuote"
execute="quoteComment()"
ng-show="vm.activity._type == 'Activity::Comment' && (vm.focussing() || vm.accessibilityModeEnabled)">
<accessible-by-keyboard ng-if="vm.userCanQuote"
execute="vm.quoteComment()"
link-title="{{ I18n.t('js.label_quote_comment') }}">
<op-icon icon-classes="action-icon icon-quote" icon-title="{{ I18n.t('js.label_quote_comment') }}"></op-icon>
<op-icon icon-classes="action-icon icon-quote"
icon-title="{{ I18n.t('js.label_quote_comment') }}"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard ng-if="userCanEdit"
execute="editComment()"
<accessible-by-keyboard ng-if="vm.userCanEdit"
execute="vm.editComment()"
link-title="{{ I18n.t('js.label_edit_comment') }}"
class="edit-activity--{{activityNo}}">
<op-icon icon-classes="action-icon icon-edit" icon-title="{{ I18n.t('js.label_edit_comment') }}"></op-icon>
class="edit-activity--{{vm.activityNo}}">
<op-icon icon-classes="action-icon icon-edit"
icon-title="{{ I18n.t('js.label_edit_comment') }}"></op-icon>
</accessible-by-keyboard>
</div>
</div>
<div class="user-comment wiki">
<div ng-if="inEdit" class="inplace-edit">
<div ng-if="vm.inEdit" class="inplace-edit">
<div class="user-comment--form inplace-edit--write-value">
<div class="textarea-wrapper" ng-class="{'-preview': isPreview}">
<textarea wiki-toolbar
msd-elastic="\n"
op-auto-complete
class="edit-comment-text focus-input inplace-edit--textarea"
id="inplace-edit--write-value--activity-comment"
ng-hide="isPreview"
ng-model="activity.editedComment"
preview-toggle="toggleCommentPreview()"
required>
</textarea>
<div class="inplace-edit--preview" ng-if="isPreview">
<span bind-unescaped-html="previewHtml"></span>
</div>
<div class="inplace-edit--dashboard">
<div class="inplace-edit--controls">
<accessible-by-keyboard execute="updateComment()"
ng-disabled="editCommentForm.$invalid"
class="inplace-edit--control inplace-edit--control--save">
<op-icon icon-classes="action-icon icon-checkmark" icon-title="{{ I18n.t('js.button_save') }}"></op-icon>
</accessible-by-keyboard>
<accessible-by-keyboard execute="cancelEdit()"
class="inplace-edit--control inplace-edit--control--cancel">
<op-icon icon-classes="action-icon icon-close" icon-title="{{ I18n.t('js.button_cancel') }}"></op-icon>
</accessible-by-keyboard>
</div>
</div>
</div>
<form name="wp-edit-form-coment"
ng-submit="vm.handleUserSubmit()"
role="form"
field-name="'comment'"
wp-attachments-formattable
tabindex="-1">
<ng-include src="vm.field.template"></ng-include>
</form>
</div>
</div>
<span ng-if="!inEdit"
<span ng-if="!vm.inEdit"
class="message"
ng-show="activity._type == 'Activity::Comment'"
bind-unescaped-html="postedComment"/>
<ul class="work-package-details-activities-messages" ng-if="!isInitial">
<li ng-repeat="detail in details track by $index">
ng-show="vm.activity._type == 'Activity::Comment'"
bind-unescaped-html="vm.postedComment"/>
<ul class="work-package-details-activities-messages" ng-if="!vm.isInitial">
<li ng-repeat="detail in vm.details track by $index">
<span class="message" ng-bind-html="detail"/>
</li>
</ul>

@ -120,7 +120,11 @@ end
shared_examples 'a workpackage autocomplete field' do
let!(:wp2) { FactoryGirl.create(:work_package, project: project, subject: 'AutoFoo') }
xit 'autocompletes the other work package' do
before do
skip('Markdown mode does not provide autocompleting') if Setting.text_formatting == 'markdown'
end
it 'autocompletes the other work package' do
field.activate!
field.input_element.send_keys(" ##{wp2.id}")
expect(page).to have_selector('.atwho-view-ul li.cur', text: wp2.to_s.strip)

@ -0,0 +1,199 @@
require 'spec_helper'
require 'features/work_packages/shared_contexts'
require 'features/work_packages/details/inplace_editor/shared_examples'
describe 'activity comments',
with_settings: { text_formatting: 'markdown' },
js: true do
let(:project) { FactoryGirl.create :project, is_public: true }
let!(:work_package) {
FactoryGirl.create(:work_package,
project: project,
journal_notes: initial_comment)
}
let(:wp_page) { Pages::SplitWorkPackage.new(work_package, project) }
let(:selector) { '.work-packages--activity--add-comment' }
let(:comment_field) {
WorkPackageEditorField.new wp_page,
'comment',
selector: selector
}
let(:initial_comment) { 'the first comment in this WP' }
before do
login_as(current_user)
allow(current_user.pref).to receive(:warn_on_leaving_unsaved?).and_return(false)
end
context 'with permission' do
let(:current_user) { FactoryGirl.create :admin }
before do
wp_page.visit!
wp_page.ensure_page_loaded
end
context 'in edit state' do
before do
comment_field.activate!
end
describe 'submitting comment' do
it 'does not submit with enter' do
comment_field.input_element.set 'this is a comment'
comment_field.submit_by_enter
expect(page).to_not have_selector('.user-comment .message', text: 'this is a comment')
end
it 'submits with click' do
comment_field.input_element.set 'this is a comment!1'
comment_field.submit_by_click
expect(page).to have_selector('.user-comment .message', text: 'this is a comment!1')
end
it 'submits comments repeatedly' do
comment_field.input_element.set 'this is my first comment!1'
comment_field.submit_by_click
expect(page).to have_selector('.user-comment > .message', count: 2)
expect(page).to have_selector('.user-comment > .message',
text: 'this is my first comment!1')
expect(comment_field.editing?).to be false
comment_field.activate!
expect(comment_field.editing?).to be true
comment_field.input_element.set 'this is my second comment!1'
comment_field.submit_by_click
expect(page).to have_selector('.user-comment > .message', count: 3)
expect(page).to have_selector('.user-comment > .message',
text: 'this is my second comment!1')
end
end
describe 'cancel comment' do
it do
expect(comment_field.editing?).to be true
comment_field.input_element.set 'this is a comment'
# Escape should NOT cancel the editing
comment_field.cancel_by_escape
expect(comment_field.editing?).to be true
expect(page).to_not have_selector('.user-comment .message', text: 'this is a comment')
# Click should cancel the editing
comment_field.cancel_by_click
expect(comment_field.editing?).to be false
expect(page).to_not have_selector('.user-comment .message', text: 'this is a comment')
end
end
describe 'autocomplete' do
before do
skip 'at.js/autocompleter does not work (yet) in CKEditor'
end
describe 'work packages' do
let!(:wp2) { FactoryGirl.create(:work_package, project: project, subject: 'AutoFoo') }
it 'autocompletes the other work package' do
comment_field.input_element.send_keys("##{wp2.id}")
expect(page).to have_selector('.atwho-view-ul li', text: wp2.to_s.strip)
end
end
describe 'users' do
it_behaves_like 'a principal autocomplete field' do
let(:field) { comment_field }
end
end
end
describe 'quoting' do
it 'can quote a previous comment' do
expect(page).to have_selector('.user-comment .message',
text: initial_comment)
# Hover comment
page.find('.user-comment > .message').hover
# Quote this comment
page.find('.comments-icons .icon-quote').click
expect(comment_field.editing?).to be true
# Add our comment
quote = comment_field.input_element[:value]
expect(quote).to include("> #{initial_comment}")
quote << "\nthis is **some remark** under a quote"
comment_field.input_element.set(quote)
comment_field.submit_by_click
expect(page).to have_selector('.user-comment > .message', count: 2)
expect(page).to have_selector('.user-comment > .message blockquote')
expect(page).to have_selector('.user-comment > .message strong')
end
end
describe 'with an existing comment' do
it 'allows to edit an existing comment' do
comment_field.input_element.set 'Comment with **bold text**'
comment_field.submit_by_click
expect(page).to have_selector('.user-comment .message strong', text: 'bold text')
expect(page).to have_selector('.user-comment .message', text: 'Comment with bold text')
# Hover the new activity
activity = page.find('#activity-2')
page.driver.browser.action.move_to(activity.native).perform
# Check the edit textarea
activity.find('.icon-edit').click
edit = WorkPackageEditorField.new wp_page,
'comment',
selector: '.user-comment--form'
edit.expect_value 'Comment with **bold text**'
edit.set_value 'Comment with _italic text_'
edit.submit_by_click
expect(page).to have_selector('.user-comment .message em', text: 'italic text')
expect(page).to have_selector('.user-comment .message', text: 'Comment with italic text')
# Clear the comment
activity = page.find('#activity-2')
page.driver.browser.action.move_to(activity.native).perform
# Check the edit textarea
activity.find('.icon-edit').click
edit = WorkPackageEditorField.new wp_page,
'comment',
selector: '.user-comment--form'
edit.set_value ''
edit.submit_by_click
expect(page).to have_no_selector('#activity-2 .user-comment .message em', text: 'italic text')
end
end
end
end
context 'with no permission' do
let(:current_user) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
let(:role) { FactoryGirl.create :role, permissions: %i(view_work_packages) }
before do
wp_page.visit!
wp_page.ensure_page_loaded
end
it 'does not show the field' do
expect(page).to have_no_selector(selector, visible: true)
end
end
end

@ -3,7 +3,9 @@ require 'spec_helper'
require 'features/work_packages/shared_contexts'
require 'features/work_packages/details/inplace_editor/shared_examples'
describe 'activity comments', js: true, selenium: true do
describe 'activity comments (textile)',
with_settings: { text_formatting: 'textile' },
js: true do
let(:project) { FactoryGirl.create :project, is_public: true }
let!(:work_package) {
FactoryGirl.create(:work_package,

@ -0,0 +1,130 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'features/work_packages/details/inplace_editor/shared_examples'
require 'features/work_packages/shared_contexts'
require 'support/work_packages/work_package_field'
require 'features/work_packages/work_packages_page'
describe 'description inplace editor', js: true, selenium: true do
let(:project) { FactoryGirl.create :project_with_types, is_public: true }
let(:property_name) { :description }
let(:property_title) { 'Description' }
let(:description_text) { 'Ima description' }
let!(:work_package) {
FactoryGirl.create(
:work_package,
project: project,
description: description_text
)
}
let(:user) { FactoryGirl.create :admin }
let(:field) { WorkPackageEditorField.new wp_page, 'description' }
let(:wp_page) { Pages::SplitWorkPackage.new(work_package, project) }
before do
login_as(user)
wp_page.visit!
wp_page.ensure_page_loaded
end
context 'with permission' do
it 'allows editing description field' do
field.expect_state_text(description_text)
# Regression test #24033
# Cancelling an edition several tiems properly resets the value
field.activate!
field.set_value "My intermittent edit 1"
field.cancel_by_escape
field.activate!
field.set_value "My intermittent edit 2"
field.cancel_by_click
field.activate!
field.expect_value description_text
field.cancel_by_click
# Activate the field
field.activate!
# Pressing escape does nothing here
field.cancel_by_escape
field.expect_active!
# Cancelling through the action panel
field.cancel_by_click
field.expect_inactive!
end
end
context 'when is empty' do
let(:description_text) { '' }
it 'renders a placeholder' do
field.expect_state_text 'Click to enter description...'
field.activate!
# An empty description is also allowed
field.expect_save_button(enabled: true)
field.set_value 'A new hope ...'
field.expect_save_button(enabled: true)
field.submit_by_click
wp_page.expect_notification message: I18n.t('js.notice_successful_update')
field.expect_state_text 'A new hope ...'
end
end
context 'with no permission' do
let(:user) { FactoryGirl.create(:user, member_in_project: project, member_through_role: role) }
let(:role) { FactoryGirl.create :role, permissions: %i(view_work_packages) }
it 'does not show the field' do
expect(page).to have_no_selector('.wp-edit-field.description.-editable')
field.display_element.click
field.expect_inactive!
end
context 'when is empty' do
let(:description_text) { '' }
it 'renders a placeholder' do
field.expect_state_text ''
end
end
end
it_behaves_like 'a workpackage autocomplete field'
it_behaves_like 'a principal autocomplete field'
end

@ -69,6 +69,7 @@ module Pages
end
def ensure_page_loaded
expect_angular_frontend_initialized
expect(page).to have_selector('.work-package-details-activities-activity-contents .user',
text: work_package.journals.last.user.name,
minimum: 1,

@ -26,6 +26,7 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
module Pages
class Page
include Capybara::DSL

Loading…
Cancel
Save