mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
[27606] Allow inline-create in embedded children tables
https://community.openproject.com/wp/27606
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
+20
-20
@@ -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>
|
||||
</div>
|
||||
|
||||
<wp-children-query
|
||||
[workPackage]="workPackage"
|
||||
[query]="childrenQueryProps">
|
||||
</wp-children-query>
|
||||
<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>
|
||||
</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)
|
||||
|
||||
Reference in New Issue
Block a user