Merge pull request #6311 from opf/feature/embedded-inline-create

[27606] Allow inline-create in embedded children tables

[ci skip]
pull/6312/head
Oliver Günther 7 years ago committed by GitHub
commit 7e9400308d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/assets/stylesheets/layout/_work_package_table_embedded.sass
  2. 2
      app/policies/work_package_policy.rb
  3. 5
      frontend/app/components/routing/wp-view-base/wp-view-base.controller.ts
  4. 36
      frontend/app/components/wp-edit-form/work-package-filter-values.ts
  5. 19
      frontend/app/components/wp-inline-create/wp-inline-create.component.ts
  6. 3
      frontend/app/components/wp-relations/wp-relation-children/wp-children-query.html
  7. 38
      frontend/app/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.template.html
  8. 10
      frontend/app/components/wp-table/embedded/wp-embedded-table.component.ts
  9. 7
      frontend/app/components/wp-table/wp-table-configuration.ts
  10. 21
      spec/features/work_packages/details/relations/hierarchy_spec.rb
  11. 13
      spec/support/components/work_packages/relations.rb
  12. 8
      spec/support/pages/work_packages_table.rb

@ -33,6 +33,7 @@
.work-package-table--container
contain: initial !important
overflow: visible
.generic-table--header,
.generic-table--sort-header

@ -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

@ -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));

@ -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<HalResource[]> {
const fieldSchema = form.schema[field];

@ -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');
}
}

@ -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 }" >
</wp-embedded-table>

@ -1,24 +1,24 @@
<div class="wp-relations-hierarchy-section">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.parentHeadline">
</h3>
</div>
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.parentHeadline">
</h3>
</div>
</div>
<wp-relation-parent [workPackage]="workPackage"></wp-relation-parent>
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.childrenHeadline">
</h3>
</div>
<wp-relation-parent [workPackage]="workPackage"></wp-relation-parent>
</div>
<div class="wp-relations-hierarchy-section wp-relations--children">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"
[textContent]="text.childrenHeadline">
</h3>
</div>
<wp-children-query
[workPackage]="workPackage"
[query]="childrenQueryProps">
</wp-children-query>
</div>
<wp-children-query
[workPackage]="workPackage"
[query]="childrenQueryProps">
</wp-children-query>
</div>

@ -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})
);

@ -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;
});
}
}

@ -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

@ -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"

@ -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)

Loading…
Cancel
Save