Merge remote-tracking branch 'origin/release/11.0' into dev

pull/8732/head
Oliver Günther 4 years ago
commit a2fd3078be
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 21
      app/models/journal.rb
  2. 2
      app/services/journals/create_service.rb
  3. 1
      app/views/projects/filters/_boolean.html.erb
  4. 37
      db/migrate/20200924085508_cleanup_orphaned_journal_data.rb
  5. 16
      frontend/src/app/components/wp-form-group/wp-attribute-group.component.ts
  6. 5
      frontend/src/app/components/wp-query-select/wp-static-queries.service.ts
  7. 8
      frontend/src/app/modules/global_search/input/global-search-input.component.sass
  8. 6
      frontend/src/app/modules/time_entries/shared/modal/base.modal.ts
  9. 4
      frontend/src/global_styles/content/_icon_control.sass
  10. 4
      frontend/src/global_styles/content/_list.sass
  11. 2
      frontend/src/global_styles/content/_news.sass
  12. 1
      frontend/src/global_styles/content/_projects_list.sass
  13. 2
      frontend/src/global_styles/content/_types_form_configuration.sass
  14. 2
      frontend/src/global_styles/content/_user_mention.sass
  15. 2
      frontend/src/global_styles/content/editor/_macros.sass
  16. 1
      frontend/src/global_styles/content/modules/_index.sass
  17. 4
      frontend/src/global_styles/content/modules/_meetings.scss
  18. 2
      frontend/src/global_styles/content/work_packages/_table_content.sass
  19. 2
      frontend/src/global_styles/content/work_packages/single_view/_attachments.sass
  20. 9
      frontend/src/global_styles/layout/_top_menu.sass
  21. 2
      frontend/src/global_styles/openproject/_functions.sass
  22. 2
      frontend/src/global_styles/openproject/_scm.sass
  23. 4
      frontend/src/global_styles/openproject/_variables.scss
  24. 23
      lib/plugins/acts_as_journalized/lib/acts/journalized/creation.rb
  25. 53
      lib/plugins/acts_as_journalized/lib/acts/journalized/options.rb
  26. 4
      lib/plugins/acts_as_journalized/lib/acts_as_journalized.rb
  27. 2
      spec/features/work_packages/tabs/activity_revisions_spec.rb
  28. 2
      spec/features/work_packages/tabs/activity_tab_spec.rb
  29. 46
      spec/models/work_package/work_package_acts_as_journalized_spec.rb

@ -48,21 +48,12 @@ class Journal < ApplicationRecord
has_many :attachable_journals, class_name: 'Journal::AttachableJournal', dependent: :destroy has_many :attachable_journals, class_name: 'Journal::AttachableJournal', dependent: :destroy
has_many :customizable_journals, class_name: 'Journal::CustomizableJournal', dependent: :destroy has_many :customizable_journals, class_name: 'Journal::CustomizableJournal', dependent: :destroy
before_destroy :destroy_data
# Scopes to all journals excluding the initial journal - useful for change # Scopes to all journals excluding the initial journal - useful for change
# logs like the history on issue#show # logs like the history on issue#show
scope :changing, -> { where(['version > 1']) } scope :changing, -> { where(['version > 1']) }
# TODO: check if this can be removed
# Overrides the +user=+ method created by the polymorphic +belongs_to+ user association.
# Based on the class of the object given, either the +user+ association columns or the
# +user_name+ string column is populated.
def user=(value)
case value
when ActiveRecord::Base then super(value)
else self.user = User.find_by_login(value)
end
end
# In conjunction with the included Comparable module, allows comparison of journal records # In conjunction with the included Comparable module, allows comparison of journal records
# based on their corresponding version numbers, creation timestamps and IDs. # based on their corresponding version numbers, creation timestamps and IDs.
def <=>(other) def <=>(other)
@ -120,6 +111,10 @@ class Journal < ApplicationRecord
private private
def destroy_data
data.destroy
end
def predecessor def predecessor
@predecessor ||= self.class @predecessor ||= self.class
.where(journable_type: journable_type, journable_id: journable_id) .where(journable_type: journable_type, journable_id: journable_id)
@ -127,8 +122,4 @@ class Journal < ApplicationRecord
.order("#{self.class.table_name}.version DESC") .order("#{self.class.table_name}.version DESC")
.first .first
end end
def journalized_object_type
"#{journaled_type.gsub('Journal', '')}".constantize
end
end end

@ -385,7 +385,7 @@ module Journals
end end
def journable_data_sql_addition def journable_data_sql_addition
journable.class.vestal_journals_options[:data_sql]&.call(journable) || '' journable.class.aaj_options[:data_sql]&.call(journable) || ''
end end
def text_column_names def text_column_names

@ -3,3 +3,4 @@
data-input-name="v-<%= filter.class.key %>" data-input-name="v-<%= filter.class.key %>"
data-filter-name="boolean"> data-filter-name="boolean">
</slide-toggle> </slide-toggle>
<div class="advanced-filters--filter-operator" style="visibility: hidden"></div>

@ -0,0 +1,37 @@
class CleanupOrphanedJournalData < ActiveRecord::Migration[6.0]
def up
cleanup_orphaned_journals('attachable_journals')
cleanup_orphaned_journals('customizable_journals')
cleanup_orphaned_journals('attachment_journals')
cleanup_orphaned_journals('changeset_journals')
cleanup_orphaned_journals('message_journals')
cleanup_orphaned_journals('news_journals')
cleanup_orphaned_journals('wiki_content_journals')
cleanup_orphaned_journals('work_package_journals')
end
# No down needed as this only cleans up data that should have been deleted anyway.
private
def cleanup_orphaned_journals(table)
execute <<~SQL
DELETE
FROM
#{table}
WHERE
#{table}.id IN (
SELECT
#{table}.id
FROM
#{table}
LEFT OUTER JOIN
journals
ON
journals.id = #{table}.journal_id
WHERE
journals.id IS NULL
)
SQL
end
end

@ -31,22 +31,36 @@ import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {FieldDescriptor, GroupDescriptor} from 'core-components/work-packages/wp-single-view/wp-single-view.component'; import {FieldDescriptor, GroupDescriptor} from 'core-components/work-packages/wp-single-view/wp-single-view.component';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component"; import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component";
import {UntilDestroyedMixin} from "core-app/helpers/angular/until-destroyed.mixin";
import {fromEvent} from "rxjs";
import {debounceTime} from "rxjs/operators";
@Component({ @Component({
selector: 'wp-attribute-group', selector: 'wp-attribute-group',
templateUrl: './wp-attribute-group.template.html' templateUrl: './wp-attribute-group.template.html'
}) })
export class WorkPackageFormAttributeGroupComponent implements AfterViewInit { export class WorkPackageFormAttributeGroupComponent extends UntilDestroyedMixin implements AfterViewInit {
@Input() public workPackage:WorkPackageResource; @Input() public workPackage:WorkPackageResource;
@Input() public group:GroupDescriptor; @Input() public group:GroupDescriptor;
constructor(readonly I18n:I18nService, constructor(readonly I18n:I18nService,
public wpEditForm:EditFormComponent, public wpEditForm:EditFormComponent,
protected injector:Injector) { protected injector:Injector) {
super();
} }
ngAfterViewInit() { ngAfterViewInit() {
setTimeout(() => this.fixColumns()); setTimeout(() => this.fixColumns());
// Listen to resize event and fix column start again
fromEvent(window, 'resize', { passive: true })
.pipe(
this.untilDestroyed(),
debounceTime(250)
)
.subscribe(() => {
this.fixColumns();
});
} }
public trackByName(_index:number, elem:{ name:string }) { public trackByName(_index:number, elem:{ name:string }) {

@ -77,7 +77,7 @@ export class WorkPackageStaticQueriesService {
{ {
identifier: 'gantt', identifier: 'gantt',
label: this.text.gantt, label: this.text.gantt,
query_props: `{"c":["id","type","subject","status","startDate","dueDate"],"tv":true,"tzl":"auto","tll":"{\\"left\\":\\"startDate\\",\\"right\\":\\"dueDate\\",\\"farRight\\":null}","hi":true,"g":"","t":"id:asc","t":"startDate:asc","f":[{"n":"status","o":"o","v":[]}]}` query_props: `{"c":["id","type","subject","status","startDate","dueDate"],"tv":true,"tzl":"auto","tll":"{\\"left\\":\\"startDate\\",\\"right\\":\\"dueDate\\",\\"farRight\\":null}","hi":true,"g":"","t":"startDate:asc","f":[{"n":"status","o":"o","v":[]}]}`
}, },
{ {
identifier: 'recently_created', identifier: 'recently_created',
@ -118,9 +118,10 @@ export class WorkPackageStaticQueriesService {
let queryProps = JSON.parse(this.$state.params.query_props); let queryProps = JSON.parse(this.$state.params.query_props);
delete queryProps.pp; delete queryProps.pp;
delete queryProps.pa; delete queryProps.pa;
let queryPropsString = JSON.stringify(queryProps);
const matched = _.find(this.all, item => const matched = _.find(this.all, item =>
item.query_props && item.query_props === JSON.stringify(queryProps) item.query_props && item.query_props === queryPropsString
); );
if (matched) { if (matched) {

@ -148,11 +148,8 @@ $search-input-height: 30px
.global-search--wp-content .global-search--wp-content
display: grid display: grid
grid-template-columns: auto 1fr auto grid-template-columns: 50% 1fr auto
grid-template-rows: auto auto auto 1fr auto
grid-template-areas: "project idlink status" grid-template-areas: "project idlink status"
overflow: hidden
flex-grow: 1
font-size: 0.8rem font-size: 0.8rem
padding: 5px 0 5px 0px padding: 5px 0 5px 0px
@ -162,12 +159,11 @@ $search-input-height: 30px
.global-search--wp-id .global-search--wp-id
grid-area: idlink grid-area: idlink
place-self: center place-self: right
font-style: italic font-style: italic
.global-search--wp-status .global-search--wp-status
grid-area: status grid-area: status
margin-right: 5px
overflow: hidden overflow: hidden
font-style: italic font-style: italic

@ -53,7 +53,10 @@ export abstract class TimeEntryBaseModal extends OpModalComponent {
this.editForm.save() this.editForm.save()
.then(() => this.reloadWorkPackageAndClose()) .then(() => this.reloadWorkPackageAndClose())
.catch(() => this.formInFlight = false); .catch(() => {
this.formInFlight = false;
this.cdRef.detectChanges();
});
} }
public get saveText() { public get saveText() {
@ -79,5 +82,6 @@ export abstract class TimeEntryBaseModal extends OpModalComponent {
} }
this.service.close(); this.service.close();
this.formInFlight = false; this.formInFlight = false;
this.cdRef.detectChanges();
} }
} }

@ -36,7 +36,7 @@
text-align: center text-align: center
cursor: pointer cursor: pointer
border-radius: 50% border-radius: 50%
color: $gray-dark color: var(--gray-dark)
&:hover, &.-active &:hover, &.-active
text-decoration: none text-decoration: none
@ -44,7 +44,7 @@
background: var(--button--highlight-background-color) background: var(--button--highlight-background-color)
&.-active:hover &.-active:hover
color: $gray-dark color: var(--gray-dark)
background: white background: white
.icon-control--icon .icon-control--icon

@ -42,11 +42,11 @@
.me .me
.time .time
border-bottom: 1px solid $gray-dark border-bottom: 1px solid var(--gray-dark)
.time, .time,
.description .description
color: $gray-dark color: var(--gray-dark)
.time, .time,
.description, .description,

@ -38,7 +38,7 @@
.news-author .news-author
font-size: 0.8rem font-size: 0.8rem
color: $gray-dark color: var(--gray-dark)
display: block display: block
padding: 5px 0 5px 40px padding: 5px 0 5px 40px
line-height: 1.25 line-height: 1.25

@ -47,6 +47,7 @@ form.project-filters
// visibility based on operator type // visibility based on operator type
&.hidden &.hidden
visibility: hidden visibility: hidden
height: 55px
// visibility for list value selectors // visibility for list value selectors
.multi-select .multi-select

@ -47,7 +47,7 @@
#type-form-conf-inactive-group #type-form-conf-inactive-group
background: $gray-dark background: var(--gray-dark)
.visibility-check, .visibility-check,
.delete-group, .delete-group,
.delete-attribute .delete-attribute

@ -34,7 +34,7 @@
&:before &:before
content: '@' content: '@'
color: $gray-dark color: var(--gray-dark)
span.user-mention span.user-mention
// Remove text selection cursor for group // Remove text selection cursor for group

@ -17,7 +17,7 @@ macro.macro-placeholder
.macro-value .macro-value
font-style: italic font-style: italic
color: $gray-dark color: var(--gray-dark)
// Unavailable macros // Unavailable macros
macro.macro-unavailable macro.macro-unavailable

@ -7,3 +7,4 @@
@import 'documents' @import 'documents'
@import '2fa' @import '2fa'
@import 'webhooks' @import 'webhooks'
@import 'meetings'

@ -39,8 +39,8 @@ dl.meetings {
margin-bottom: 2em; margin-bottom: 2em;
} }
dt.meeting { dd.meeting {
margin-bottom: 1.5em; margin-bottom: 2rem;
} }
dl.meetings p { dl.meetings p {

@ -117,7 +117,7 @@ html:not(.-browser-mobile)
color: var(--content-link-color) color: var(--content-link-color)
padding: 0 0 0 0.25rem padding: 0 0 0 0.25rem
&.-disabled .icon:before &.-disabled .icon:before
color: $gray-dark color: var(--gray-dark)
&:hover &:hover
text-decoration: none text-decoration: none

@ -72,7 +72,7 @@
label label
cursor: pointer cursor: pointer
color: $gray-dark color: var(--gray-dark)
font-size: 0.9rem font-size: 0.9rem
font-weight: bold font-weight: bold
line-height: 1.4 line-height: 1.4

@ -253,10 +253,11 @@ $hamburger-width: 50px
.nosidebar & .nosidebar &
display: none display: none
@media only screen and (max-width: 18.75rem) @media only screen and (max-width: 750px)
#logo #logo
max-width: 170px display: none
@media only screen and (max-width: 1210px) @media only screen and (max-width: 1000px)
#logo #logo
display: none max-width: 100px

@ -26,6 +26,8 @@
// See docs/COPYRIGHT.rdoc for more details. // See docs/COPYRIGHT.rdoc for more details.
//++ //++
@import "../vendor/foundation-apps/scss/helpers/functions"
@function rem-concat-list($list-of-pixels) @function rem-concat-list($list-of-pixels)
$result: '' $result: ''
@each $pixel-val in $list-of-pixels @each $pixel-val in $list-of-pixels

@ -92,7 +92,7 @@ li.change
&::before &::before
content: '' content: ''
color: $gray-dark color: var(--gray-dark)
table.filecontent table.filecontent
border: 1px solid #ccc border: 1px solid #ccc

@ -210,3 +210,7 @@
--work-package-details--tab-height: 40px; --work-package-details--tab-height: 40px;
--warn:#C92A2A; --warn:#C92A2A;
} }
// SCSS variables needed for foundation, but that are set in css variables now only
// which cannot be set to sass unfortunately
$secondary-color: #bfbfbf;

@ -79,29 +79,6 @@ module Acts::Journalized
end end
end end
# Class methods added to ActiveRecord::Base to facilitate the creation of new journals.
module ClassMethods
# Overrides the basal +prepare_journaled_options+ method defined in VestalVersions::Options
# to extract the <tt>:calculate</tt> option into +vestal_journals_options+.
def prepare_journaled_options(options)
result = super(options)
assign_vestal = lambda do |key, array|
return unless result[key]
vestal_journals_options[key] = if array
Array(result.delete(key)).map(&:to_s).uniq
else
result.delete(key)
end
end
assign_vestal.call(:data_sql, false)
result
end
end
module InstanceMethods module InstanceMethods
# Returns an array of column names that are journaled. # Returns an array of column names that are journaled.
def journaled_columns_names def journaled_columns_names

@ -85,55 +85,28 @@ module Acts::Journalized
# 1. Those passed directly to the +journaled+ method # 1. Those passed directly to the +journaled+ method
# 2. Those specified in an initializer +configure+ block # 2. Those specified in an initializer +configure+ block
# 3. Default values specified in +prepare_journaled_options+ # 3. Default values specified in +prepare_journaled_options+
#
# The method is overridden in feature modules that require specific options outside the
# standard +has_many+ associations.
def prepare_journaled_options(options) def prepare_journaled_options(options)
result_options = options_with_defaults(options) class_attribute :aaj_options
self.aaj_options = options_with_defaults(options)
class_attribute :vestal_journals_options
self.vestal_journals_options = result_options.dup
result_options.merge!(
extend: Array(result_options[:extend]).unshift(Versions)
)
result_options
end end
private private
def options_with_defaults(options) # Returns all of the provided options suitable for the
journal_options = split_option_hashes(options) # has_many :journals
# association created by aaj
def has_many_journals_options
aaj_options
.slice(*ActiveRecord::Associations::Builder::HasMany.send(:valid_options, {}))
end
result_options = journal_options.symbolize_keys def options_with_defaults(options)
result_options.reverse_merge!( {
class_name: Journal.name, class_name: Journal.name,
dependent: :delete_all, dependent: :destroy,
foreign_key: :journable_id, foreign_key: :journable_id,
as: :journable as: :journable
) }.merge(options.symbolize_keys)
result_options
end
# Splits an option has into three hashes:
## => [{ options prefixed with "activity_" }, { options prefixed with "event_" }, { other options }]
def split_option_hashes(options)
journal_hash = {}
options.each_pair do |k, v|
case k.to_s
when /\Aactivity_(.+)\z/
raise "Configuring activity via acts_as_journalized is no longer supported."
when /\Aevent_(.+)\z/
raise "Configuring events via acts_as_journalized is no longer supported."
else
journal_hash[k.to_sym] = v
end
end
journal_hash
end end
end end
end end

@ -73,11 +73,11 @@ module Acts
include_aaj_modules include_aaj_modules
journal_hash = prepare_journaled_options(options) prepare_journaled_options(options)
has_many :journals, -> { has_many :journals, -> {
order("#{Journal.table_name}.version ASC") order("#{Journal.table_name}.version ASC")
}, **journal_hash }, **has_many_journals_options
end end
private private

@ -8,7 +8,7 @@ describe 'Activity tab', js: true, selenium: true do
work_package.update(attributes.merge(updated_at: at)) work_package.update(attributes.merge(updated_at: at))
note_journal = work_package.journals.last note_journal = work_package.journals.last
note_journal.update(created_at: at, user: attributes[:user]) note_journal.update(created_at: at, user: user)
end end
let(:project) { FactoryBot.create :project_with_types, public: true } let(:project) { FactoryBot.create :project_with_types, public: true }

@ -8,7 +8,7 @@ describe 'Activity tab', js: true, selenium: true do
work_package.update(attributes.merge(updated_at: at)) work_package.update(attributes.merge(updated_at: at))
note_journal = work_package.journals.last note_journal = work_package.journals.last
note_journal.update(created_at: at, user: attributes[:user]) note_journal.update(created_at: at, user: user)
end end
let(:project) { FactoryBot.create :project_with_types, public: true } let(:project) { FactoryBot.create :project_with_types, public: true }

@ -458,6 +458,52 @@ describe WorkPackage, type: :model do
end end
end end
context 'on #destroy' do
let(:project) { FactoryBot.create(:project) }
let(:type) { FactoryBot.create(:type) }
let(:custom_field) do
FactoryBot.create(:int_wp_custom_field).tap do |cf|
project.work_package_custom_fields << cf
type.custom_fields << cf
end
end
let(:work_package) do
FactoryBot.create(:work_package,
project: project,
type: type,
custom_field_values: { custom_field.id => 5 },
attachments: [attachment])
end
let(:attachment) { FactoryBot.build(:attachment) }
let!(:journal) { work_package.journals.first }
let!(:customizable_journals) { journal.customizable_journals }
let!(:attachable_journals) { journal.attachable_journals }
before do
work_package.destroy
end
it 'removes the journal' do
expect(Journal.find_by(id: journal.id))
.to be_nil
end
it 'removes the journal data' do
expect(Journal::WorkPackageJournal.find_by(journal_id: journal.id))
.to be_nil
end
it 'removes the customizable journals' do
expect(Journal::CustomizableJournal.find_by(id: customizable_journals.map(&:id)))
.to be_nil
end
it 'removes the attachable journals' do
expect(Journal::AttachableJournal.find_by(id: attachable_journals.map(&:id)))
.to be_nil
end
end
describe 'Acts as journalized' do describe 'Acts as journalized' do
before(:each) do before(:each) do
@type ||= FactoryBot.create(:type_feature) @type ||= FactoryBot.create(:type_feature)

Loading…
Cancel
Save