diff --git a/app/assets/javascripts/onboarding/work_package_tour.js b/app/assets/javascripts/onboarding/work_package_tour.js index 38329ef4a2..9b3d56ff1d 100644 --- a/app/assets/javascripts/onboarding/work_package_tour.js +++ b/app/assets/javascripts/onboarding/work_package_tour.js @@ -37,16 +37,18 @@ resolve(); }); }); - + }, + onNext: function () { + $('#wp-view-toggle-button').click(); } }, { - 'next .timeline-toolbar--button': I18n.t('js.onboarding.steps.wp.timeline_button'), + 'next #wp-view-toggle-button': I18n.t('js.onboarding.steps.wp.timeline_button'), 'showSkip': false, 'nextButton': {text: I18n.t('js.onboarding.buttons.next')}, - 'shape': 'circle', + 'bottom': '-100', onNext: function () { - $('.timeline-toolbar--button')[0].click(); + $('#wp-view-context-menu .icon-view-timeline')[0].click(); } }, { diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index ccea0324fc..1771fff6b5 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -917,3 +917,8 @@ en: all_projects: "all projects" project_and_subprojects: "and all subprojects" search_for: "Search for" + + views: + card: 'Cards' + list: 'Table' + timeline: 'Gantt' diff --git a/frontend/src/app/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts b/frontend/src/app/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts new file mode 100644 index 0000000000..df353a84b6 --- /dev/null +++ b/frontend/src/app/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts @@ -0,0 +1,107 @@ +//-- copyright +// OpenProject is a project management system. +// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +// +// 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 doc/COPYRIGHT.rdoc for more details. +//++ + +import {OPContextMenuService} from "core-components/op-context-menu/op-context-menu.service"; +import {Directive, ElementRef} from "@angular/core"; +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 { + WorkPackageViewDisplayRepresentationService, + wpDisplayCardRepresentation, + wpDisplayListRepresentation +} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; +import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service"; + +@Directive({ + selector: '[wpViewDropdown]' +}) +export class WorkPackageViewDropdownMenuDirective extends OpContextMenuTrigger { + constructor(readonly elementRef:ElementRef, + readonly opContextMenu:OPContextMenuService, + readonly I18n:I18nService, + readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService, + readonly wpTableTimeline:WorkPackageViewTimelineService) { + + super(elementRef, opContextMenu); + } + + protected open(evt:JQueryEventObject) { + this.buildItems(); + this.opContextMenu.show(this, evt); + } + + public get locals() { + return { + items: this.items, + contextMenuId: 'wp-view-context-menu' + }; + } + + private buildItems() { + this.items = [ + { + // Card View + linkText: this.I18n.t('js.views.card'), + icon: 'icon-view-card', + onClick: (evt:any) => { + this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayCardRepresentation); + if (this.wpTableTimeline.isVisible) { + // Necessary for the timeline buttons to disappear + this.wpTableTimeline.toggle(); + } + return true; + } + }, + { + // List View + linkText: this.I18n.t('js.views.list'), + icon: 'icon-view-list', + onClick: (evt:any) => { + this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayListRepresentation); + if (this.wpTableTimeline.isVisible) { + this.wpTableTimeline.toggle(); + } + return true; + } + }, + { + // List View with enabled Gantt + linkText: this.I18n.t('js.views.timeline'), + icon: 'icon-view-timeline', + onClick: (evt:any) => { + this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayListRepresentation); + if (!this.wpTableTimeline.isVisible) { + this.wpTableTimeline.toggle(); + } + return true; + } + } + ]; + } +} + diff --git a/frontend/src/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts b/frontend/src/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts index 7580989f3d..edc273c828 100644 --- a/frontend/src/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts +++ b/frontend/src/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts @@ -30,18 +30,16 @@ import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.servic import {States} from '../../states.service'; import {WorkPackageViewFocusService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.service'; import {StateService, TransitionService} from '@uirouter/core'; -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy} from '@angular/core'; import {AbstractWorkPackageButtonComponent} from 'core-components/wp-buttons/wp-buttons.module'; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; -import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; -import {WorkPackageViewDisplayRepresentationService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; @Component({ templateUrl: '../wp-button.template.html', changeDetection: ChangeDetectionStrategy.OnPush, selector: 'wp-details-view-button', }) -export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageButtonComponent implements OnInit, OnDestroy { +export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageButtonComponent implements OnDestroy { public projectIdentifier:string; public accessKey:number = 8; public activeState:string = 'work-packages.list.details'; @@ -62,9 +60,7 @@ export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageBu readonly cdRef:ChangeDetectorRef, public states:States, public wpTableFocus:WorkPackageViewFocusService, - public keepTab:KeepTabService, - public wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService) { - + public keepTab:KeepTabService) { super(I18n); this.activateLabel = I18n.t('js.button_open_details'); @@ -76,16 +72,6 @@ export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageBu }); } - public ngOnInit() { - this.wpDisplayRepresentationService.live$() - .pipe( - untilComponentDestroyed(this) - ) - .subscribe(() => { - this.cdRef.detectChanges(); - }); - } - public ngOnDestroy() { this.transitionListener(); } diff --git a/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.component.ts b/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.component.ts index 5ed82bbda9..ea01998808 100644 --- a/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.component.ts +++ b/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.component.ts @@ -31,10 +31,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {TimelineZoomLevel} from 'core-app/modules/hal/resources/query-resource'; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; -import { - WorkPackageViewDisplayRepresentationService, - wpDisplayCardRepresentation -} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service"; export interface TimelineButtonText extends ButtonControllerText { @@ -67,8 +63,7 @@ export class WorkPackageTimelineButtonComponent extends AbstractWorkPackageButto constructor(readonly I18n:I18nService, readonly cdRef:ChangeDetectorRef, - public wpTableTimeline:WorkPackageViewTimelineService, - public wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService) { + public wpTableTimeline:WorkPackageViewTimelineService) { super(I18n); this.activateLabel = I18n.t('js.timelines.button_activate'); @@ -102,15 +97,6 @@ export class WorkPackageTimelineButtonComponent extends AbstractWorkPackageButto this.isMinLevel = current === this.minZoomLevel; this.cdRef.detectChanges(); }); - - this.wpDisplayRepresentationService.live$() - .pipe( - untilComponentDestroyed(this) - ) - .subscribe(() => { - this.disabled = this.wpDisplayRepresentationService.current === wpDisplayCardRepresentation; - this.cdRef.detectChanges(); - }); } ngOnDestroy():void { diff --git a/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.html b/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.html index 21b751cc61..6a4ad8e322 100644 --- a/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.html +++ b/frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.html @@ -30,16 +30,4 @@ - -
  • - -
  • - diff --git a/frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts b/frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts index bb8d9830e5..f9d03b148c 100644 --- a/frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts +++ b/frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts @@ -26,65 +26,47 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {AbstractWorkPackageButtonComponent} from '../wp-buttons.module'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper"; -import {StateService} from "@uirouter/core"; import { - WorkPackageViewDisplayRepresentationService, wpDisplayCardRepresentation, + WorkPackageViewDisplayRepresentationService, + wpDisplayCardRepresentation, wpDisplayListRepresentation } from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; +import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service"; @Component({ template: ` - + `, changeDetection: ChangeDetectionStrategy.OnPush, selector: 'wp-view-toggle-button', }) -export class WorkPackageViewToggleButton extends AbstractWorkPackageButtonComponent implements OnInit, OnDestroy { - public iconListView:string = 'icon-view-list'; - public iconCardView:string = 'icon-view-card'; +export class WorkPackageViewToggleButton implements OnInit, OnDestroy { + public view:string; - public inListView:boolean = true; + public text:any = { + card: this.I18n.t('js.views.card'), + list: this.I18n.t('js.views.list'), + timeline: this.I18n.t('js.views.timeline'), + }; - public cardLabel:string; - public listLabel:string; - - constructor(readonly $state:StateService, - readonly I18n:I18nService, + constructor(readonly I18n:I18nService, readonly cdRef:ChangeDetectorRef, - readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService) { - super(I18n); - - this.cardLabel = I18n.t('js.button_card_list'); - this.listLabel = I18n.t('js.button_show_list'); + readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService, + readonly wpTableTimeline:WorkPackageViewTimelineService) { } ngOnInit() { @@ -93,40 +75,26 @@ export class WorkPackageViewToggleButton extends AbstractWorkPackageButtonCompon untilComponentDestroyed(this) ) .subscribe(() => { - this.inListView = this.wpDisplayRepresentationService.current !== wpDisplayCardRepresentation; + this.detectView(); this.cdRef.detectChanges(); }); } ngOnDestroy() { - // + // Nothing to do } - public performAction(evt:Event):false { - if (this.inListView) { - this.activateCardView(); + public detectView() { + if (this.wpDisplayRepresentationService.current !== wpDisplayCardRepresentation) { + if (this.wpTableTimeline.isVisible) { + this.view = 'timeline'; + } else { + this.view = wpDisplayListRepresentation; + } } else { - this.activateListView(); + this.view = wpDisplayCardRepresentation; } - - evt.preventDefault(); - return false; - } - - private activateCardView() { - this.inListView = false; - this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayCardRepresentation); - - this.cdRef.detectChanges(); } - - private activateListView() { - this.inListView = true; - this.wpDisplayRepresentationService.setDisplayRepresentation(wpDisplayListRepresentation); - - this.cdRef.detectChanges(); - } - } DynamicBootstrapper.register({ selector: 'wp-view-toggle-button', cls: WorkPackageViewToggleButton }); diff --git a/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts b/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts index 0c32977eb2..354e80c263 100644 --- a/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts +++ b/frontend/src/app/modules/work_packages/openproject-work-packages.module.ts @@ -158,6 +158,7 @@ import {WorkPackagesTableConfigMenu} from "core-components/wp-table/config-menu/ import {WorkPackageIsolatedGraphQuerySpaceDirective} from "core-app/modules/work_packages/query-space/wp-isolated-graph-query-space.directive"; import {WorkPackageViewToggleButton} from "core-components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component"; import {WorkPackagesGridComponent} from "core-components/wp-grid/wp-grid.component"; +import {WorkPackageViewDropdownMenuDirective} from "core-components/op-context-menu/handlers/wp-view-dropdown-menu.directive"; @NgModule({ @@ -275,6 +276,7 @@ import {WorkPackagesGridComponent} from "core-components/wp-grid/wp-grid.compone WorkPackageCreateSettingsMenuDirective, WorkPackageSingleContextMenuDirective, WorkPackageQuerySelectDropdownComponent, + WorkPackageViewDropdownMenuDirective, // Timeline WorkPackageTimelineButtonComponent, diff --git a/spec/support/components/work_packages/display_representation.rb b/spec/support/components/work_packages/display_representation.rb index c20ee11b04..a457e47711 100644 --- a/spec/support/components/work_packages/display_representation.rb +++ b/spec/support/components/work_packages/display_representation.rb @@ -35,15 +35,39 @@ module Components def initialize; end def switch_to_card_layout - expect(page).to have_button('wp-view-toggle-button--card', disabled: false) - expect(page).to have_button('wp-view-toggle-button--list', disabled: true) - page.find('#wp-view-toggle-button--card').click + expect_button 'Card' + select_view 'Card' end def switch_to_list_layout - expect(page).to have_button('wp-view-toggle-button--card', disabled: true) - expect(page).to have_button('wp-view-toggle-button--list', disabled: false) - page.find('#wp-view-toggle-button--list').click + expect_button 'Table' + select_view 'Table' + end + + def switch_to_gantt_layout + expect_button 'Gantt' + select_view 'Gantt' + end + + private + + def expect_button(forbidden_text) + expect(page).to have_button('wp-view-toggle-button', disabled: false) + expect(page).to have_no_selector('#wp-view-toggle-button', text: forbidden_text) + end + + def select_view(view_name) + page.find('wp-view-toggle-button').click + + within_view_context_menu do + click_link(view_name) + end + end + + def within_view_context_menu + page.within('#wp-view-context-menu') do + yield + end end end end diff --git a/spec/support/pages/work_packages/work_packages_timeline.rb b/spec/support/pages/work_packages/work_packages_timeline.rb index 2418b6d933..6be4f2ea2c 100644 --- a/spec/support/pages/work_packages/work_packages_timeline.rb +++ b/spec/support/pages/work_packages/work_packages_timeline.rb @@ -32,7 +32,7 @@ require 'support/pages/work_packages/work_packages_table' module Pages class WorkPackagesTimeline < WorkPackagesTable def toggle_timeline - find('#work-packages-timeline-toggle-button').click + ::Components::WorkPackages::DisplayRepresentation.new.switch_to_gantt_layout end def timeline_row_selector(wp_id) @@ -52,7 +52,7 @@ module Pages def expect_work_package_listed(*work_packages) super(*work_packages) - if page.has_selector?('#work-packages-timeline-toggle-button.-active') + if page.has_selector?('#wp-view-toggle-button', text: 'Gantt') within(timeline_container) do work_packages.each do |wp| expect(page).to have_selector(".wp-row-#{wp.id}-timeline", visible: true) @@ -64,7 +64,7 @@ module Pages def expect_work_package_not_listed(*work_packages) super(*work_packages) - if page.has_selector?('#work-packages-timeline-toggle-button.-active') + if page.has_selector?('#wp-view-toggle-button', text: 'Gantt') within(timeline_container) do work_packages.each do |wp| expect(page).to have_no_selector(".wp-row-#{wp.id}-timeline", visible: true) @@ -85,10 +85,10 @@ module Pages def expect_timeline!(open: true) if open - expect(page).to have_selector('#work-packages-timeline-toggle-button.-active') + expect(page).to have_selector('#wp-view-toggle-button', text: 'Gantt') expect(page).to have_selector('.wp-table-timeline--container .wp-timeline-cell') else - expect(page).to have_no_selector('#work-packages-timeline-toggle-button.-active') + expect(page).to have_no_selector('#wp-view-toggle-button', text: 'Gantt') expect(page).to have_no_selector('.wp-table-timeline--container .wp-timeline-cell', visible: true) end end