Merge pull request #8073 from opf/bim/feature/31979-Allow-filtering-WPs-within-IFC-Models-module

[31979] Allow filtering WPs within IFC Models module

[ci skip]
pull/8086/head
Oliver Günther 5 years ago committed by GitHub
commit 041dc3fbdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 74
      frontend/src/app/components/wp-query/query-param-listener.service.ts
  2. 61
      frontend/src/app/modules/ifc_models/bcf/container/bcf-container.component.ts
  3. 8
      frontend/src/app/modules/ifc_models/openproject-ifc-models.routes.ts
  4. 10
      frontend/src/app/modules/ifc_models/pages/viewer/ifc-viewer-page.component.html
  5. 11
      frontend/src/app/modules/ifc_models/pages/viewer/ifc-viewer-page.component.sass
  6. 23
      frontend/src/app/modules/ifc_models/pages/viewer/ifc-viewer-page.component.ts
  7. 17
      frontend/src/app/modules/ifc_models/view-toggle/bim-view-toggle-dropdown.directive.ts
  8. 4
      frontend/src/app/modules/ifc_models/view-toggle/bim-view-toggle.component.ts
  9. 12
      frontend/src/app/modules/ifc_models/view-toggle/bim-view.service.ts
  10. 43
      frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts
  11. 133
      modules/bim/spec/features/bim_filter_spec.rb
  12. 14
      modules/bim/spec/support/pages/ifc_models/show_default.rb

@ -0,0 +1,74 @@
// -- 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-2013 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.
// ++
import {Injectable, Injector} from '@angular/core';
import {WorkPackagesListChecksumService} from "core-components/wp-list/wp-list-checksum.service";
import {WorkPackagesListService} from "core-components/wp-list/wp-list.service";
import {TransitionService} from "@uirouter/core";
import {Subject} from "rxjs";
@Injectable()
export class QueryParamListenerService {
readonly wpListChecksumService:WorkPackagesListChecksumService = this.injector.get(WorkPackagesListChecksumService);
readonly wpListService:WorkPackagesListService = this.injector.get(WorkPackagesListService);
readonly $transitions:TransitionService = this.injector.get(TransitionService);
public observe$ = new Subject<any>();
public queryChangeListener:Function;
constructor(readonly injector:Injector) {
this.listenForQueryParamsChanged();
}
public listenForQueryParamsChanged():any {
// Listen for param changes
return this.queryChangeListener = this.$transitions.onSuccess({}, (transition):any => {
let options = transition.options();
const params = transition.params('to');
let newChecksum = this.wpListService.getCurrentQueryProps(params);
let newId:string = params.query_id ? params.query_id.toString() : null;
// Avoid performing any changes when we're going to reload
if (options.reload || (options.custom && options.custom.notify === false)) {
return true;
}
return this.wpListChecksumService
.executeIfOutdated(newId,
newChecksum,
() => {
this.observe$.next(newChecksum);
});
});
}
public removeQueryChangeListener() {
this.queryChangeListener();
}
}

@ -1,51 +1,84 @@
import {Component} from "@angular/core"; import {ChangeDetectorRef, Component, Injector, OnDestroy, OnInit} from "@angular/core";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {CurrentProjectService} from "core-components/projects/current-project.service"; import {CurrentProjectService} from "core-components/projects/current-project.service";
import {WorkPackageTableConfigurationObject} from "core-components/wp-table/wp-table-configuration"; import {WorkPackageTableConfigurationObject} from "core-components/wp-table/wp-table-configuration";
import { StateService } from '@uirouter/core'; import {StateService} from '@uirouter/core';
import {GonService} from "core-app/modules/common/gon/gon.service"; import {GonService} from "core-app/modules/common/gon/gon.service";
import {QueryParamListenerService} from "core-components/wp-query/query-param-listener.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
import {WorkPackagesListService} from "core-components/wp-list/wp-list.service";
import {UrlParamsHelperService} from "core-components/wp-query/url-params-helper";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
@Component({ @Component({
templateUrl: './bcf-container.component.html' templateUrl: './bcf-container.component.html',
providers: [
QueryParamListenerService
]
}) })
export class BCFContainerComponent { export class BCFContainerComponent implements OnInit, OnDestroy {
@InjectField() public queryParamListener:QueryParamListenerService;
@InjectField() public wpListService:WorkPackagesListService;
@InjectField() public urlParamsHelper:UrlParamsHelperService;
public queryProps:{ [key:string]:any }; public queryProps:{ [key:string]:any };
public configuration:WorkPackageTableConfigurationObject = { public configuration:WorkPackageTableConfigurationObject = {
actionsColumnEnabled: false, actionsColumnEnabled: false,
columnMenuEnabled: false, columnMenuEnabled: false,
contextMenuEnabled: false, contextMenuEnabled: false,
inlineCreateEnabled: false, inlineCreateEnabled: false,
withFilters: true, withFilters: false,
showFilterButton: false, showFilterButton: false,
isCardView: true isCardView: true
}; };
private filters:any[] = [];
constructor(readonly state:StateService, constructor(readonly state:StateService,
readonly i18n:I18nService, readonly i18n:I18nService,
readonly paths:PathHelperService, readonly paths:PathHelperService,
readonly currentProject:CurrentProjectService, readonly currentProject:CurrentProjectService,
readonly gon:GonService) { readonly gon:GonService,
readonly injector:Injector,
readonly cdRef:ChangeDetectorRef) {
}
ngOnInit():void {
this.refresh();
this.applyFilters(); this.queryParamListener
.observe$
.pipe(
untilComponentDestroyed(this)
).subscribe((queryProps) => {
this.refresh(this.urlParamsHelper.buildV3GetQueryFromJsonParams(queryProps));
});
}
ngOnDestroy():void {
this.queryParamListener.removeQueryChangeListener();
} }
private applyFilters() { private defaultQueryProps() {
// TODO: Limit to project let filters = [];
this.filters.push({ filters.push({
status: { status: {
operator: 'o', operator: 'o',
values: [] values: []
} }
}); });
this.queryProps = { return {
'columns[]': ['id', 'subject'], 'columns[]': ['id', 'subject'],
filters: JSON.stringify(this.filters), filters: JSON.stringify(filters),
sortBy: JSON.stringify([['updatedAt', 'desc']]), sortBy: JSON.stringify([['updatedAt', 'desc']]),
showHierarchies: false showHierarchies: false
}; };
} }
public refresh(queryProps:{ [key:string]:any }|undefined = undefined) {
this.wpListService.loadCurrentQueryFromParams(this.currentProject.identifier!);
this.queryProps = queryProps || this.state.params.query_props || this.defaultQueryProps();
this.cdRef.detectChanges();
}
} }

@ -36,9 +36,13 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [
{ {
name: 'bim', name: 'bim',
parent: 'root', parent: 'root',
url: '/ifc_models', url: '/ifc_models?query_props',
abstract: true, abstract: true,
component: WorkPackagesBaseComponent component: WorkPackagesBaseComponent,
params: {
// Use custom encoder/decoder that ensures validity of URL string
query_props: {type: 'opQueryString', dynamic: true}
}
}, },
{ {
name: 'bim.space', name: 'bim.space',

@ -5,6 +5,11 @@
</h2> </h2>
</div> </div>
<ul class="toolbar-items"> <ul class="toolbar-items">
<li class="toolbar-item"
*ngIf="filterAllowed">
<wp-filter-button>
</wp-filter-button>
</li>
<li class="toolbar-item"> <li class="toolbar-item">
<bim-view-toggle-button></bim-view-toggle-button> <bim-view-toggle-button></bim-view-toggle-button>
</li> </li>
@ -25,10 +30,13 @@
</div> </div>
</div> </div>
<filter-container></filter-container>
<div class="ifc-model-viewer--container" <div class="ifc-model-viewer--container"
ui-view="viewer"> ui-view="viewer">
</div> </div>
<div class="bcf-table--container" <div class="bcf-table--container loading-indicator--location"
data-indicator-name="ifc-table-container"
ui-view="list"> ui-view="list">
</div> </div>

@ -3,17 +3,17 @@
overflow: hidden overflow: hidden
display: grid display: grid
grid-template-columns: 1fr 500px grid-template-columns: 1fr 500px
grid-template-rows: 60px calc(100% - 60px) grid-template-rows: 60px auto minmax(200px, 1fr)
grid-column-gap: 10px grid-column-gap: 10px
&.-split &.-split
grid-template-areas: "header header" "viewer list" grid-template-areas: "header header" "filter filter" "viewer list"
&.-viewer &.-viewer
grid-template-areas: "header header" "viewer viewer" grid-template-areas: "header header" "filter filter" "viewer viewer"
&.-list &.-list
grid-template-areas: "header header" "list list" grid-template-areas: "header header" "filter filter" "list list"
.toolbar-container .toolbar-container
grid-area: header grid-area: header
@ -23,3 +23,6 @@
.bcf-table--container .bcf-table--container
grid-area: list grid-area: list
filter-container
grid-area: filter

@ -1,7 +1,11 @@
import {Component, Injector, HostBinding, ChangeDetectionStrategy} from "@angular/core"; import {ChangeDetectionStrategy, Component, HostBinding, Injector} from "@angular/core";
import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service";
import {GonService} from "core-app/modules/common/gon/gon.service"; import {GonService} from "core-app/modules/common/gon/gon.service";
import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-packages-view.base"; import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-packages-view.base";
import {
bimViewerViewIdentifier, BimViewService
} from "core-app/modules/ifc_models/view-toggle/bim-view.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@Component({ @Component({
templateUrl: './ifc-viewer-page.component.html', templateUrl: './ifc-viewer-page.component.html',
@ -9,6 +13,8 @@ import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-vi
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class IFCViewerPageComponent extends WorkPackagesViewBase { export class IFCViewerPageComponent extends WorkPackagesViewBase {
@InjectField() bimView:BimViewService;
text = { text = {
title: this.I18n.t('js.ifc_models.models.default'), title: this.I18n.t('js.ifc_models.models.default'),
manage: this.I18n.t('js.ifc_models.models.manage'), manage: this.I18n.t('js.ifc_models.models.manage'),
@ -25,13 +31,7 @@ export class IFCViewerPageComponent extends WorkPackagesViewBase {
@HostBinding('class') @HostBinding('class')
get gridTemplateAreas() { get gridTemplateAreas() {
if (this.$state.includes('bim.space.list')) { return '-' + this.bimView.currentViewerState();
return '-list';
} else if (this.$state.includes('bim.space.*.model')) {
return '-viewer';
} else {
return '-split';
}
} }
public get title() { public get title() {
@ -50,16 +50,19 @@ export class IFCViewerPageComponent extends WorkPackagesViewBase {
return this.gonIFC.permissions.manage; return this.gonIFC.permissions.manage;
} }
public get filterAllowed():boolean {
return this.bimView.currentViewerState() !== bimViewerViewIdentifier;
}
private get gonIFC() { private get gonIFC() {
return (this.gon.get('ifc_models') as any); return (this.gon.get('ifc_models') as any);
} }
protected set loadingIndicator(promise:Promise<unknown>) { protected set loadingIndicator(promise:Promise<unknown>) {
// TODO: do something useful this.loadingIndicatorService.indicator('ifc-table-container').promise = promise;
} }
public refresh(visibly:boolean, firstPage:boolean):Promise<unknown> { public refresh(visibly:boolean, firstPage:boolean):Promise<unknown> {
// TODO: do something useful
return this.loadingIndicator = return this.loadingIndicator =
this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier); this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier);
} }

@ -30,13 +30,14 @@ import {OPContextMenuService} from "core-components/op-context-menu/op-context-m
import {Directive, ElementRef} from "@angular/core"; import {Directive, ElementRef} from "@angular/core";
import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive"; import {OpContextMenuTrigger} from "core-components/op-context-menu/handlers/op-context-menu-trigger.directive";
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {StateService} from "@uirouter/core";
import { import {
bimListViewIdentifier, bimListViewIdentifier,
bimSplitViewIdentifier, bimSplitViewIdentifier,
bimViewerViewIdentifier bimViewerViewIdentifier,
} from "core-app/modules/ifc_models/view-toggle/bim-view-toggle.component"; BimViewService
import {StateService} from "@uirouter/core"; } from "core-app/modules/ifc_models/view-toggle/bim-view.service";
import {BimViewService} from "core-app/modules/ifc_models/view-toggle/bim-view.service"; import {WorkPackageFiltersService} from "core-components/filters/wp-filters/wp-filters.service";
@Directive({ @Directive({
selector: '[bimViewDropdown]' selector: '[bimViewDropdown]'
@ -46,7 +47,8 @@ export class BimViewToggleDropdownDirective extends OpContextMenuTrigger {
readonly opContextMenu:OPContextMenuService, readonly opContextMenu:OPContextMenuService,
readonly bimView:BimViewService, readonly bimView:BimViewService,
readonly I18n:I18nService, readonly I18n:I18nService,
readonly state:StateService) { readonly state:StateService,
readonly wpFiltersService:WorkPackageFiltersService) {
super(elementRef, opContextMenu); super(elementRef, opContextMenu);
} }
@ -73,6 +75,11 @@ export class BimViewToggleDropdownDirective extends OpContextMenuTrigger {
hidden: key === current, hidden: key === current,
linkText: this.bimView.text[key], linkText: this.bimView.text[key],
onClick: () => { onClick: () => {
// Close filter section
if (this.wpFiltersService.visible) {
this.wpFiltersService.toggleVisibility();
}
switch (key) { switch (key) {
case bimListViewIdentifier: case bimListViewIdentifier:
this.state.go('bim.space.list'); this.state.go('bim.space.list');

@ -31,10 +31,6 @@ import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {BimViewService} from "core-app/modules/ifc_models/view-toggle/bim-view.service"; import {BimViewService} from "core-app/modules/ifc_models/view-toggle/bim-view.service";
export const bimListViewIdentifier = 'list';
export const bimViewerViewIdentifier = 'viewer';
export const bimSplitViewIdentifier = 'split';
@Component({ @Component({
template: ` template: `
<ng-container *ngIf="(view$ | async) as current"> <ng-container *ngIf="(view$ | async) as current">

@ -75,14 +75,18 @@ export class BimViewService implements OnDestroy {
return this.view.getValueOr(bimSplitViewIdentifier); return this.view.getValueOr(bimSplitViewIdentifier);
} }
private detectView() { public currentViewerState():BimViewState {
if (this.state.current.name === 'bim.space.list') { if (this.state.current.name === 'bim.space.list') {
this.view.putValue(bimListViewIdentifier); return bimListViewIdentifier;
} else if (this.state.includes('bim.**.model')) { } else if (this.state.includes('bim.**.model')) {
this.view.putValue(bimViewerViewIdentifier); return bimViewerViewIdentifier;
} else { } else {
this.view.putValue(bimSplitViewIdentifier); return bimSplitViewIdentifier;
}
} }
private detectView() {
this.view.putValue(this.currentViewerState());
} }
ngOnDestroy() { ngOnDestroy() {

@ -39,7 +39,7 @@ import {wpDisplayCardRepresentation} from "core-app/modules/work_packages/routin
import {WorkPackageTableConfigurationObject} from "core-components/wp-table/wp-table-configuration"; import {WorkPackageTableConfigurationObject} from "core-components/wp-table/wp-table-configuration";
import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service"; import {HalResourceNotificationService} from "core-app/modules/hal/services/hal-resource-notification.service";
import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service";
import {scrollHeaderOnMobile} from "core-app/globals/global-listeners/top-menu-scroll"; import {QueryParamListenerService} from "core-components/wp-query/query-param-listener.service";
import {InjectField} from "core-app/helpers/angular/inject-field.decorator"; import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
@Component({ @Component({
@ -51,12 +51,14 @@ import {InjectField} from "core-app/helpers/angular/inject-field.decorator";
/** We need to provide the wpNotification service here to get correct save notifications for WP resources */ /** We need to provide the wpNotification service here to get correct save notifications for WP resources */
{provide: HalResourceNotificationService, useClass: WorkPackageNotificationService}, {provide: HalResourceNotificationService, useClass: WorkPackageNotificationService},
DragAndDropService, DragAndDropService,
CausedUpdatesService CausedUpdatesService,
QueryParamListenerService
] ]
}) })
export class WorkPackagesListComponent extends WorkPackagesViewBase implements OnDestroy { export class WorkPackagesListComponent extends WorkPackagesViewBase implements OnDestroy {
@InjectField() titleService:OpTitleService; @InjectField() titleService:OpTitleService;
@InjectField() bcfDetectorService:BcfDetectorService; @InjectField() bcfDetectorService:BcfDetectorService;
@InjectField() queryParamListener:QueryParamListenerService;
text = { text = {
'jump_to_pagination': this.I18n.t('js.work_packages.jump_marks.pagination'), 'jump_to_pagination': this.I18n.t('js.work_packages.jump_marks.pagination'),
@ -98,13 +100,23 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
super.ngOnInit(); super.ngOnInit();
this.hasQueryProps = !!this.$state.params.query_props; this.hasQueryProps = !!this.$state.params.query_props;
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
const params = transition.params('to');
this.hasQueryProps = !!params.query_props;
});
// If the query was loaded, reload invisibly // If the query was loaded, reload invisibly
const isFirstLoad = !this.querySpace.initialized.hasValue(); const isFirstLoad = !this.querySpace.initialized.hasValue();
this.refresh(isFirstLoad, isFirstLoad); this.refresh(isFirstLoad, isFirstLoad);
// Load query on URL transitions // Load query on URL transitions
this.updateQueryOnParamsChanges(); this.queryParamListener
.observe$
.pipe(
untilComponentDestroyed(this)
).subscribe(() => {
this.refresh(true, true);
});
// Mark tableInformationLoaded when initially loading done // Mark tableInformationLoaded when initially loading done
this.setupInformationLoadedListener(); this.setupInformationLoadedListener();
@ -138,6 +150,7 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
super.ngOnDestroy(); super.ngOnDestroy();
this.unRegisterTitleListener(); this.unRegisterTitleListener();
this.removeTransitionSubscription(); this.removeTransitionSubscription();
this.queryParamListener.removeQueryChangeListener();
} }
public setAnchorToNextElement() { public setAnchorToNextElement() {
@ -229,30 +242,6 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
return this.bcfDetectorService.isBcfActivated; return this.bcfDetectorService.isBcfActivated;
} }
protected updateQueryOnParamsChanges() {
// Listen for param changes
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
let options = transition.options();
const params = transition.params('to');
this.hasQueryProps = !!params.query_props;
let newChecksum = this.wpListService.getCurrentQueryProps(params);
let newId:string = params.query_id ? params.query_id.toString() : null;
this.cdRef.detectChanges();
// Avoid performing any changes when we're going to reload
if (options.reload || (options.custom && options.custom.notify === false)) {
return true;
}
this.wpListChecksumService
.executeIfOutdated(newId,
newChecksum,
() => this.refresh(true, true));
});
}
protected setupInformationLoadedListener() { protected setupInformationLoadedListener() {
this this
.querySpace .querySpace

@ -0,0 +1,133 @@
#-- 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 'spec_helper'
require_relative '../support/pages/ifc_models/show'
require_relative '../support/pages/ifc_models/show_default'
describe 'BIM filter spec', type: :feature, js: true do
let(:project) { FactoryBot.create :project, enabled_module_names: %w(bim work_package_tracking) }
let(:open_status) { FactoryBot.create(:status, is_closed: false) }
let(:closed_status) { FactoryBot.create(:status, is_closed: true) }
let(:wp1) { FactoryBot.create(:work_package, project: project, status: open_status) }
let(:wp2) { FactoryBot.create(:work_package, project: project, status: closed_status) }
let(:admin) { FactoryBot.create :admin }
let!(:model) do
FactoryBot.create(:ifc_model_converted,
project: project,
uploader: admin)
end
let(:card_view) { ::Pages::WorkPackageCards.new(project) }
let(:filters) { ::Components::WorkPackages::Filters.new }
let(:model_page) { ::Pages::IfcModels::ShowDefault.new project }
before do
wp1
wp2
login_as(admin)
model_page.visit!
model_page.finished_loading
end
context 'on default page' do
before do
# Per default all open work packages are shown
filters.expect_loaded
filters.expect_filter_count 1
filters.open
filters.expect_filter_by('Status', 'open', nil)
card_view.expect_work_package_listed wp1
card_view.expect_work_package_not_listed wp2
end
it 'shows a filter button when there is a list shown' do
model_page.page_shows_a_filter_button true
model_page.switch_view 'Viewer only'
model_page.page_shows_a_filter_button false
end
it 'the filter is applied even after browser back' do
# Change filter
filters.set_operator('Status', 'closed', nil)
filters.expect_filter_count 1
# Otherwise the check for the loading indicator is done
# before it is even shown and the next steps will fail
sleep 0.5
loading_indicator_saveguard
card_view.expect_work_package_not_listed wp1
card_view.expect_work_package_listed wp2
# Using the browser back will reload the filter and the work packages
page.go_back
loading_indicator_saveguard
filters.expect_loaded
filters.expect_filter_count 1
filters.expect_filter_by('Status', 'open', nil)
card_view.expect_work_package_listed wp1
card_view.expect_work_package_not_listed wp2
end
it 'the filter is applied even after reload' do
# Change filter
filters.set_operator('Status', 'closed', nil)
filters.expect_filter_count 1
# Otherwise the check for the loading indicator is done
# before it is even shown and the next steps will fail
sleep 0.5
loading_indicator_saveguard
card_view.expect_work_package_not_listed wp1
card_view.expect_work_package_listed wp2
# Reload and the filter is still correctly applied
page.driver.browser.navigate.refresh
loading_indicator_saveguard
filters.expect_loaded
filters.expect_filter_count 1
filters.open
filters.expect_filter_by('Status', 'closed', nil)
card_view.expect_work_package_not_listed wp1
card_view.expect_work_package_listed wp2
end
end
end

@ -68,16 +68,18 @@ module Pages
tabs = ['Models', 'Objects', 'Classes', 'Storeys'] tabs = ['Models', 'Objects', 'Classes', 'Storeys']
tabs.each do |tab| tabs.each do |tab|
expect(page).to (visible ? have_selector(selector, text: tab) : have_no_selector(selector, text: tab)) element_visible? visible, selector, tab
end end
end end
def page_shows_a_toolbar(visible) def page_shows_a_toolbar(visible)
selector = '.toolbar-item'
toolbar_items.each do |button| toolbar_items.each do |button|
expect(page).to (visible ? have_selector(selector, text: button) : have_no_selector(selector, text: button)) element_visible? visible, '.toolbar-item', button
end
end end
def page_shows_a_filter_button(visible)
element_visible? visible, '.toolbar-item', 'Filter'
end end
def switch_view(value) def switch_view(value)
@ -94,6 +96,10 @@ module Pages
def toolbar_items def toolbar_items
['Manage models'] ['Manage models']
end end
def element_visible?(visible, selector, name)
expect(page).to (visible ? have_selector(selector, text: name) : have_no_selector(selector, text: name))
end
end end
end end
end end

Loading…
Cancel
Save