Merge pull request #9885 from opf/feature/39772-query-select-in-bcf-module

[#39772] Show queries in bcf module
pull/9902/head
Oliver Günther 3 years ago committed by GitHub
commit 4ca62f81fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      app/views/work_packages/_menu_query_select.html.erb
  2. 9
      frontend/src/app/core/setup/global-dynamic-components.const.ts
  3. 3
      frontend/src/app/features/bim/ifc_models/openproject-ifc-models.routes.ts
  4. 4
      frontend/src/app/features/work-packages/openproject-work-packages.module.ts
  5. 44
      frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts
  6. 4
      frontend/src/app/features/work-packages/routing/wp-view-base/work-packages-view.base.ts
  7. 52
      frontend/src/app/shared/components/op-query-select/op-query-select.component.ts
  8. 2
      frontend/src/app/shared/components/op-query-select/op-query-select.template.html
  9. 98
      frontend/src/app/shared/components/op-query-select/op-static-queries.service.ts
  10. 6
      frontend/src/app/shared/shared.module.ts
  11. 5
      lib/redmine/menu_manager/menu_helper.rb
  12. 15
      modules/bim/app/views/bim/ifc_models/ifc_models/_panels.html.erb
  13. 2
      spec/support/pages/work_packages/work_packages_table.rb

@ -1 +1,8 @@
<wp-query-select data-project-identifier="<%= @project ? @project.identifier : '' %>"></wp-query-select>
<%=
angular_component_tag 'op-query-select',
inputs: {
projectIdentifier: (@project ? @project.identifier : ''),
menuItems: [parent_name, name],
baseRoute: 'work-packages'
}
%>

@ -98,9 +98,9 @@ import {
wpOverviewGraphSelector,
} from 'core-app/shared/components/work-package-graphs/overview/wp-overview-graph.component';
import {
WorkPackageQuerySelectDropdownComponent,
wpQuerySelectSelector,
} from 'core-app/features/work-packages/components/wp-query-select/wp-query-select-dropdown.component';
opQuerySelectSelector,
QuerySelectComponent,
} from 'core-app/shared/components/op-query-select/op-query-select.component';
import {
GlobalSearchTitleComponent,
globalSearchTitleSelector,
@ -184,7 +184,6 @@ import {
ianMenuSelector,
} from 'core-app/features/in-app-notifications/center/menu/menu.component';
export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: appBaseSelector, cls: ApplicationBaseComponent },
{ selector: attributeHelpTextSelector, cls: AttributeHelpTextComponent },
@ -224,7 +223,7 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: projectMenuAutocompleteSelector, cls: ProjectMenuAutocompleteComponent },
{ selector: remoteFieldUpdaterSelector, cls: RemoteFieldUpdaterComponent },
{ selector: wpOverviewGraphSelector, cls: WorkPackageOverviewGraphComponent },
{ selector: wpQuerySelectSelector, cls: WorkPackageQuerySelectDropdownComponent },
{ selector: opQuerySelectSelector, cls: QuerySelectComponent },
{ selector: triggerActionsEntryComponentSelector, cls: TriggerActionsEntryComponent, embeddable: true },
{ selector: backlogsPageComponentSelector, cls: BacklogsPageComponent },
{ selector: attributeValueMacro, cls: AttributeValueMacroComponent, embeddable: true },

@ -40,12 +40,13 @@ export const IFC_ROUTES:Ng2StateDeclaration[] = [
{
name: 'bim',
parent: 'optional_project',
url: '/bcf?query_props&models&viewpoint',
url: '/bcf?query_id&query_props&models&viewpoint',
abstract: true,
component: WorkPackagesBaseComponent,
redirectTo: 'bim.partitioned',
params: {
// Use custom encoder/decoder that ensures validity of URL string
query_id: { type: 'query', dynamic: true },
query_props: { type: 'opQueryString', dynamic: true },
models: { type: 'opQueryString', dynamic: true },
viewpoint: { type: 'int', dynamic: true },

@ -42,7 +42,6 @@ import { OpSettingsMenuDirective } from 'core-app/shared/components/op-context-m
import { WorkPackageStatusDropdownDirective } from 'core-app/shared/components/op-context-menu/handlers/wp-status-dropdown-menu.directive';
import { WorkPackageCreateSettingsMenuDirective } from 'core-app/shared/components/op-context-menu/handlers/wp-create-settings-menu.directive';
import { WorkPackageSingleContextMenuDirective } from 'core-app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu';
import { WorkPackageQuerySelectDropdownComponent } from 'core-app/features/work-packages/components/wp-query-select/wp-query-select-dropdown.component';
import { WorkPackageTimelineHeaderController } from 'core-app/features/work-packages/components/wp-table/timeline/header/wp-timeline-header.directive';
import { WorkPackageTableTimelineRelations } from 'core-app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-relations.directive';
import { WorkPackageTableTimelineStaticElements } from 'core-app/features/work-packages/components/wp-table/timeline/global-elements/wp-timeline-static-elements.directive';
@ -85,7 +84,6 @@ import { ExternalQueryConfigurationComponent } from 'core-app/features/work-pack
import { ExternalQueryConfigurationService } from 'core-app/features/work-packages/components/wp-table/external-configuration/external-query-configuration.service';
import { ExternalRelationQueryConfigurationComponent } from 'core-app/features/work-packages/components/wp-table/external-configuration/external-relation-query-configuration.component';
import { ExternalRelationQueryConfigurationService } from 'core-app/features/work-packages/components/wp-table/external-configuration/external-relation-query-configuration.service';
import { WorkPackageStaticQueriesService } from 'core-app/features/work-packages/components/wp-query-select/wp-static-queries.service';
import { WorkPackagesListInvalidQueryService } from 'core-app/features/work-packages/components/wp-list/wp-list-invalid-query.service';
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import { WorkPackageWatchersService } from 'core-app/features/work-packages/components/wp-single-view-tabs/watchers-tab/wp-watchers.service';
@ -213,7 +211,6 @@ import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
// For any service that depends on the isolated query space,
// they should be provided in wp-isolated-query-space.directive instead
QueryFiltersService,
WorkPackageStaticQueriesService,
WorkPackagesListInvalidQueryService,
// Provide a separate service for creation events of WP Inline create
@ -298,7 +295,6 @@ import isNewResource from 'core-app/features/hal/helpers/is-new-resource';
WorkPackageStatusDropdownDirective,
WorkPackageCreateSettingsMenuDirective,
WorkPackageSingleContextMenuDirective,
WorkPackageQuerySelectDropdownComponent,
WorkPackageViewDropdownMenuDirective,
WorkPackageGroupToggleDropdownMenuDirective,

@ -100,9 +100,6 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
showToolbarSaveButton:boolean;
/** Listener callbacks */
// eslint-disable-next-line @typescript-eslint/ban-types
unRegisterTitleListener:Function;
// eslint-disable-next-line @typescript-eslint/ban-types
removeTransitionSubscription:Function;
@ -149,24 +146,20 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
// Load query on URL transitions
this.queryParamListener
.observe$
.pipe(
this.untilDestroyed(),
).subscribe(() => {
/** Ensure we reload the query from the changed props */
.pipe(this.untilDestroyed())
.subscribe(() => {
/** Ensure we reload the query from the changed props */
this.currentQuery = undefined;
this.refresh(true, true);
void this.refresh(true, true);
});
// Update title on entering this state
this.unRegisterTitleListener = this.$transitions.onSuccess({}, () => {
this.updateTitle(this.querySpace.query.value);
});
this.querySpace.query.values$().pipe(
this.untilDestroyed(),
).subscribe((query) => {
this.onQueryUpdated(query);
});
this.querySpace.query.values$()
.pipe(this.untilDestroyed())
.subscribe((query) => {
// Update the title whenever the query changes
this.updateTitle(query);
this.currentQuery = query;
});
}
/**
@ -175,11 +168,11 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
*
* @param state The current or entering state
*/
protected setPartition(state:Ng2StateDeclaration) {
protected setPartition(state:Ng2StateDeclaration):void {
this.currentPartition = (state.data && state.data.partition) ? state.data.partition : '-split';
}
protected setupInformationLoadedListener() {
protected setupInformationLoadedListener():void {
this
.querySpace
.initialized
@ -191,17 +184,8 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
});
}
protected onQueryUpdated(query:QueryResource) {
// Update the title whenever the query changes
this.updateTitle(query);
this.currentQuery = query;
this.cdRef.detectChanges();
}
ngOnDestroy():void {
super.ngOnDestroy();
this.unRegisterTitleListener();
this.removeTransitionSubscription();
this.queryParamListener.removeQueryChangeListener();
}
@ -234,7 +218,7 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
if (isPersistedResource(query)) {
this.selectedTitle = query.name;
} else {
this.selectedTitle = this.wpStaticQueries.getStaticName(query);
this.selectedTitle = this.opStaticQueries.getStaticName(query);
}
this.titleEditingEnabled = this.authorisationService.can('query', 'updateImmediately');

@ -35,7 +35,7 @@ import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/q
import { filter, take, withLatestFrom } from 'rxjs/operators';
import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading-indicator.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { WorkPackageStaticQueriesService } from 'core-app/features/work-packages/components/wp-query-select/wp-static-queries.service';
import { StaticQueriesService } from 'core-app/shared/components/op-query-select/op-static-queries.service';
import { WorkPackageViewHighlightingService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-highlighting.service';
import { States } from 'core-app/core/states/states.service';
import { WorkPackageViewColumnsService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-columns.service';
@ -98,7 +98,7 @@ export abstract class WorkPackagesViewBase extends UntilDestroyedMixin implement
@InjectField() I18n!:I18nService;
@InjectField() wpStaticQueries:WorkPackageStaticQueriesService;
@InjectField() opStaticQueries:StaticQueriesService;
@InjectField() wpStatesInitialization:WorkPackageStatesInitializationService;

@ -30,28 +30,35 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnInit,
} from '@angular/core';
import { map } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import {
BehaviorSubject,
combineLatest,
Observable,
} from 'rxjs';
import { States } from 'core-app/core/states/states.service';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { APIV3Service } from 'core-app/core/apiv3/api-v3.service';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
import { DatasetInputs } from 'core-app/shared/components/dataset-inputs.decorator';
import { MainMenuNavigationService } from 'core-app/core/main-menu/main-menu-navigation.service';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { IOpSidemenuItem } from 'core-app/shared/components/sidemenu/sidemenu.component';
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { WorkPackageStaticQueriesService } from 'core-app/features/work-packages/components/wp-query-select/wp-static-queries.service';
import { StaticQueriesService } from 'core-app/shared/components/op-query-select/op-static-queries.service';
export const wpQuerySelectSelector = 'wp-query-select';
export const opQuerySelectSelector = 'op-query-select';
@DatasetInputs
@Component({
selector: wpQuerySelectSelector,
selector: opQuerySelectSelector,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './wp-query-select.template.html',
templateUrl: './op-query-select.template.html',
})
export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin implements OnInit {
export class QuerySelectComponent extends UntilDestroyedMixin implements OnInit {
public text = {
search: this.I18n.t('js.toolbar.search_query_label'),
label: this.I18n.t('js.toolbar.search_query_label'),
@ -64,6 +71,12 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
public $queries:Observable<IOpSidemenuItem[]>;
@Input() menuItems:string[] = [];
@Input() projectIdentifier:string;
@Input() baseRoute:string;
private $queryCategories = new BehaviorSubject<IOpSidemenuItem[]>([]);
private $search = new BehaviorSubject<string>('');
@ -71,11 +84,11 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
private initialized = false;
constructor(
readonly elementRef:ElementRef,
readonly apiV3Service:APIV3Service,
readonly I18n:I18nService,
readonly states:States,
readonly CurrentProject:CurrentProjectService,
readonly wpStaticQueries:WorkPackageStaticQueriesService,
readonly opStaticQueries:StaticQueriesService,
readonly mainMenuService:MainMenuNavigationService,
readonly cdRef:ChangeDetectorRef,
) {
@ -92,7 +105,7 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
// When activating the work packages submenu,
// either initially or through click on the toggle, load the results
this.mainMenuService
.onActivate('work_packages', 'work_packages_query_select')
.onActivate(...this.menuItems)
.subscribe(() => this.initializeAutocomplete());
this.$queries = combineLatest(
@ -102,11 +115,12 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
.pipe(
map(([searchText, categories]) => categories
.map((category) => {
if (this.matchesText(category.title, searchText)) {
if (QuerySelectComponent.matchesText(category.title, searchText)) {
return category;
}
const filteredChildren = category.children?.filter((query) => this.matchesText(query.title, searchText));
const filteredChildren = category.children
?.filter((query) => QuerySelectComponent.matchesText(query.title, searchText));
return { title: category.title, children: filteredChildren, collapsible: true };
})
.filter((category) => category.children && category.children.length > 0)),
@ -131,8 +145,7 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
this.initialized = true;
}
// noinspection JSMethodCanBeStatic
private matchesText(text:string, searchText:string):boolean {
private static matchesText(text:string, searchText:string):boolean {
return text.toLowerCase().includes(searchText.toLowerCase());
}
@ -147,7 +160,7 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
// TODO: use global query store
this.apiV3Service
.queries
.filterNonHidden(this.CurrentProject.identifier)
.filterNonHidden(this.projectIdentifier)
.pipe(this.untilDestroyed())
.subscribe((queryCollection) => {
queryCollection.elements.forEach((query) => {
@ -159,22 +172,23 @@ export class WorkPackageQuerySelectDropdownComponent extends UntilDestroyedMixin
cat = 'starred';
}
categories[cat].push(WorkPackageQuerySelectDropdownComponent.toOpSideMenuItem(query));
categories[cat].push(this.toOpSideMenuItem(query));
});
const staticQueries = this.opStaticQueries.getStaticQueries(this.baseRoute);
this.$queryCategories.next([
{ title: this.text.scope_starred, children: categories.starred, collapsible: true },
{ title: this.text.scope_default, children: this.wpStaticQueries.all, collapsible: true },
{ title: this.text.scope_default, children: staticQueries, collapsible: true },
{ title: this.text.scope_global, children: categories.public, collapsible: true },
{ title: this.text.scope_private, children: categories.private, collapsible: true },
]);
});
}
private static toOpSideMenuItem(query:QueryResource):IOpSidemenuItem {
private toOpSideMenuItem(query:QueryResource):IOpSidemenuItem {
return {
title: query.name,
uiSref: 'work-packages',
uiSref: this.baseRoute,
uiParams: { query_id: query.id, query_props: '' },
};
}

@ -1,4 +1,4 @@
<div id="querySelectDropdown" class="searchable-menu">
<div id="querySelect" class="searchable-menu">
<div class="searchable-menu--search-container">
<div class="searchable-menu--search-bar">
<label for="query-title-filter"

@ -36,7 +36,7 @@ import { CurrentUserService } from 'core-app/core/current-user/current-user.serv
import { IOpSidemenuItem } from 'core-app/shared/components/sidemenu/sidemenu.component';
@Injectable()
export class WorkPackageStaticQueriesService {
export class StaticQueriesService {
constructor(private readonly I18n:I18nService,
private readonly $state:StateService,
private readonly CurrentProject:CurrentProjectService,
@ -60,32 +60,53 @@ export class WorkPackageStaticQueriesService {
summary: this.I18n.t('js.work_packages.default_queries.summary'),
};
public get all():IOpSidemenuItem[] {
public getStaticName(query:QueryResource):string {
if (this.$state.params.query_props) {
const queryProps = JSON.parse(this.$state.params.query_props) as { pa:unknown, pp:unknown }&unknown;
delete queryProps.pp;
delete queryProps.pa;
const queryPropsString = JSON.stringify(queryProps);
// TODO: Get module route from query
const matched = this.getStaticQueries('work-packages').find((item) => {
const uiParams = item.uiParams as { query_id:string, query_props:string };
return uiParams && uiParams.query_props === queryPropsString;
});
if (matched) {
return matched.title;
}
}
// Try to detect the all open filter
if (query.filters.length === 1 // Only one filter
&& query.filters[0].id === 'status' // that is status
&& query.filters[0].operator.id === 'o') { // and is open
return this.text.all_open;
}
// Otherwise, fall back to work packages
return this.text.work_packages;
}
public getStaticQueries(module:string):IOpSidemenuItem[] {
let items:IOpSidemenuItem[] = [
{
title: this.text.all_open,
uiSref: 'work-packages',
uiSref: module,
uiParams: { query_id: '', query_props: '' },
},
{
title: this.text.latest_activity,
uiSref: 'work-packages',
uiSref: module,
uiParams: {
query_id: '',
query_props: '{"c":["id","subject","type","status","assignee","updatedAt"],"hi":false,"g":"","t":"updatedAt:desc","f":[{"n":"status","o":"o","v":[]}]}',
},
},
{
title: this.text.gantt,
uiSref: 'work-packages',
uiParams: {
query_id: '',
query_props: '{"c":["id","type","subject","status","startDate","dueDate"],"tv":true,"tzl":"auto","tll":"{\\"left\\":\\"startDate\\",\\"right\\":\\"dueDate\\",\\"farRight\\":\\"subject\\"}","hi":true,"g":"","t":"startDate:asc","f":[{"n":"status","o":"o","v":[]}]}',
},
},
{
title: this.text.recently_created,
uiSref: 'work-packages',
uiSref: module,
uiParams: {
query_id: '',
query_props: '{"c":["id","subject","type","status","assignee","createdAt"],"hi":false,"g":"","t":"createdAt:desc","f":[{"n":"status","o":"o","v":[]}]}',
@ -93,12 +114,23 @@ export class WorkPackageStaticQueriesService {
},
];
const projectIdentifier = this.CurrentProject.identifier;
if (projectIdentifier) {
if (module === 'work-packages') {
items.push({
title: this.text.summary,
href: `${this.PathHelper.projectWorkPackagesPath(projectIdentifier)}/report`,
title: this.text.gantt,
uiSref: module,
uiParams: {
query_id: '',
query_props: '{"c":["id","type","subject","status","startDate","dueDate"],"tv":true,"tzl":"auto","tll":"{\\"left\\":\\"startDate\\",\\"right\\":\\"dueDate\\",\\"farRight\\":\\"subject\\"}","hi":true,"g":"","t":"startDate:asc","f":[{"n":"status","o":"o","v":[]}]}',
},
});
const projectIdentifier = this.CurrentProject.identifier;
if (projectIdentifier) {
items.push({
title: this.text.summary,
href: `${this.PathHelper.projectWorkPackagesPath(projectIdentifier)}/report`,
});
}
}
if (this.CurrentUser.isLoggedIn) {
@ -106,7 +138,7 @@ export class WorkPackageStaticQueriesService {
...items,
{
title: this.text.created_by_me,
uiSref: 'work-packages',
uiSref: module,
uiParams: {
query_id: '',
query_props: '{"c":["id","subject","type","status","assignee","updatedAt"],"hi":false,"g":"","t":"updatedAt:desc,id:asc","f":[{"n":"status","o":"o","v":[]},{"n":"author","o":"=","v":["me"]}]}',
@ -114,7 +146,7 @@ export class WorkPackageStaticQueriesService {
},
{
title: this.text.assigned_to_me,
uiSref: 'work-packages',
uiSref: module,
uiParams: {
query_id: '',
query_props: '{"c":["id","subject","type","status","author","updatedAt"],"hi":false,"g":"","t":"updatedAt:desc,id:asc","f":[{"n":"status","o":"o","v":[]},{"n":"assigneeOrGroup","o":"=","v":["me"]}]}',
@ -125,32 +157,4 @@ export class WorkPackageStaticQueriesService {
return items;
}
public getStaticName(query:QueryResource):string {
if (this.$state.params.query_props) {
const queryProps = JSON.parse(this.$state.params.query_props);
delete queryProps.pp;
delete queryProps.pa;
const queryPropsString = JSON.stringify(queryProps);
const matched = this.all.find((item) => {
const uiParams = item.uiParams as { query_id:string, query_props:string };
return uiParams && uiParams.query_props === queryPropsString;
});
if (matched) {
return matched.title;
}
}
// Try to detect the all open filter
if (query.filters.length === 1 // Only one filter
&& query.filters[0].id === 'status' // that is status
&& query.filters[0].operator.id === 'o') { // and is open
return this.text.all_open;
}
// Otherwise, fall back to work packages
return this.text.work_packages;
}
}

@ -57,6 +57,8 @@ import { EnterpriseBannerBootstrapComponent } from 'core-app/shared/components/e
import { HomescreenNewFeaturesBlockComponent } from 'core-app/features/homescreen/blocks/new-features.component';
import { TablePaginationComponent } from 'core-app/shared/components/table-pagination/table-pagination.component';
import { HookService } from 'core-app/features/plugins/hook-service';
import { QuerySelectComponent } from 'core-app/shared/components/op-query-select/op-query-select.component';
import { StaticQueriesService } from 'core-app/shared/components/op-query-select/op-static-queries.service';
import {
highlightColSelector,
OpHighlightColDirective,
@ -186,8 +188,12 @@ export function bootstrapModule(injector:Injector) {
OpOptionListComponent,
OpSidemenuComponent,
],
providers: [
StaticQueriesService,
],
declarations: [
OpDateTimeComponent,
QuerySelectComponent,
ToastsContainerComponent,
ToastComponent,

@ -231,7 +231,10 @@ module Redmine::MenuManager::MenuHelper
def render_single_node_or_partial(node, project)
if node.partial
content_tag('li', render(partial: node.partial), class: "partial", data: { name: node.name })
content_tag('li',
render(partial: node.partial, locals: { name: node.name, parent_name: node.parent.name }),
class: "partial",
data: { name: node.name })
else
content_tag('li', render_single_menu_node(node, project), data: { name: node.name })
end

@ -1 +1,14 @@
<div class="op-ifc-viewer--tree-panel active" data-qa-selector="op-ifc-viewer--tree-panel"></div>
<div class="op-ifc-viewer--sidemenu">
<%=
angular_component_tag 'op-query-select',
inputs: {
projectIdentifier: (@project ? @project.identifier : ''),
menuItems: [parent_name, name],
baseRoute: 'bim.partitioned.split'
}
%>
<hr>
<div class="op-ifc-viewer--tree-panel active" data-qa-selector="op-ifc-viewer--tree-panel"></div>
</div>

@ -123,7 +123,7 @@ module Pages
def expect_query_in_select_dropdown(name)
page.find('.title-container').click
page.within('#querySelectDropdown') do
page.within('#querySelect') do
expect(page).to have_selector('.op-sidemenu--item-action', text: name)
end
end

Loading…
Cancel
Save