Merge pull request #8071 from opf/bim/feature/create_bcf

Create work packages on IFC model page

[ci skip]
pull/8102/head
Oliver Günther 5 years ago committed by GitHub
commit 254eddb354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/assets/stylesheets/layout/work_packages/_details_view.sass
  2. 4
      app/assets/stylesheets/layout/work_packages/_mobile.sass
  3. 4
      app/assets/stylesheets/layout/work_packages/_print.sass
  4. 4
      app/assets/stylesheets/layout/work_packages/_table.sass
  5. 11
      frontend/src/app/components/modals/editor/macro-wp-button-modal/wp-button-macro.modal.ts
  6. 1
      frontend/src/app/components/op-context-menu/handlers/op-types-context-menu.directive.ts
  7. 29
      frontend/src/app/components/wp-buttons/wp-create-button/wp-create-button.component.ts
  8. 4
      frontend/src/app/components/wp-buttons/wp-create-button/wp-create-button.html
  9. 5
      frontend/src/app/components/wp-copy/wp-copy.controller.ts
  10. 73
      frontend/src/app/components/wp-new/wp-create.component.ts
  11. 7
      frontend/src/app/components/wp-new/wp-create.service.ts
  12. 9
      frontend/src/app/components/wp-new/wp-new-full-view.component.ts
  13. 11
      frontend/src/app/components/wp-new/wp-new-split-view.component.ts
  14. 2
      frontend/src/app/components/wp-table/embedded/wp-embedded-base.component.ts
  15. 0
      frontend/src/app/modules/ifc_models/bcf/list-container/bcf-list-container.component.html
  16. 4
      frontend/src/app/modules/ifc_models/bcf/list-container/bcf-list-container.component.ts
  17. 24
      frontend/src/app/modules/ifc_models/bcf/new-split/bcf-new-split.component.html
  18. 35
      frontend/src/app/modules/ifc_models/bcf/new-split/bcf-new-split.component.ts
  19. 8
      frontend/src/app/modules/ifc_models/openproject-ifc-models.module.ts
  20. 28
      frontend/src/app/modules/ifc_models/openproject-ifc-models.routes.ts
  21. 33
      frontend/src/app/modules/ifc_models/pages/viewer/ifc-viewer-page.component.ts
  22. 1
      frontend/src/app/modules/time_entries/form/form.component.ts
  23. 8
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  24. 42
      frontend/src/app/modules/work_packages/routing/split-view-routes.template.ts
  25. 19
      frontend/src/app/modules/work_packages/routing/work-packages-routes.ts
  26. 3
      frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.component.ts
  27. 2
      frontend/src/app/modules/work_packages/routing/wp-full-view/wp-full-view.html
  28. 9
      frontend/src/app/modules/work_packages/routing/wp-split-view/wp-split-view.component.ts
  29. 27
      frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts
  30. 3
      frontend/src/app/modules/work_packages/routing/wp-view-page/wp-view-page.component.ts
  31. 0
      modules/bim/spec/features/bcf/api_authorization_spec.rb
  32. 124
      modules/bim/spec/features/bcf/create_spec.rb
  33. 6
      modules/bim/spec/features/bcf/export_spec.rb
  34. 67
      modules/bim/spec/support/pages/bcf/create_split.rb
  35. 1
      modules/bim/spec/support/pages/ifc_models/index.rb
  36. 13
      modules/bim/spec/support/pages/ifc_models/show_default.rb
  37. 1
      modules/meeting/lib/open_project/meeting/patches/textile_converter_patch.rb
  38. 2
      spec/features/work_packages/new/attributes_from_filter_spec.rb
  39. 2
      spec/features/work_packages/new/work_package_default_description_spec.rb
  40. 2
      spec/features/work_packages/table/inline_create/parallel_creation_spec.rb
  41. 2
      spec/features/work_packages/table/queries/me_filter_spec.rb
  42. 13
      spec/support/pages/work_packages/abstract_work_package.rb
  43. 76
      spec/support/pages/work_packages/concerns/work_package_by_button_creator.rb
  44. 33
      spec/support/pages/work_packages/work_packages_table.rb

@ -28,8 +28,8 @@
// Right part of the split view
// Visible only in the details ui-view
body.router--work-packages-partitioned-split-view,
body.router--work-packages-split-view-new
body.router--work-packages-partitioned-split-view-details,
body.router--work-packages-partitioned-split-view-new
.work-packages-partitioned-page--content-right
overflow-x: hidden

@ -137,11 +137,11 @@
border-top: none
.router--work-packages-partitioned-split-view,
.router--work-packages-split-view-new
.router--work-packages-partitioned-split-view-new
.work-packages-partitioned-page--content-right
overflow-x: auto
.router--work-packages-list-view
.router--work-packages-partitioned-split-view
// Ensure the WP cards can span the complete width on mobile
#content-wrapper
padding: 15px 0 !important

@ -5,7 +5,7 @@
//
@media print
// -------------------- Work Package views --------------------
.router--work-packages-list-view,
.router--work-packages-partitioned-split-view,
.router--work-packages-full-view,
.router--work-packages-full-create
#wrapper
@ -61,7 +61,7 @@
// -------------------- Sepcial Rules for FF --------------------
html.-browser-firefox
.router--work-packages-list-view,
.router--work-packages-partitioned-split-view,
.router--work-packages-full-view,
.router--work-packages-full-create
#main

@ -26,7 +26,7 @@
// See docs/COPYRIGHT.rdoc for more details.
//++
.router--work-packages-list-view,
.router--work-packages-partitioned-split-view,
.router--work-packages-full-view,
.router--work-packages-full-create
.in_modal &
@ -34,7 +34,7 @@
top: 12px
.router--work-packages-list-view:not(.router--work-packages-full-create)
.router--work-packages-partitioned-split-view:not(.router--work-packages-full-create)
@include extended-content--bottom
@include extended-content--right

@ -28,10 +28,17 @@
import {OpModalComponent} from "core-components/op-modals/op-modal.component";
import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service";
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Inject, ViewChild} from "@angular/core";
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
ViewChild
} from "@angular/core";
import {OpModalLocalsMap} from "core-components/op-modals/op-modal.types";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {TypeResource} from "core-app/modules/hal/resources/type-resource";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-package-dm.service";

@ -33,7 +33,6 @@ import {Directive, ElementRef, Input, Inject} from "@angular/core";
import {LinkHandling} from "core-app/modules/common/link-handling/link-handling";
import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive";
import {TypeResource} from 'core-app/modules/hal/resources/type-resource';
import {TypeDmService} from "core-app/modules/hal/dm-services/type-dm.service";
import {Highlighting} from 'core-app/components/wp-fast-table/builders/highlighting/highlighting.functions';
import {BrowserDetector} from "core-app/modules/common/browser/browser-detector.service";
import {WorkPackageCreateService} from 'core-components/wp-new/wp-create.service';

@ -26,12 +26,13 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {StateService} from '@uirouter/core';
import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core';
import {StateService, TransitionService} from '@uirouter/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit} from '@angular/core';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service";
import {componentDestroyed} from "ng2-rx-componentdestroyed";
import {Observable} from "rxjs";
@Component({
selector: 'wp-create-button',
@ -40,11 +41,13 @@ import {componentDestroyed} from "ng2-rx-componentdestroyed";
})
export class WorkPackageCreateButtonComponent implements OnInit, OnDestroy {
@Input('allowed') allowedWhen:string[];
@Input('stateName') stateName:string;
@Input('stateName$') stateName$:Observable<string>;
allowed:boolean;
disabled:boolean
projectIdentifier:string|null;
types:any;
transitionUnregisterFn:Function;
text = {
createWithDropdown: this.I18n.t('js.work_packages.create.button'),
@ -55,7 +58,9 @@ export class WorkPackageCreateButtonComponent implements OnInit, OnDestroy {
constructor(readonly $state:StateService,
readonly currentProject:CurrentProjectService,
readonly authorisationService:AuthorisationService,
readonly I18n:I18nService) {
readonly transition:TransitionService,
readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef) {
}
ngOnInit() {
@ -72,18 +77,20 @@ export class WorkPackageCreateButtonComponent implements OnInit, OnDestroy {
let [module, permission] = combined.split('.');
return this.authorisationService.can(module, permission);
});
this.updateDisabledState();
});
}
ngOnDestroy():void {
// Nothing to do
this.transitionUnregisterFn = this.transition.onSuccess({}, this.updateDisabledState.bind(this));
}
createWorkPackage() {
this.$state.go(this.stateName, {projectPath: this.projectIdentifier});
ngOnDestroy():void {
this.transitionUnregisterFn();
}
isDisabled() {
return !this.allowed || this.$state.includes('**.new');
private updateDisabledState() {
this.disabled = !this.allowed || this.$state.includes('**.new');
this.cdRef.detectChanges();
}
}

@ -1,10 +1,10 @@
<div class="wp-create-button">
<button class="button -alt-highlight add-work-package"
[disabled]="isDisabled()"
[disabled]="disabled"
[attr.aria-label]="text.explanation"
opTypesCreateDropdown
[projectIdentifier]="projectIdentifier"
[stateName]="stateName"
[stateName]="stateName$ | async"
[dropdownActive]="allowed">
<op-icon icon-classes="button--icon icon-add"></op-icon>
<span class="button--text"

@ -28,17 +28,16 @@
import {take} from 'rxjs/operators';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {WorkPackageCreateController} from 'core-components/wp-new/wp-create.controller';
import {WorkPackageCreateComponent} from 'core-components/wp-new/wp-create.component';
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
import {HalResourceEditingService} from "core-app/modules/fields/edit/services/hal-resource-editing.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {ChangeDetectionStrategy} from "@angular/core";
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
export class WorkPackageCopyController extends WorkPackageCreateController {
export class WorkPackageCopyController extends WorkPackageCreateComponent {
private __initialized_at:Number;
private copiedWorkPackageId:string;

@ -26,7 +26,7 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {ChangeDetectorRef, Injectable, Injector, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ChangeDetectorRef, Injector, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {StateService, Transition} from '@uirouter/core';
import {PathHelperService} from 'core-app/modules/common/path-helper/path-helper.service';
import {componentDestroyed, untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
@ -34,24 +34,20 @@ import {States} from '../states.service';
import {WorkPackageResource} from 'core-app/modules/hal/resources/work-package-resource';
import {RootResource} from 'core-app/modules/hal/resources/root-resource';
import {WorkPackageCacheService} from '../work-packages/work-package-cache.service';
import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service";
import {WorkPackageCreateService} from './wp-create.service';
import {takeUntil} from 'rxjs/operators';
import {RootDmService} from 'core-app/modules/hal/dm-services/root-dm.service';
import {OpTitleService} from 'core-components/html/op-title.service';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {CurrentUserService} from "core-app/components/user/current-user.service";
import {WorkPackageViewFiltersService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-filters.service";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {WorkPackageViewFocusService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service";
import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {EditFormComponent} from "core-app/modules/fields/edit/edit-form/edit-form.component";
import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service";
@Injectable()
export class WorkPackageCreateController implements OnInit, OnDestroy {
export abstract class WorkPackageCreateComponent implements OnInit, OnDestroy {
public successState:string;
public cancelState:string = this.$state.current.data.baseRoute;
public newWorkPackage:WorkPackageResource;
public parentWorkPackage:WorkPackageResource;
public change:WorkPackageChangeset;
@ -86,6 +82,36 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
public ngOnInit() {
this.closeEditFormWhenNewWorkPackageSaved();
this.showForm();
}
public ngOnDestroy() {
// Nothing to do
}
public switchToFullscreen() {
this.$state.go('work-packages.new', this.$state.params);
}
public onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }) {
let {savedResource, isInitial} = params;
// Shouldn't this always be true in create controller?
// Close all edit fields when saving
if (isInitial && this.editForm && this.editForm.editMode) {
this.editForm.stop();
}
if (this.successState) {
this.$state.go(this.successState, {workPackageId: savedResource.id})
.then(() => {
this.wpViewFocus.updateFocus(savedResource.id!);
this.notificationService.showSave(savedResource, isInitial);
});
}
}
protected showForm() {
this
.createdWorkPackage()
.then((changeset:WorkPackageChangeset) => {
@ -96,8 +122,9 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
this.setTitle();
if (this.stateParams['parent_id']) {
this.newWorkPackage.parent =
{href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path};
this.newWorkPackage.parent = {
href: this.pathHelper.api.v3.work_packages.id(this.stateParams['parent_id']).path
};
}
// Load the parent simply to display the type name :-/
@ -128,39 +155,13 @@ export class WorkPackageCreateController implements OnInit, OnDestroy {
});
}
public ngOnDestroy() {
// Nothing to do
}
public switchToFullscreen() {
this.$state.go('work-packages.new', this.$state.params);
}
public onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }) {
let {savedResource, isInitial} = params;
// Shouldn't this always be true in create controller?
// Close all edit fields when saving
if (isInitial && this.editForm && this.editForm.editMode) {
this.editForm.stop();
}
if (this.successState) {
this.$state.go(this.successState, {workPackageId: savedResource.id})
.then(() => {
this.wpViewFocus.updateFocus(savedResource.id!);
this.notificationService.showSave(savedResource, isInitial);
});
}
}
protected setTitle() {
this.titleService.setFirstPart(this.I18n.t('js.work_packages.create.title'));
}
public cancelAndBackToList() {
this.wpCreate.cancelCreation();
this.$state.go('work-packages.partitioned.list', this.$state.params);
this.$state.go(this.cancelState, this.$state.params);
}
protected createdWorkPackage() {

@ -46,6 +46,7 @@ import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-packag
import {FormResource} from "core-app/modules/hal/resources/form-resource";
import {HalEventsService} from "core-app/modules/hal/services/hal-events.service";
import {ResourceChangeset} from "core-app/modules/fields/changeset/resource-changeset";
import {AuthorisationService} from "core-app/modules/common/model-auth/model-auth.service";
export const newWorkPackageHref = '/api/v3/work_packages/new';
@ -60,10 +61,11 @@ export class WorkPackageCreateService implements OnDestroy {
protected hooks:HookService,
protected wpCacheService:WorkPackageCacheService,
protected halResourceService:HalResourceService,
protected readonly querySpace:IsolatedQuerySpace,
protected querySpace:IsolatedQuerySpace,
protected authorisationService:AuthorisationService,
protected halEditing:HalResourceEditingService,
protected workPackageDmService:WorkPackageDmService,
protected readonly halEvents:HalEventsService) {
protected halEvents:HalEventsService) {
this.halEditing
.comittedChanges
@ -179,6 +181,7 @@ export class WorkPackageCreateService implements OnDestroy {
}
return changePromise.then((change:WorkPackageChangeset) => {
this.authorisationService.initModelAuth('work_package', change.pristineResource);
this.halEditing.updateValue(newWorkPackageHref, change);
this.wpCacheService.updateWorkPackage(change.pristineResource);

@ -26,14 +26,15 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {WorkPackageCreateController} from 'core-components/wp-new/wp-create.controller';
import {Component} from '@angular/core';
import {WorkPackageCreateComponent} from 'core-components/wp-new/wp-create.component';
import {ChangeDetectionStrategy, Component} from '@angular/core';
@Component({
selector: 'wp-new-full-view',
host: { 'class': 'work-packages-page--ui-view' },
templateUrl: './wp-new-full-view.html'
templateUrl: './wp-new-full-view.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WorkPackageNewFullViewComponent extends WorkPackageCreateController {
export class WorkPackageNewFullViewComponent extends WorkPackageCreateComponent {
public successState:string = 'work-packages.show';
}

@ -26,13 +26,14 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {WorkPackageCreateController} from 'core-components/wp-new/wp-create.controller';
import {Component} from '@angular/core';
import {WorkPackageCreateComponent} from 'core-components/wp-new/wp-create.component';
import {ChangeDetectionStrategy, Component} from '@angular/core';
@Component({
selector: 'wp-new-split-view',
templateUrl: './wp-new-split-view.html'
templateUrl: './wp-new-split-view.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkPackageNewSplitViewComponent extends WorkPackageCreateController {
public successState:string = 'work-packages.partitioned.list.details';
export class WorkPackageNewSplitViewComponent extends WorkPackageCreateComponent {
public successState:string = this.$state.current.data.baseRoute + '.details';
}

@ -50,7 +50,7 @@ export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewB
}
ngOnDestroy():void {
super.ngOnInit();
super.ngOnDestroy();
}
ngOnChanges(changes:SimpleChanges) {

@ -14,13 +14,13 @@ import {WorkPackageViewHandlerToken} from "core-app/modules/work_packages/routin
import {BcfCardViewHandlerRegistry} from "core-app/modules/ifc_models/ifc-base-view/event-handler/bcf-card-view-handler-registry";
@Component({
templateUrl: './bcf-container.component.html',
templateUrl: './bcf-list-container.component.html',
providers: [
{ provide: WorkPackageViewHandlerToken, useValue: BcfCardViewHandlerRegistry }
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BCFContainerComponent implements OnInit, OnDestroy {
export class BcfListContainerComponent implements OnInit, OnDestroy {
@InjectField() public queryParamListener:QueryParamListenerService;
@InjectField() public wpListService:WorkPackagesListService;
@InjectField() public urlParamsHelper:UrlParamsHelperService;

@ -0,0 +1,24 @@
<div
class="work-packages--details work-packages--new"
*ngIf="newWorkPackage"
>
<edit-form [resource]="newWorkPackage"
[skippedFields]="['status', 'type']"
[inEditMode]="true"
(onSaved)="onSaved($event)">
<div class="work-packages--details-content -create-mode">
<div class="work-packages--new-details-header">
<wp-type-status [workPackage]="newWorkPackage"></wp-type-status>
</div>
<wp-single-view [workPackage]="newWorkPackage"
[showProject]="copying">
</wp-single-view>
</div>
<div class="work-packages--details-toolbar-container">
<wp-edit-actions-bar
(onCancel)="cancelAndBackToList()">
</wp-edit-actions-bar>
</div>
</edit-form>
</div>

@ -26,31 +26,24 @@
// See docs/COPYRIGHT.rdoc for more details.
// ++
import {Component, Injector, OnInit} from '@angular/core';
import {WorkPackageSingleViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-package-single-view.base";
import {StateService} from "@uirouter/core";
import {WorkPackageCreateComponent} from 'core-components/wp-new/wp-create.component';
import {Component} from '@angular/core';
import {WorkPackageResource} from "core-app/modules/hal/resources/work-package-resource";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {IFCViewerService} from "core-app/modules/ifc_models/ifc-viewer/ifc-viewer.service";
@Component({
selector: 'bcf-single-view',
templateUrl: '/app/modules/work_packages/routing/wp-split-view/wp-split-view.html'
selector: 'bcf-new-split',
templateUrl: './bcf-new-split.component.html'
})
export class BcfSingleViewComponent extends WorkPackageSingleViewBase implements OnInit {
export class BCFNewSplitComponent extends WorkPackageCreateComponent {
public successState:string = '^.details';
public cancelState:string = '^';
constructor(public injector:Injector,
readonly $state:StateService) {
super(injector, $state.params['workPackageId']);
}
ngOnInit():void {
this.observeWorkPackage();
}
@InjectField()
readonly viewer:IFCViewerService;
public close() {
this.$state.go('^.^');
public onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }) {
super.onSaved(params);
}
public switchToFullscreen() {
this.$state.go(this.keepTab.currentShowState, this.$state.params);
}
}

@ -32,14 +32,14 @@ import {OpenprojectCommonModule} from "core-app/modules/common/openproject-commo
import {IFCViewerComponent} from './ifc-viewer/ifc-viewer.component';
import {IFC_ROUTES} from "core-app/modules/ifc_models/openproject-ifc-models.routes";
import {IFCViewerPageComponent} from "core-app/modules/ifc_models/pages/viewer/ifc-viewer-page.component";
import {BCFContainerComponent} from "core-app/modules/ifc_models/bcf/container/bcf-container.component";
import {EmptyComponent} from "core-app/modules/ifc_models/empty/empty-component";
import {BimViewToggleButtonComponent} from "core-app/modules/ifc_models/toolbar/view-toggle/bim-view-toggle-button.component";
import {BimViewToggleDropdownDirective} from "core-app/modules/ifc_models/toolbar/view-toggle/bim-view-toggle-dropdown.directive";
import {BimManageIfcModelsButtonComponent} from "core-app/modules/ifc_models/toolbar/manage-ifc-models-button/bim-manage-ifc-models-button.component";
import {IFCViewerService} from "core-app/modules/ifc_models/ifc-viewer/ifc-viewer.service";
import {BcfSingleViewComponent} from "core-app/modules/ifc_models/bcf/single-view/bcf-single-view.component";
import {OpenprojectFieldsModule} from "core-app/modules/fields/openproject-fields.module";
import {BCFNewSplitComponent} from "core-app/modules/ifc_models/bcf/new-split/bcf-new-split.component";
import {BcfListContainerComponent} from "core-app/modules/ifc_models/bcf/list-container/bcf-list-container.component";
@NgModule({
imports: [
@ -59,14 +59,14 @@ import {OpenprojectFieldsModule} from "core-app/modules/fields/openproject-field
// Regions of pages
EmptyComponent,
BCFContainerComponent,
BcfListContainerComponent,
// Toolbar
BimManageIfcModelsButtonComponent,
BimViewToggleButtonComponent,
BimViewToggleDropdownDirective,
BcfSingleViewComponent,
BCFNewSplitComponent,
IFCViewerComponent
],
exports: [],

@ -27,12 +27,12 @@
// ++
import {Ng2StateDeclaration} from '@uirouter/angular';
import {IFCViewerPageComponent} from "core-app/modules/ifc_models/pages/viewer/ifc-viewer-page.component";
import {BCFContainerComponent} from "core-app/modules/ifc_models/bcf/container/bcf-container.component";
import {IFCViewerComponent} from "core-app/modules/ifc_models/ifc-viewer/ifc-viewer.component";
import {WorkPackagesBaseComponent} from "core-app/modules/work_packages/routing/wp-base/wp--base.component";
import {EmptyComponent} from "core-app/modules/ifc_models/empty/empty-component";
import {BcfSingleViewComponent} from "core-app/modules/ifc_models/bcf/single-view/bcf-single-view.component";
import {makeSplitViewRoutes} from "core-app/modules/work_packages/routing/split-view-routes.template";
import {BcfListContainerComponent} from "core-app/modules/ifc_models/bcf/list-container/bcf-list-container.component";
import {WorkPackageSplitViewComponent} from "core-app/modules/work_packages/routing/wp-split-view/wp-split-view.component";
export const IFC_ROUTES:Ng2StateDeclaration[] = [
@ -59,50 +59,54 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [
name: 'bim.partitioned.list',
url: '/list',
data: {
newRoute: 'bim.partitioned.list.new',
partition: '-left-only'
},
reloadOnSearch: false,
views: {
'content-left': { component: BCFContainerComponent }
'content-left': {component: BcfListContainerComponent}
}
},
{
name: 'bim.partitioned.split',
url: '/split',
data: {
partition: '-split'
partition: '-split',
newRoute: 'bim.partitioned.split.new',
bodyClasses: 'router--work-packages-partitioned-split-view'
},
reloadOnSearch: false,
views: {
'content-left': { component: IFCViewerComponent },
'content-right': { component: BCFContainerComponent }
'content-left': {component: IFCViewerComponent},
'content-right': {component: BcfListContainerComponent}
}
},
{
name: 'bim.partitioned.model',
url: '/model',
data: {
partition: '-left-only'
partition: '-left-only',
newRoute: 'bim.partitioned.split.new',
},
reloadOnSearch: false,
views: {
// Retarget and by that override the grandparent views
// https://ui-router.github.io/guide/views#relative-parent-state{
'content-right@^': { component: EmptyComponent },
'content-left': { component: IFCViewerComponent }
'content-right@^': {component: EmptyComponent},
'content-left': {component: IFCViewerComponent}
}
},
// BCF single view for list
...makeSplitViewRoutes(
'bim.partitioned.list',
undefined,
BcfSingleViewComponent
WorkPackageSplitViewComponent
),
// BCF single view for split
...makeSplitViewRoutes(
'bim.partitioned.split',
undefined,
BcfSingleViewComponent
)
WorkPackageSplitViewComponent
),
];

@ -1,5 +1,4 @@
import {ChangeDetectionStrategy, Component, Injector} from "@angular/core";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {GonService} from "core-app/modules/common/gon/gon.service";
import {
PartitionedQuerySpacePageComponent,
@ -20,6 +19,9 @@ import {IfcModelsDataService} from "core-app/modules/ifc_models/pages/viewer/ifc
import {QueryParamListenerService} from "core-components/wp-query/query-param-listener.service";
import {QueryResource} from "core-app/modules/hal/resources/query-resource";
import {BimManageIfcModelsButtonComponent} from "core-app/modules/ifc_models/toolbar/manage-ifc-models-button/bim-manage-ifc-models-button.component";
import {WorkPackageCreateButtonComponent} from "core-components/wp-buttons/wp-create-button/wp-create-button.component";
import {StateService, TransitionService} from "@uirouter/core";
import {BehaviorSubject, Subject} from "rxjs";
@Component({
templateUrl: '/app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html',
@ -41,7 +43,17 @@ export class IFCViewerPageComponent extends PartitionedQuerySpacePageComponent {
areYouSure: this.I18n.t('js.text_are_you_sure')
};
newRoute$ = new BehaviorSubject<string>(this.state.current.data.newRoute);
transitionUnsubscribeFn:Function;
toolbarButtonComponents:ToolbarButtonComponentDefinition[] = [
{
component: WorkPackageCreateButtonComponent,
inputs: {
stateName$: this.newRoute$,
allowed: ['work_packages.createWorkPackage', 'work_package.copy']
}
},
{
component: BcfImportButtonComponent,
show: () => this.ifcData.allowed('manage_bcf')
@ -69,7 +81,10 @@ export class IFCViewerPageComponent extends PartitionedQuerySpacePageComponent {
];
constructor(readonly ifcData:IfcModelsDataService,
readonly state:StateService,
readonly bimView:BimViewService,
readonly transition:TransitionService,
readonly gon:GonService,
readonly injector:Injector) {
super(injector);
}
@ -83,6 +98,17 @@ export class IFCViewerPageComponent extends PartitionedQuerySpacePageComponent {
.subscribe((view) => {
this.filterAllowed = view !== bimViewerViewIdentifier;
});
// Keep the new route up to date depending on where we move to
this.transitionUnsubscribeFn = this.transition.onSuccess({}, () => {
this.newRoute$.next(this.state.current.data.newRoute);
});
}
ngOnDestroy():void {
super.ngOnDestroy();
}
/**
@ -105,9 +131,4 @@ export class IFCViewerPageComponent extends PartitionedQuerySpacePageComponent {
// For now, disable any editing
this.titleEditingEnabled = false;
}
/** We do not have a mapping for html title in this module yet */
protected shouldUpdateHtmlTitle():boolean {
return false;
}
}

@ -64,6 +64,7 @@ export class TimeEntryFormComponent implements OnInit, OnDestroy {
public signalModifiedEntry($event:{savedResource:HalResource, isInital:boolean}) {
this.modifiedEntry.emit($event as {savedResource:TimeEntryResource, isInital:boolean});
}
public save() {
return this.editForm.save();
}

@ -163,6 +163,7 @@ import {HalResource} from "core-app/modules/hal/resources/hal-resource";
import {WorkPackageChangeset} from "core-components/wp-edit/work-package-changeset";
import {WorkPackageSingleCardComponent} from "core-components/wp-card-view/wp-single-card/wp-single-card.component";
import { TimeEntryChangeset } from 'core-app/components/time-entries/time-entry-changeset';
import {WorkPackageCreateService} from "core-components/wp-new/wp-create.service";
import {WorkPackageListViewComponent} from "core-app/modules/work_packages/routing/wp-list-view/wp-list-view.component";
import {PartitionedQuerySpacePageComponent} from "core-app/modules/work_packages/routing/partitioned-query-space-page/partitioned-query-space-page.component";
import {WorkPackageViewPageComponent} from "core-app/modules/work_packages/routing/wp-view-page/wp-view-page.component";
@ -503,6 +504,13 @@ import {WorkPackageSettingsButtonComponent} from "core-components/wp-buttons/wp-
// Modals
WpTableConfigurationModalComponent,
WpTableConfigurationFiltersTab,
// Needed so that e.g. IFC can access it.
WorkPackageCreateButtonComponent,
WorkPackageTypeStatusComponent,
WorkPackageEditActionsBarComponent,
WorkPackageSingleViewComponent,
WorkPackageSplitViewComponent
]
})
export class OpenprojectWorkPackagesModule {

@ -30,17 +30,8 @@ import {WorkPackageOverviewTabComponent} from 'core-components/wp-single-view-ta
import {WorkPackageActivityTabComponent} from 'core-components/wp-single-view-tabs/activity-panel/activity-tab.component';
import {WorkPackageRelationsTabComponent} from 'core-components/wp-single-view-tabs/relations-tab/relations-tab.component';
import {WorkPackageWatchersTabComponent} from 'core-components/wp-single-view-tabs/watchers-tab/watchers-tab.component';
import {WorkPackageNewFullViewComponent} from 'core-components/wp-new/wp-new-full-view.component';
import {WorkPackageCopyFullViewComponent} from 'core-components/wp-copy/wp-copy-full-view.component';
import {WorkPackageNewSplitViewComponent} from 'core-components/wp-new/wp-new-split-view.component';
import {WorkPackageCopySplitViewComponent} from 'core-components/wp-copy/wp-copy-split-view.component';
import {WorkPackagesFullViewComponent} from "core-app/modules/work_packages/routing/wp-full-view/wp-full-view.component";
import {WorkPackageSplitViewComponent} from "core-app/modules/work_packages/routing/wp-split-view/wp-split-view.component";
import {Ng2StateDeclaration} from "@uirouter/angular";
import {WorkPackagesBaseComponent} from "core-app/modules/work_packages/routing/wp-base/wp--base.component";
import {WorkPackageListViewComponent} from "core-app/modules/work_packages/routing/wp-list-view/wp-list-view.component";
import {WorkPackageViewPageComponent} from "core-app/modules/work_packages/routing/wp-view-page/wp-view-page.component";
import {WorkPackageSingleViewComponent} from "core-components/work-packages/wp-single-view/wp-single-view.component";
import {ComponentType} from "@angular/cdk/overlay";
/**
@ -60,11 +51,12 @@ import {ComponentType} from "@angular/cdk/overlay";
* otherwise AOT will not be able to look them up. This might result in missing routes.
*
* @param baseRoute The base route to mount under
* @param component The split view component to mount
* @param showComponent The split view component to mount
*/
export function makeSplitViewRoutes(baseRoute:string,
menuItemClass:string|undefined,
component:ComponentType<any>):Ng2StateDeclaration[] {
showComponent:ComponentType<any>,
newComponent:ComponentType<any> = WorkPackageNewSplitViewComponent):Ng2StateDeclaration[] {
return [
{
name: baseRoute + '.details',
@ -72,14 +64,17 @@ export function makeSplitViewRoutes(baseRoute:string,
redirectTo: baseRoute + '.details.overview',
reloadOnSearch: false,
data: {
bodyClasses: 'router--work-packages-partitioned-split-view',
bodyClasses: 'router--work-packages-partitioned-split-view-details',
menuItem: menuItemClass,
partition: '-split'
// Remember the base route so we can route back to it anywhere
baseRoute: baseRoute,
partition: '-split',
newRoute: baseRoute + '.new',
},
views: {
// Retarget and by that override the grandparent views
// https://ui-router.github.io/guide/views#relative-parent-state
'content-right@^.^': {component: component}
'content-right@^.^': {component: showComponent}
}
},
{
@ -118,5 +113,24 @@ export function makeSplitViewRoutes(baseRoute:string,
parent: baseRoute + '.details'
}
},
// Split create route
{
name: baseRoute + '.new',
url: '/create_new?{type:[0-9]+}',
reloadOnSearch: false,
data: {
partition: '-split',
allowMovingInEditMode: true,
bodyClasses: 'router--work-packages-partitioned-split-view-new',
// Remember the base route so we can route back to it anywhere
baseRoute: baseRoute,
parent: baseRoute
},
views: {
// Retarget and by that override the grandparent views
// https://ui-router.github.io/guide/views#relative-parent-state
'content-right@^.^': {component: newComponent}
}
},
];
}

@ -31,7 +31,6 @@ import {WorkPackageRelationsTabComponent} from 'core-components/wp-single-view-t
import {WorkPackageWatchersTabComponent} from 'core-components/wp-single-view-tabs/watchers-tab/watchers-tab.component';
import {WorkPackageNewFullViewComponent} from 'core-components/wp-new/wp-new-full-view.component';
import {WorkPackageCopyFullViewComponent} from 'core-components/wp-copy/wp-copy-full-view.component';
import {WorkPackageNewSplitViewComponent} from 'core-components/wp-new/wp-new-split-view.component';
import {WorkPackageCopySplitViewComponent} from 'core-components/wp-copy/wp-copy-split-view.component';
import {WorkPackagesFullViewComponent} from "core-app/modules/work_packages/routing/wp-full-view/wp-full-view.component";
import {WorkPackageSplitViewComponent} from "core-app/modules/work_packages/routing/wp-split-view/wp-split-view.component";
@ -92,6 +91,7 @@ export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [
component: WorkPackagesFullViewComponent,
data: {
bodyClasses: 'router--work-packages-full-view',
newRoute: 'work-packages.new',
menuItem: menuItemClass
}
},
@ -148,26 +148,11 @@ export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [
'content-left': { component: WorkPackageListViewComponent }
},
data: {
bodyClasses: 'router--work-packages-list-view',
bodyClasses: 'router--work-packages-partitioned-split-view',
menuItem: menuItemClass,
partition: '-left-only'
}
},
{
name: 'work-packages.partitioned.list.new',
url: '/create_new?type&parent_id',
views: {
'content-right@^.^': { component: WorkPackageNewSplitViewComponent }
},
reloadOnSearch: false,
data: {
allowMovingInEditMode: true,
bodyClasses: 'router--work-packages-split-view-new',
menuItem: menuItemClass,
parent: 'work-packages.partitioned.list',
partition: '-split'
},
},
{
name: 'work-packages.partitioned.list.copy',
url: '/details/{copiedFromWorkPackageId:[0-9]+}/copy',

@ -34,6 +34,7 @@ import {Component, Injector, OnInit} from '@angular/core';
import {WorkPackageViewSelectionService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-selection.service';
import {WorkPackageSingleViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-package-single-view.base";
import {BackRoutingService} from "core-app/modules/common/back-routing/back-routing.service";
import {of} from "rxjs";
@Component({
templateUrl: './wp-full-view.html',
@ -59,6 +60,8 @@ export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase imp
public actionsAvailable:any;
public triggerMoreMenuAction:Function;
stateName$ = of('work-packages.new');
constructor(public injector:Injector,
public wpTableSelection:WorkPackageViewSelectionService,
public backRoutingService:BackRoutingService,

@ -26,7 +26,7 @@
<li class="toolbar-item hidden-for-mobile">
<wp-create-button
[allowed]="['work_package.addChild', 'work_package.copy']"
stateName="work-packages.new">
[stateName$]="stateName$">
</wp-create-button>
</li>
<li class="toolbar-item" *ngIf="displayWatchButton">

@ -49,6 +49,9 @@ import {WorkPackageNotificationService} from "core-app/modules/work_packages/not
})
export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase implements OnInit {
/** Reference to the base route e.g., work-packages.partitioned.list or bim.partitioned.split */
private baseRoute:string = this.$state.current.data.baseRoute;
constructor(public injector:Injector,
public states:States,
public firstRoute:FirstRouteService,
@ -67,7 +70,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp
if (!focusedWP) {
// Focus on the work package if we're the first route
const isFirstRoute = this.firstRoute.name === 'work-packages.partitioned.list.details.overview';
const isFirstRoute = this.firstRoute.name === `${this.baseRoute}.details.overview`;
const isSameID = this.firstRoute.params && wpId === this.firstRoute.params.workPackageI;
this.wpTableFocus.updateFocus(wpId, (isFirstRoute && isSameID));
} else {
@ -84,7 +87,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp
)
.subscribe(newId => {
const idSame = wpId.toString() === newId.toString();
if (!idSame && this.$state.includes('work-packages.partitioned.list.details')) {
if (!idSame && this.$state.includes(`${this.baseRoute}.details.overview`)) {
this.$state.go(
(this.$state.current.name as string),
{workPackageId: newId, focus: false}
@ -95,7 +98,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp
public close() {
this.$state.go('work-packages.partitioned.list', this.$state.params);
this.$state.go(this.baseRoute, this.$state.params);
}
public switchToFullscreen() {

@ -31,7 +31,7 @@ import {StateService, TransitionService} from '@uirouter/core';
import {AuthorisationService} from 'core-app/modules/common/model-auth/model-auth.service';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
import {filter, map, withLatestFrom} from 'rxjs/operators';
import {filter, map, withLatestFrom, take} from 'rxjs/operators';
import {LoadingIndicatorService} from "core-app/modules/common/loading-indicator/loading-indicator.service";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WorkPackageStaticQueriesService} from 'core-components/wp-query-select/wp-static-queries.service';
@ -89,6 +89,11 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy {
@InjectField() deviceService:DeviceService;
@InjectField() currentProject:CurrentProjectService;
/** Determine when query is initially loaded */
queryLoaded = false;
/** Remember explicitly when this component was destroyed */
destroyed = false;
constructor(public injector:Injector) {
}
@ -99,10 +104,13 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy {
// Listen for refresh changes
this.setupRefreshObserver();
// Mark tableInformationLoaded when initially loading done
this.setupQueryLoadedListener();
}
ngOnDestroy():void {
// Nothing to do
this.destroyed = true;
}
private setupQueryObservers() {
@ -217,4 +225,19 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy {
return false;
}
protected setupQueryLoadedListener() {
this
.querySpace
.initialized
.values$()
.pipe(
take(1),
filter(() => !this.destroyed)
)
.subscribe(() => {
this.queryLoaded = true;
this.cdRef.detectChanges();
});
}
}

@ -46,6 +46,7 @@ import {BcfImportButtonComponent} from "core-app/modules/bcf/bcf-buttons/bcf-imp
import {BcfExportButtonComponent} from "core-app/modules/bcf/bcf-buttons/bcf-export-button.component";
import {ZenModeButtonComponent} from "core-components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component";
import {WorkPackageSettingsButtonComponent} from "core-components/wp-buttons/wp-settings-button/wp-settings-button.component";
import {Observable, of} from "rxjs";
@Component({
selector: 'wp-view-page',
@ -66,7 +67,7 @@ export class WorkPackageViewPageComponent extends PartitionedQuerySpacePageCompo
{
component: WorkPackageCreateButtonComponent,
inputs: {
stateName: "work-packages.partitioned.list.new",
stateName$: of("work-packages.partitioned.list.new"),
allowed: ['work_packages.createWorkPackage']
}
},

@ -0,0 +1,124 @@
require 'spec_helper'
require_relative '../../support/pages/ifc_models/show_default'
describe 'Create BCF', type: :feature, js: true, with_mail: false do
let(:project) do
FactoryBot.create(:project, types: [type, type_with_cf], work_package_custom_fields: [integer_cf])
end
let(:index_page) { Pages::IfcModels::ShowDefault.new(project) }
let(:permissions) { %i[view_ifc_models manage_ifc_models add_work_packages view_work_packages] }
let!(:status) { FactoryBot.create(:default_status) }
let!(:priority) { FactoryBot.create :priority, is_default: true }
let(:user) do
FactoryBot.create :user,
member_in_project: project,
member_with_permissions: permissions
end
let!(:model) do
FactoryBot.create(:ifc_model_converted,
project: project,
uploader: user)
end
let(:type) { FactoryBot.create(:type) }
let(:type_with_cf) do
FactoryBot.create(:type, custom_fields: [integer_cf])
end
let(:integer_cf) do
FactoryBot.create(:int_wp_custom_field)
end
shared_examples 'bcf details creation' do
it 'can create a new bcf work package' do
create_page = index_page.create_wp_by_button(type)
create_page.view_route = view_route
create_page.expect_current_path
create_page.subject_field.set(subject)
# switch the type
type_field = create_page.edit_field(:type)
type_field.activate!
type_field.set_value type_with_cf.name
cf_field = create_page.edit_field(:"customField#{integer_cf.id}")
cf_field.set_value(815)
create_page.save!
index_page.expect_and_dismiss_notification(
message: 'Successful creation. Click here to open this work package in fullscreen view.'
)
work_package = WorkPackage.last
split_page = ::Pages::SplitWorkPackage.new(work_package, project)
split_page.ensure_page_loaded
split_page.expect_subject
split_page.close
split_page.expect_closed
expect(page).to have_current_path /bcf\/#{Regexp.escape(view_route)}$/, ignore_query: true
end
end
before do
login_as(user)
end
context 'with all permissions' do
context 'on the split page' do
let(:view_route) { 'split' }
before do
index_page.visit!
end
it_behaves_like 'bcf details creation'
end
context 'on the split page switching to list' do
let(:view_route) { 'list' }
before do
index_page.visit!
index_page.switch_view 'List'
expect(page).to have_current_path /\/bcf\/list$/, ignore_query: true
end
it_behaves_like 'bcf details creation'
end
context 'starting on the list page' do
let(:view_route) { 'list' }
before do
visit bcf_project_frontend_path(project, "list")
expect(page).to have_current_path /\/bcf\/list$/, ignore_query: true
end
it_behaves_like 'bcf details creation'
end
context 'starting on the details page of an existing work package' do
let(:work_package) { FactoryBot.create :work_package, project: project }
let(:view_route) { 'split' }
before do
visit bcf_project_frontend_path(project, "split/details/#{work_package.id}")
expect(page).to have_current_path /\/bcf\/split\/details/, ignore_query: true
end
it_behaves_like 'bcf details creation'
end
end
context 'without create work package permission' do
let(:permissions) { %i[view_ifc_models manage_ifc_models view_work_packages] }
it 'has the create button disabled' do
index_page.visit!
index_page.expect_wp_create_button_disabled
end
end
end

@ -26,12 +26,12 @@
# See docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require_relative '../support/pages/ifc_models/show_default'
require_relative '../../support/pages/ifc_models/show_default'
describe 'work package export', type: :feature, js: true do
describe 'bcf export', type: :feature, js: true do
let(:status) { FactoryBot.create(:status, name: 'New', is_default: true) }
let(:closed_status) { FactoryBot.create(:closed_status, name: 'Closed') }
let(:project) { FactoryBot.create :project, enabled_module_names: [:bim, :work_package_tracking] }
let(:project) { FactoryBot.create :project, enabled_module_names: %i[bim work_package_tracking] }
let!(:open_work_package) { FactoryBot.create(:work_package, project: project, subject: 'Open WP', status: status) }
let!(:closed_work_package) { FactoryBot.create(:work_package, project: project, subject: 'Closed WP', status: closed_status) }

@ -0,0 +1,67 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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 'support/pages/page'
require 'support/pages/work_packages/abstract_work_package_create'
module Pages
module BCF
class CreateSplit < ::Pages::AbstractWorkPackageCreate
attr_accessor :project,
:model_id,
:type_id,
:view_route
def initialize(project:, model_id: nil, type_id: nil)
super(project: project)
self.model_id = model_id
self.type_id = type_id
self.view_route = :split
end
def path
bcf_project_frontend_path(project, "#{view_route}/create_new")
end
def expect_current_path
expect(page)
.to have_current_path(path, ignore_query: true)
end
def container
find("wp-new-split-view")
end
private
def default?
model_id.nil?
end
end
end
end

@ -27,6 +27,7 @@
#++
require 'support/pages/page'
require 'support/pages/work_packages/concerns/work_package_by_button_creator'
module Pages
module IfcModels

@ -27,10 +27,13 @@
#++
require 'support/pages/page'
require_relative '../bcf/create_split'
module Pages
module IfcModels
class ShowDefault < ::Pages::Page
class ShowDefault < ::Pages::WorkPackageCards
include ::Pages::WorkPackages::Concerns::WorkPackageByButtonCreator
attr_accessor :project
def initialize(project)
@ -97,6 +100,14 @@ module Pages
['Manage models']
end
def create_page_class_instance(type)
create_page_class.new(project: project, type_id: type.id)
end
def create_page_class
Pages::BCF::CreateSplit
end
def element_visible?(visible, selector, name)
expect(page).to (visible ? have_selector(selector, text: name) : have_no_selector(selector, text: name))
end

@ -1,4 +1,5 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH

@ -92,7 +92,7 @@ RSpec.feature 'Work package create uses attributes from filters', js: true, sele
end
it 'allows to save with a single value (Regression test #27833)' do
split_page = wp_table.create_wp_split_screen type_task
split_page = wp_table.create_wp_by_button type_task
subject = split_page.edit_field(:subject)
subject.expect_active!

@ -83,7 +83,7 @@ describe 'new work package', js: true, with_mail: false do
it 'shows the template after selection of project and type' do
wp_table.visit!
wp_table.create_wp_split_screen type_feature
wp_table.create_wp_by_button type_feature
wp_page.expect_fully_loaded

@ -36,7 +36,7 @@ describe 'Parallel work package creation spec', js: true do
subject_field.set_value subject
# Create in split screen
split = wp_table.create_wp_split_screen type
split = wp_table.create_wp_by_button type
description_field = TextEditorField.new split, 'description'
description_field.expect_active!
description_field.set_value description

@ -106,7 +106,7 @@ describe 'filter me value', js: true do
filters.expect_filter_by('Assignee', 'is', 'me')
# Expect new work packages receive assignee
split_screen = wp_table.create_wp_split_screen wp_user.type
split_screen = wp_table.create_wp_by_button wp_user.type
subject = split_screen.edit_field :subject
subject.set_value 'foobar'
subject.submit_by_enter

@ -30,14 +30,11 @@ require 'support/pages/page'
module Pages
class AbstractWorkPackage < Page
attr_reader :project, :work_package, :type_field_selector, :subject_field_selector
attr_reader :project, :work_package
def initialize(work_package, project = nil)
@work_package = work_package
@project = project
@type_field_selector = '.inline-edit--container.type'
@subject_field_selector = '.inline-edit--container.subject'
end
def visit_tab!(tab)
@ -272,13 +269,9 @@ module Pages
find('#types-context-menu .menu-item', text: type.name.upcase, wait: 10).click
end
def select_type(type)
find(@type_field_selector + ' option', text: type.name.upcase).select_option
end
def subject_field
expect(page).to have_selector(@subject_field_selector + ' input', wait: 10)
find(@subject_field_selector + ' input')
expect(page).to have_selector('.inline-edit--container.subject input', wait: 10)
find('.inline-edit--container.subject input')
end
private

@ -0,0 +1,76 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 the OpenProject GmbH
#
# 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.
#++
module Pages
module WorkPackages
module Concerns
module WorkPackageByButtonCreator
def create_wp_by_button(type)
click_wp_create_button
find('#types-context-menu .menu-item', text: type.name.upcase, wait: 10).click
create_page_class_instance(type)
end
def click_wp_create_button
find('.add-work-package:not([disabled])', text: 'Create').click
end
def expect_wp_create_button_disabled
expect(page)
.to have_selector('.add-work-package[disabled]', text: 'Create')
end
def expect_type_available_for_create(type)
click_wp_create_button
expect(page)
.to have_selector('#types-context-menu .menu-item', text: type.name.upcase)
end
def expect_type_not_available_for_create(type)
click_wp_create_button
expect(page)
.to have_no_selector('#types-context-menu .menu-item', text: type.name.upcase)
end
private
def create_page_class_instance(_type)
create_page_class.new(project: project)
end
def create_page_class
raise NotImplementedError
end
end
end
end
end

@ -27,9 +27,12 @@
#++
require 'support/pages/page'
require_relative 'concerns/work_package_by_button_creator'
module Pages
class WorkPackagesTable < Page
include ::Pages::WorkPackages::Concerns::WorkPackageByButtonCreator
attr_reader :project
def initialize(project = nil)
@ -135,32 +138,6 @@ module Pages
expect(container).to have_selector('.wp-inline-create-row', wait: 10)
end
def create_wp_split_screen(type)
click_wp_create_button
find('#types-context-menu .menu-item', text: type.name.upcase, wait: 10).click
SplitWorkPackageCreate.new(project: project)
end
def click_wp_create_button
find('.add-work-package:not([disabled])', text: 'Create').click
end
def expect_type_available_for_create(type)
click_wp_create_button
expect(page)
.to have_selector('#types-context-menu .menu-item', text: type.name.upcase)
end
def expect_type_not_available_for_create(type)
click_wp_create_button
expect(page)
.to have_no_selector('#types-context-menu .menu-item', text: type.name.upcase)
end
def open_split_view(work_package)
split_page = SplitWorkPackage.new(work_package, project)
@ -311,5 +288,9 @@ module Pages
filter_container['id'].gsub('filter_', '')
end
end
def create_page_class
SplitWorkPackageCreate
end
end
end

Loading…
Cancel
Save