From 1ffeedbd5d93c38e8a23fc8119b316ff06dcce99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 15 May 2018 11:20:09 +0200 Subject: [PATCH] [27606] Allow inline-create in embedded children tables https://community.openproject.com/wp/27606 --- .../layout/_work_package_table_embedded.sass | 1 + app/policies/work_package_policy.rb | 2 + .../wp-view-base/wp-view-base.controller.ts | 5 +++ .../work-package-filter-values.ts | 36 +++++++++++++----- .../wp-inline-create.component.ts | 19 +++++----- .../wp-children-query.html | 3 +- .../wp-relations-hierarchy.template.html | 38 +++++++++---------- .../embedded/wp-embedded-table.component.ts | 10 ++++- .../wp-table/wp-table-configuration.ts | 7 +++- .../details/relations/hierarchy_spec.rb | 21 +++++++++- .../components/work_packages/relations.rb | 13 +++++++ spec/support/pages/work_packages_table.rb | 8 ++++ 12 files changed, 119 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/layout/_work_package_table_embedded.sass b/app/assets/stylesheets/layout/_work_package_table_embedded.sass index fad7cb1263..9038b0dc37 100644 --- a/app/assets/stylesheets/layout/_work_package_table_embedded.sass +++ b/app/assets/stylesheets/layout/_work_package_table_embedded.sass @@ -33,6 +33,7 @@ .work-package-table--container contain: initial !important + overflow: visible .generic-table--header, .generic-table--sort-header diff --git a/app/policies/work_package_policy.rb b/app/policies/work_package_policy.rb index da5bce0ad0..464be260e4 100644 --- a/app/policies/work_package_policy.rb +++ b/app/policies/work_package_policy.rb @@ -102,6 +102,8 @@ class WorkPackagePolicy < BasePolicy end def type_active_in_project?(work_package) + return false unless work_package.project + @type_active_cache ||= Hash.new do |hash, project| hash[project] = project.types.pluck(:id) end diff --git a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts index 02c8d1bf79..c92a1fc52f 100644 --- a/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts +++ b/frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts @@ -40,6 +40,7 @@ import {WorkPackageTableRefreshService} from '../../wp-table/wp-table-refresh-re import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource'; import {ProjectCacheService} from 'core-components/projects/project-cache.service'; import {OpTitleService} from 'core-components/html/op-title.service'; +import {AuthorisationService} from "core-components/common/model-auth/model-auth.service"; export class WorkPackageViewController implements OnDestroy { @@ -52,6 +53,7 @@ export class WorkPackageViewController implements OnDestroy { protected wpEditing:WorkPackageEditingService = this.injector.get(WorkPackageEditingService); protected wpTableFocus:WorkPackageTableFocusService = this.injector.get(WorkPackageTableFocusService); protected projectCacheService:ProjectCacheService = this.injector.get(ProjectCacheService); + protected authorisationService:AuthorisationService = this.injector.get(AuthorisationService); // Static texts public text:any = {}; @@ -109,6 +111,9 @@ export class WorkPackageViewController implements OnDestroy { this.projectIdentifier = this.workPackage.project.identifier; }); + // Set authorisation data + this.authorisationService.initModelAuth('work_package', this.workPackage.$links); + // Push the current title this.titleService.setFirstPart(this.workPackage.subjectWithType(20)); diff --git a/frontend/app/components/wp-edit-form/work-package-filter-values.ts b/frontend/app/components/wp-edit-form/work-package-filter-values.ts index 56d37f2167..8aaef6932e 100644 --- a/frontend/app/components/wp-edit-form/work-package-filter-values.ts +++ b/frontend/app/components/wp-edit-form/work-package-filter-values.ts @@ -3,6 +3,7 @@ import {CollectionResource} from 'core-app/modules/hal/resources/collection-reso import {FormResource} from 'core-app/modules/hal/resources/form-resource'; import {WorkPackageChangeset} from './work-package-changeset'; import {QueryFilterInstanceResource} from 'core-app/modules/hal/resources/query-filter-instance-resource'; +import {all} from "@uirouter/core"; export class WorkPackageFilterValues { @@ -43,16 +44,7 @@ export class WorkPackageFilterValues { private async setAllowedValueFor(form:FormResource, field:string, value:string|HalResource) { return this.allowedValuesFor(form, field).then((allowedValues) => { - let newValue; - - if ((value as HalResource)['$href']) { - newValue = _.find(allowedValues, - (entry:any) => entry.$href === (value as HalResource).$href); - } else if (allowedValues) { - newValue = _.find(allowedValues, (entry:any) => entry === value); - } else { - newValue = value; - } + let newValue = this.findSpecialValue(value, field) || this.findAllowedValue(value, allowedValues); if (newValue) { this.changeset.setValue(field, newValue); @@ -61,6 +53,30 @@ export class WorkPackageFilterValues { }); } + /** + * Returns special values for which no allowed values exist (e.g., parent ID in embedded queries) + * @param {string | HalResource} value + * @param {string} field + */ + private findSpecialValue(value:string|HalResource, field:string):string|HalResource|undefined { + if (field === 'parent') { + return value; + } + + return undefined; + } + + private findAllowedValue(value:string|HalResource, allowedValues:HalResource[]) { + if (value instanceof HalResource && !!value.$href) { + return _.find(allowedValues, + (entry:any) => entry.$href === value.$href); + } else if (allowedValues) { + return _.find(allowedValues, (entry:any) => entry === value); + } else { + return value; + } + } + private async allowedValuesFor(form:FormResource, field:string):Promise { const fieldSchema = form.schema[field]; diff --git a/frontend/app/components/wp-inline-create/wp-inline-create.component.ts b/frontend/app/components/wp-inline-create/wp-inline-create.component.ts index f652edb671..da7bcd961e 100644 --- a/frontend/app/components/wp-inline-create/wp-inline-create.component.ts +++ b/frontend/app/components/wp-inline-create/wp-inline-create.component.ts @@ -28,7 +28,7 @@ import { Component, - ElementRef, + ElementRef, HostListener, Inject, Injector, Input, @@ -122,7 +122,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe this.timelineBuilder = new TimelineRowBuilder(this.injector, this.table); // Mirror the row height in timeline - const container = jQuery('.wp-table-timeline--body'); + const container = jQuery(this.table.timelineBody); container.addClass('-inline-create-mirror'); // Remove temporary rows on creation of new work package @@ -137,7 +137,9 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe this.addWorkPackageRow(); // Focus on the last inserted id - this.wpTableFocus.updateFocus(wp.id); + if (!this.table.configuration.isEmbedded) { + this.wpTableFocus.updateFocus(wp.id); + } } else { // Remove current row this.table.editing.stopEditing('new'); @@ -171,11 +173,6 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe evt.stopImmediatePropagation(); return false; }); - - // Additionally, cancel on escape - Mousetrap(this.$element[0]).bind('escape', () => { - this.resetRow(); - }); } public handleAddRowClick() { @@ -192,7 +189,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe const wp = this.currentWorkPackage = changeset.workPackage; // Apply filter values - const filter = new WorkPackageFilterValues(changeset, this.wpTableFilters.current); + const filter = new WorkPackageFilterValues(changeset, this.tableState.query.value!.filters); filter.applyDefaultsFromFilters().then(() => { this.wpEditing.updateValue('new', changeset); this.wpCacheService.updateWorkPackage(this.currentWorkPackage!); @@ -222,6 +219,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe /** * Reset the new work package row and refocus on the button */ + @HostListener('keydown.escape') public resetRow() { this.focus = true; this.removeWorkPackageRow(); @@ -251,6 +249,7 @@ export class WorkPackageInlineCreateComponent implements OnInit, OnChanges, OnDe } public get isAllowed():boolean { - return this.authorisationService.can('work_packages', 'createWorkPackage'); + return this.authorisationService.can('work_packages', 'createWorkPackage') || + this.authorisationService.can('work_package', 'addChild'); } } diff --git a/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html b/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html index 10b0f0af34..95cf2dd9a4 100644 --- a/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html +++ b/frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html @@ -4,9 +4,10 @@ [tableActions]="childrenTableActions" [configuration]="{ actionsColumnEnabled: true, hierarchyToggleEnabled: false, - inlineCreateEnabled: false, + inlineCreateEnabled: true, columnMenuEnabled: false, contextMenuEnabled: false, + projectIdentifier: workPackage.project.idFromLink, projectContext: false }" > diff --git a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html index 80771befba..7a73dd41fd 100644 --- a/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html +++ b/frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html @@ -1,24 +1,24 @@
-
-
-

-

-
+
+
+

+

+
- - -
-
-

-

-
+ +
+
+
+
+

+

- - - +
+ +
diff --git a/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts index 3f84581f80..51e255e014 100644 --- a/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts @@ -95,6 +95,12 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit, // Load initial query this.loadQuery(); + // Reload results on refresh requests + this.tableState.refreshRequired + .values$() + .pipe(untilComponentDestroyed(this)) + .subscribe(async () => this.refresh()); + // Reload results on changes to pagination this.tableState.ready.fireOnStateChange(this.wpTablePagination.state, 'Query loaded').values$().pipe( @@ -126,6 +132,8 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit, if (this.configuration.projectContext) { identifier = this.currentProject.identifier; + } else { + identifier = this.configuration.projectIdentifier; } return identifier || undefined; @@ -188,5 +196,5 @@ export class WorkPackageEmbeddedTableComponent implements OnInit, AfterViewInit, // TODO: remove as this should also work by angular2 only opUiComponentsModule.directive( 'wpEmbeddedTable', - downgradeComponent({ component: WorkPackageEmbeddedTableComponent }) + downgradeComponent({component: WorkPackageEmbeddedTableComponent}) ); diff --git a/frontend/app/components/wp-table/wp-table-configuration.ts b/frontend/app/components/wp-table/wp-table-configuration.ts index e2faae9810..d5d1b8d602 100644 --- a/frontend/app/components/wp-table/wp-table-configuration.ts +++ b/frontend/app/components/wp-table/wp-table-configuration.ts @@ -27,7 +27,7 @@ // ++ -export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:boolean }>; +export type WorkPackageTableConfigurationObject = Partial<{ [field in keyof WorkPackageTableConfiguration]:string|boolean }>; export class WorkPackageTableConfiguration { /** Render the table results, set to false when only wanting the table initialization */ @@ -45,6 +45,9 @@ export class WorkPackageTableConfiguration { /** Whether the query should be resolved using the current project identifier */ public projectContext:boolean = true; + /** Whether the embedded table should live within a specific project context (e.g., given by its parent) */ + public projectIdentifier:string|null = null; + /** Whether inline create is enabled*/ public inlineCreateEnabled:boolean = true; @@ -57,7 +60,7 @@ export class WorkPackageTableConfiguration { constructor(private providedConfig:WorkPackageTableConfigurationObject) { _.each(providedConfig, (value, k) => { let key = (k as keyof WorkPackageTableConfiguration); - this[key] = !!value; + this[key] = value as any; }); } } diff --git a/spec/features/work_packages/details/relations/hierarchy_spec.rb b/spec/features/work_packages/details/relations/hierarchy_spec.rb index 87f1c318d8..78e53ac18d 100644 --- a/spec/features/work_packages/details/relations/hierarchy_spec.rb +++ b/spec/features/work_packages/details/relations/hierarchy_spec.rb @@ -5,7 +5,8 @@ describe 'Work package relations tab', js: true, selenium: true do let(:user) { FactoryBot.create :admin } - let(:project) { FactoryBot.create :project } + + let(:project) { FactoryBot.create(:project) } let(:work_package) { FactoryBot.create(:work_package, project: project) } let(:work_packages_page) { ::Pages::SplitWorkPackage.new(work_package) } let(:full_wp) { ::Pages::FullWorkPackage.new(work_package) } @@ -56,6 +57,24 @@ describe 'Work package relations tab', js: true, selenium: true do relations.add_existing_child(child2) end + + describe 'inline create' do + let!(:status) { FactoryBot.create(:status, is_default: true) } + let!(:priority) { FactoryBot.create(:priority, is_default: true) } + let(:type_bug) { FactoryBot.create(:type_bug) } + let!(:project) do + FactoryBot.create(:project, types: [type_bug]) + end + + it 'can inline-create children' do + relations.inline_create_child 'my new child' + table = relations.children_table + + table.expect_work_package_subject 'my new child' + work_package.reload + expect(work_package.children.count).to eq(1) + end + end end describe 'relation group-by toggler' do diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb index 9f002b7fad..3cdb6bcf6c 100644 --- a/spec/support/components/work_packages/relations.rb +++ b/spec/support/components/work_packages/relations.rb @@ -144,6 +144,15 @@ module Components expect(page).to have_no_selector('.relation-row--parent', text: removed_text, wait: 10) end + def inline_create_child(subject_text) + container = find('.wp-relations--children') + scroll_to_and_click(container.find('.wp-inline-create-button-row .wp-inline-create--add-link')) + + subject = ::WorkPackageField.new(container, 'subject') + subject.expect_active! + subject.update subject_text + end + def add_existing_child(work_package) # Locate the create row container container = find('.wp-relations--child-form') @@ -155,6 +164,10 @@ module Components container.find('.wp-create-relation--save').click end + def children_table + ::Pages::EmbeddedWorkPackagesTable.new find('.work-packages-embedded-view--container') + end + def remove_child(work_package) page.within('.work-packages-embedded-view--container') do row = ".wp-row-#{work_package.id}-table" diff --git a/spec/support/pages/work_packages_table.rb b/spec/support/pages/work_packages_table.rb index 357830136d..d34f148e2b 100644 --- a/spec/support/pages/work_packages_table.rb +++ b/spec/support/pages/work_packages_table.rb @@ -54,6 +54,14 @@ module Pages end end + def expect_work_package_subject(subject) + within(table_container) do + expect(page).to have_selector("td.subject", + text: subject, + wait: 20) + end + end + def expect_work_package_count(n) within(table_container) do expect(page).to have_selector(".wp--row", count: n, wait: 20)