Merge pull request #7565 from opf/merge/release-dev

Merge release/9.1 into dev

[ci skip]
pull/7570/head
Oliver Günther 5 years ago committed by GitHub
commit 1c5ec3db79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      app/assets/stylesheets/content/work_packages/inplace_editing/_display_fields.sass
  2. 8
      app/models/custom_field.rb
  3. 2
      app/models/mail_handler.rb
  4. 4
      config/locales/js-en.yml
  5. 36
      db/migrate/20190722082648_add_derived_estimated_hours_to_work_packages.rb
  6. 3
      frontend/src/app/components/op-context-menu/wp-context-menu/wp-static-context-menu-actions.ts
  7. 1
      frontend/src/app/components/wp-card-view/styles/wp-card-view.component.sass
  8. 11
      frontend/src/app/components/wp-card-view/wp-card-view.component.ts
  9. 8
      frontend/src/app/modules/fields/display/field-types/wp-display-duration-field.module.ts
  10. 3
      frontend/src/app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service.ts
  11. 34
      spec/features/work_packages/bulk/move_work_package_spec.rb
  12. 4
      spec/features/work_packages/table/context_menu_spec.rb
  13. 18
      spec/fixtures/mail_handler/work_package_with_list_custom_field.eml
  14. 18
      spec/fixtures/mail_handler/work_package_with_text_custom_field.eml
  15. 48
      spec/models/mail_handler_spec.rb
  16. 53
      spec/support/work_packages/work_package_cards.rb

@ -36,13 +36,24 @@
&.-placeholder &.-placeholder
font-style: italic font-style: italic
span.-derived &.split-time-field
white-space: nowrap
.-actual-value,
.-derived-value
display: inline-block
width: 48%
.-actual-value
text-align: right
padding-right: 0.5rem
.-derived-value
@include varprop(color, gray-dark) @include varprop(color, gray-dark)
font-style: italic font-style: italic
font-weight: bold font-weight: bold
span.-with-actual &:not(.-with-actual-value)
margin-left: 0.5em margin-left: 48%
.wp-edit-field--text, .wp-edit-field--text,
wp-edit-field wp-edit-field

@ -139,6 +139,14 @@ class CustomField < ActiveRecord::Base
end end
end end
def value_of(value)
if list?
custom_options.where(value: value).pluck(:id).first
else
CustomValue.new(custom_field: self, value: value).valid? && value
end
end
## ##
# Returns possible values for this custom field. # Returns possible values for this custom field.
# Options may be a customizable, or options suitable for ActiveRecord#read_attribute. # Options may be a customizable, or options suitable for ActiveRecord#read_attribute.

@ -346,7 +346,7 @@ class MailHandler < ActionMailer::Base
def custom_field_values_from_keywords(customized) def custom_field_values_from_keywords(customized)
"#{customized.class.name}CustomField".constantize.all.inject({}) do |h, v| "#{customized.class.name}CustomField".constantize.all.inject({}) do |h, v|
if value = get_keyword(v.name, override: true) if value = get_keyword(v.name, override: true)
h[v.id.to_s] = value h[v.id.to_s] = v.value_of value
end end
h h
end end

@ -52,6 +52,7 @@ en:
button_back_to_list_view: "Back to list view" button_back_to_list_view: "Back to list view"
button_cancel: "Cancel" button_cancel: "Cancel"
button_close: "Close" button_close: "Close"
button_change_project: "Change project"
button_check_all: "Check all" button_check_all: "Check all"
button_configure-form: "Configure form" button_configure-form: "Configure form"
button_confirm: "Confirm" button_confirm: "Confirm"
@ -69,7 +70,6 @@ en:
button_show_view: "Fullscreen view" button_show_view: "Fullscreen view"
button_log_time: "Log time" button_log_time: "Log time"
button_more: "More" button_more: "More"
button_move: "Move"
button_open_details: "Open details view" button_open_details: "Open details view"
button_close_details: "Close details view" button_close_details: "Close details view"
button_open_fullscreen: "Open fullscreen view" button_open_fullscreen: "Open fullscreen view"
@ -646,7 +646,7 @@ en:
image: "Image" image: "Image"
work_packages: work_packages:
bulk_actions: bulk_actions:
move: 'Bulk move' move: 'Bulk change of project'
edit: 'Bulk edit' edit: 'Bulk edit'
copy: 'Bulk copy' copy: 'Bulk copy'
delete: 'Bulk delete' delete: 'Bulk delete'

@ -22,6 +22,12 @@ class AddDerivedEstimatedHoursToWorkPackages < ActiveRecord::Migration[5.2]
migrate_to_derived_estimated_hours! migrate_to_derived_estimated_hours!
end end
end end
change.down do
WorkPackage.transaction do
rollback_from_derived_estimated_hours!
end
end
end end
end end
@ -34,7 +40,6 @@ class AddDerivedEstimatedHoursToWorkPackages < ActiveRecord::Migration[5.2]
# only touches the derived_estimated_hours column. # only touches the derived_estimated_hours column.
def migrate_to_derived_estimated_hours! def migrate_to_derived_estimated_hours!
last_id = Journal.order(id: :desc).limit(1).pluck(:id).first || 0 last_id = Journal.order(id: :desc).limit(1).pluck(:id).first || 0
wp_journals = "work_package_journals"
work_packages = WorkPackageWithRelations.with_children.where("estimated_hours > ?", 0) work_packages = WorkPackageWithRelations.with_children.where("estimated_hours > ?", 0)
work_packages.update_all("derived_estimated_hours = estimated_hours, estimated_hours = NULL") work_packages.update_all("derived_estimated_hours = estimated_hours, estimated_hours = NULL")
@ -45,7 +50,22 @@ class AddDerivedEstimatedHoursToWorkPackages < ActiveRecord::Migration[5.2]
create_customizable_journals last_id: last_id create_customizable_journals last_id: last_id
create_attachable_journals last_id: last_id create_attachable_journals last_id: last_id
work_packages.each(&:touch) # invalidate cache touch_work_packages work_packages # to invalidate cache
end
def rollback_from_derived_estimated_hours!
last_id = Journal.order(id: :desc).limit(1).pluck(:id).first || 0
work_packages = WorkPackageWithRelations.with_children.where("derived_estimated_hours > ?", 0)
work_packages.update_all("estimated_hours = derived_estimated_hours, derived_estimated_hours = NULL")
work_packages = WorkPackageWithRelations.with_children.where("estimated_hours > ?", 0)
create_journals_for work_packages, notes: rollback_notes
create_work_package_journals last_id: last_id
create_customizable_journals last_id: last_id
create_attachable_journals last_id: last_id
touch_work_packages work_packages # to invalidate cache
end end
## ##
@ -76,6 +96,10 @@ class AddDerivedEstimatedHoursToWorkPackages < ActiveRecord::Migration[5.2]
"_'Estimated hours' changed to 'Derived estimated hours'_" "_'Estimated hours' changed to 'Derived estimated hours'_"
end end
def rollback_notes
"_'Derived estimated hours' rolled back to 'Estimated hours'_"
end
## ##
# Creates work package journals for the move of estimated_hours to derived_estimated_hours. # Creates work package journals for the move of estimated_hours to derived_estimated_hours.
# #
@ -164,4 +188,12 @@ class AddDerivedEstimatedHoursToWorkPackages < ActiveRecord::Migration[5.2]
WHERE #{journals}.id > #{last_id} -- make sure to only create entries for the newly created journals WHERE #{journals}.id > #{last_id} -- make sure to only create entries for the newly created journals
") ")
end end
def touch_work_packages(work_packages)
where = work_packages.arel.where_sql
WorkPackage.connection.execute("
UPDATE work_packages SET updated_at = NOW(), lock_version = lock_version + 1 #{where}
")
end
end end

@ -5,7 +5,8 @@ export const PERMITTED_CONTEXT_MENU_ACTIONS = [
resource: 'workPackage' resource: 'workPackage'
}, },
{ {
key: 'move', key: 'change_project',
icon: 'icon-move',
link: 'move', link: 'move',
resource: 'workPackage' resource: 'workPackage'
}, },

@ -13,6 +13,7 @@
box-shadow: 1px 1px 3px 0px lightgrey box-shadow: 1px 1px 3px 0px lightgrey
background: var(--body-background) background: var(--body-background)
font-size: var(--card-font-size) font-size: var(--card-font-size)
max-width: 400px
&:hover &:hover
box-shadow: 0px 0px 10px lightgrey box-shadow: 0px 0px 10px lightgrey

@ -3,11 +3,14 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, EventEmitter, ElementRef,
EventEmitter,
Inject, Inject,
Injector, Injector,
Input, Input,
OnInit, Output, ViewChild OnInit,
Output,
ViewChild
} from "@angular/core"; } from "@angular/core";
import {QueryResource} from 'core-app/modules/hal/resources/query-resource'; import {QueryResource} from 'core-app/modules/hal/resources/query-resource';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
@ -25,8 +28,6 @@ import {CardHighlightingMode} from "core-components/wp-fast-table/builders/highl
import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service"; import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service";
import {StateService} from "@uirouter/core"; import {StateService} from "@uirouter/core";
import {States} from "core-components/states.service"; import {States} from "core-components/states.service";
import {DragAndDropService} from "core-app/modules/common/drag-and-drop/drag-and-drop.service";
import {DragAndDropHelpers} from "core-app/modules/common/drag-and-drop/drag-and-drop.helpers";
import {WorkPackageViewOrderService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-order.service"; import {WorkPackageViewOrderService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-order.service";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {filter, withLatestFrom} from 'rxjs/operators'; import {filter, withLatestFrom} from 'rxjs/operators';
@ -35,7 +36,7 @@ import {WorkPackageViewSelectionService} from "core-app/modules/work_packages/ro
import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handler/card-view-handler-registry"; import {CardViewHandlerRegistry} from "core-components/wp-card-view/event-handler/card-view-handler-registry";
import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service"; import {WorkPackageCardViewService} from "core-components/wp-card-view/services/wp-card-view.service";
import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service"; import {WorkPackageCardDragAndDropService} from "core-components/wp-card-view/services/wp-card-drag-and-drop.service";
import {uiStateLinkClass, checkedClassName} from "core-components/wp-fast-table/builders/ui-state-link-builder"; import {checkedClassName, uiStateLinkClass} from "core-components/wp-fast-table/builders/ui-state-link-builder";
export type CardViewOrientation = 'horizontal'|'vertical'; export type CardViewOrientation = 'horizontal'|'vertical';

@ -65,6 +65,7 @@ export class DurationDisplayField extends DisplayField {
return; return;
} }
element.classList.add('split-time-field');
let value = this.value; let value = this.value;
let actual:number = (value && this.timezoneService.toHours(value)) || 0; let actual:number = (value && this.timezoneService.toHours(value)) || 0;
@ -83,6 +84,7 @@ export class DurationDisplayField extends DisplayField {
span.textContent = displayText; span.textContent = displayText;
span.title = this.valueString; span.title = this.valueString;
span.classList.add('-actual-value')
element.appendChild(span); element.appendChild(span);
} }
@ -91,12 +93,12 @@ export class DurationDisplayField extends DisplayField {
const span = document.createElement('span'); const span = document.createElement('span');
span.setAttribute('title', this.texts.empty); span.setAttribute('title', this.texts.empty);
span.textContent = "(" + (actualPresent ? "+" : "") + displayText + ")"; span.textContent = '(' + (actualPresent ? '+' : '') + displayText + ')';
span.title = `${this.derivedValueString} ${this.derivedText}`; span.title = `${this.derivedValueString} ${this.derivedText}`;
span.classList.add("-derived"); span.classList.add('-derived-value');
if (actualPresent) { if (actualPresent) {
span.classList.add("-with-actual"); span.classList.add('-with-actual-value');
} }
element.appendChild(span); element.appendChild(span);

@ -1,7 +1,7 @@
import {input} from 'reactivestates'; import {input} from 'reactivestates';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space"; import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service'; import {WorkPackageCacheService} from 'core-components/work-packages/work-package-cache.service';
import {Injectable, Injector, OnDestroy} from '@angular/core'; import {Injectable, OnDestroy} from '@angular/core';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {States} from 'core-components/states.service'; import {States} from 'core-components/states.service';
import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service"; import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service";
@ -15,7 +15,6 @@ export interface WorkPackageViewSelectionState {
activeRowIndex:number | null; activeRowIndex:number | null;
} }
@Injectable() @Injectable()
export class WorkPackageViewSelectionService implements OnDestroy { export class WorkPackageViewSelectionService implements OnDestroy {

@ -74,7 +74,7 @@ describe 'Moving a work package through Rails view', js: true do
expect(child_wp.project_id).to eq(project.id) expect(child_wp.project_id).to eq(project.id)
context_menu.open_for work_package context_menu.open_for work_package
context_menu.choose 'Move' context_menu.choose 'Change project'
# On work packages move page # On work packages move page
expect(page).to have_selector('#new_project_id') expect(page).to have_selector('#new_project_id')
@ -117,7 +117,7 @@ describe 'Moving a work package through Rails view', js: true do
it 'does not allow to move' do it 'does not allow to move' do
context_menu.open_for work_package context_menu.open_for work_package
context_menu.expect_no_options 'Move' context_menu.expect_no_options 'Change project'
end end
end end
end end
@ -134,7 +134,7 @@ describe 'Moving a work package through Rails view', js: true do
it 'does allow to move' do it 'does allow to move' do
context_menu.open_for work_package, false context_menu.open_for work_package, false
context_menu.expect_options ['Bulk move'] context_menu.expect_options ['Bulk change of project']
end end
end end
@ -143,7 +143,33 @@ describe 'Moving a work package through Rails view', js: true do
it 'does not allow to move' do it 'does not allow to move' do
context_menu.open_for work_package, false context_menu.open_for work_package, false
context_menu.expect_no_options ['Bulk move'] context_menu.expect_no_options ['Bulk change of project']
end
end
end
describe 'accessing the bulk move from the card view' do
before do
display_representation.switch_to_card_layout
loading_indicator_saveguard
find('body').send_keys [:control, 'a']
end
context 'with permissions' do
let(:current_user) { mover }
it 'does allow to move' do
context_menu.open_for work_package, false
context_menu.expect_options ['Bulk change of project']
end
end
context 'without permission' do
let(:current_user) { dev }
it 'does not allow to move' do
context_menu.open_for work_package, false
context_menu.expect_no_options ['Bulk change of project']
end end
end end
end end

@ -47,7 +47,7 @@ describe 'Work package table context menu', js: true do
# Open Move # Open Move
goto_context_menu list_view goto_context_menu list_view
menu.choose('Move') menu.choose('Change project')
expect(page).to have_selector('h2', text: I18n.t(:button_move)) expect(page).to have_selector('h2', text: I18n.t(:button_move))
expect(page).to have_selector('a.issue', text: "##{work_package.id}") expect(page).to have_selector('a.issue', text: "##{work_package.id}")
@ -100,7 +100,7 @@ describe 'Work package table context menu', js: true do
menu.open_for(work_package, list_view) menu.open_for(work_package, list_view)
menu.expect_options ['Open details view', 'Open fullscreen view', menu.expect_options ['Open details view', 'Open fullscreen view',
'Bulk edit', 'Bulk copy', 'Bulk move', 'Bulk delete'] 'Bulk edit', 'Bulk copy', 'Bulk change of project', 'Bulk delete']
end end
end end
end end

@ -0,0 +1,18 @@
Return-Path: <h.wurst@openproject.com>
Received: from osiris ([127.0.0.1])
by OSIRIS
with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
From: "Hans Wurst" <h.wurst@openproject.com>
To: <notifications@openproject.com>
Subject: OnlineStore - new Bug #42: Work package with list custom field
Date: Sun, 22 Jun 2008 12:28:07 +0200
MIME-Version: 1.0
Content-Type: text/plain;
format=flowed;
charset="iso-8859-1";
reply-type=original
Content-Transfer-Encoding: 7bit
This is a ticket submitted with a list custom field.
Letters:B

@ -0,0 +1,18 @@
Return-Path: <h.wurst@openproject.com>
Received: from osiris ([127.0.0.1])
by OSIRIS
with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
From: "Hans Wurst" <h.wurst@openproject.com>
To: <notifications@openproject.com>
Subject: OnlineStore - new Bug #42: Work package with text custom field
Date: Sun, 22 Jun 2008 12:28:07 +0200
MIME-Version: 1.0
Content-Type: text/plain;
format=flowed;
charset="iso-8859-1";
reply-type=original
Content-Transfer-Encoding: 7bit
This is a ticket submitted with a text custom field.
Notes:some text

@ -112,6 +112,54 @@ describe MailHandler, type: :model do
expect(work_package.attachments.length).to eq 2 expect(work_package.attachments.length).to eq 2
end end
end end
context 'with a custom field' do
let(:work_package) { FactoryBot.create :work_package, project: project }
let(:type) { FactoryBot.create :type }
before do
type.custom_fields << custom_field
type.save!
allow_any_instance_of(WorkPackage).to receive(:available_custom_fields).and_return([custom_field])
expect(WorkPackage).to receive(:find_by).with(id: 42).and_return(work_package)
expect(User).to receive(:find_by_mail).with("h.wurst@openproject.com").and_return(mail_user)
end
context 'of type text' do
let(:custom_field) { FactoryBot.create :text_wp_custom_field, name: "Notes" }
before do
submit_email 'work_package_with_text_custom_field.eml', issue: { project: project.identifier }
work_package.reload
end
it "sets the value" do
value = work_package.custom_values.where(custom_field_id: custom_field.id).pluck(:value).first
expect(value).to eq "some text" # as given in .eml fixture
end
end
context 'of type list' do
let(:custom_field) { FactoryBot.create :list_wp_custom_field, name: "Letters", possible_values: %w(A B C) }
before do
submit_email 'work_package_with_list_custom_field.eml', issue: { project: project.identifier }
work_package.reload
end
it "sets the value" do
option = CustomOption.where(custom_field_id: custom_field.id, value: "B").first # as given in .eml fixture
value = work_package.custom_values.where(custom_field_id: custom_field.id).pluck(:value).first
expect(value).to eq option.id.to_s
end
end
end
end end
describe '#category' do describe '#category' do

@ -0,0 +1,53 @@
#-- 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'
class WorkPackageCards
include Capybara::DSL
include RSpec::Matchers
attr_reader :project
def initialize(project = nil)
@project = project
end
def open_full_screen_by_doubleclick(work_package)
loading_indicator_saveguard
page.driver.browser.action.double_click(card(work_package).native).perform
Pages::FullWorkPackage.new(work_package, project)
end
def select_work_package(work_package)
card(work_package).click
end
def card(work_package)
page.find(".wp-card-#{work_package.id}")
end
end
Loading…
Cancel
Save