Merge pull request #7548 from opf/feature/30746-View-selector-to-switch-between-Gantt-view,-tiles-view-and-list-view

[30746] View selector to switch between Gantt view, tiles view and list view

[ci skip]
pull/7551/head
Oliver Günther 5 years ago committed by GitHub
commit 39de230bd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      app/assets/javascripts/onboarding/work_package_tour.js
  2. 5
      config/locales/js-en.yml
  3. 107
      frontend/src/app/components/op-context-menu/handlers/wp-view-dropdown-menu.directive.ts
  4. 20
      frontend/src/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts
  5. 16
      frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.component.ts
  6. 12
      frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.html
  7. 92
      frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts
  8. 2
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  9. 36
      spec/support/components/work_packages/display_representation.rb
  10. 10
      spec/support/pages/work_packages/work_packages_timeline.rb

@ -37,16 +37,18 @@
resolve(); 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, 'showSkip': false,
'nextButton': {text: I18n.t('js.onboarding.buttons.next')}, 'nextButton': {text: I18n.t('js.onboarding.buttons.next')},
'shape': 'circle', 'bottom': '-100',
onNext: function () { onNext: function () {
$('.timeline-toolbar--button')[0].click(); $('#wp-view-context-menu .icon-view-timeline')[0].click();
} }
}, },
{ {

@ -917,3 +917,8 @@ en:
all_projects: "all projects" all_projects: "all projects"
project_and_subprojects: "and all subprojects" project_and_subprojects: "and all subprojects"
search_for: "Search for" search_for: "Search for"
views:
card: 'Cards'
list: 'Table'
timeline: 'Gantt'

@ -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;
}
}
];
}
}

@ -30,18 +30,16 @@ import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.servic
import {States} from '../../states.service'; import {States} from '../../states.service';
import {WorkPackageViewFocusService} from 'core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-focus.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 {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 {AbstractWorkPackageButtonComponent} from 'core-components/wp-buttons/wp-buttons.module';
import {I18nService} from "core-app/modules/common/i18n/i18n.service"; 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({ @Component({
templateUrl: '../wp-button.template.html', templateUrl: '../wp-button.template.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wp-details-view-button', selector: 'wp-details-view-button',
}) })
export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageButtonComponent implements OnInit, OnDestroy { export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageButtonComponent implements OnDestroy {
public projectIdentifier:string; public projectIdentifier:string;
public accessKey:number = 8; public accessKey:number = 8;
public activeState:string = 'work-packages.list.details'; public activeState:string = 'work-packages.list.details';
@ -62,9 +60,7 @@ export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageBu
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
public states:States, public states:States,
public wpTableFocus:WorkPackageViewFocusService, public wpTableFocus:WorkPackageViewFocusService,
public keepTab:KeepTabService, public keepTab:KeepTabService) {
public wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService) {
super(I18n); super(I18n);
this.activateLabel = I18n.t('js.button_open_details'); 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() { public ngOnDestroy() {
this.transitionListener(); this.transitionListener();
} }

@ -31,10 +31,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit
import {I18nService} from 'core-app/modules/common/i18n/i18n.service'; import {I18nService} from 'core-app/modules/common/i18n/i18n.service';
import {TimelineZoomLevel} from 'core-app/modules/hal/resources/query-resource'; import {TimelineZoomLevel} from 'core-app/modules/hal/resources/query-resource';
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; 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"; import {WorkPackageViewTimelineService} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-timeline.service";
export interface TimelineButtonText extends ButtonControllerText { export interface TimelineButtonText extends ButtonControllerText {
@ -67,8 +63,7 @@ export class WorkPackageTimelineButtonComponent extends AbstractWorkPackageButto
constructor(readonly I18n:I18nService, constructor(readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
public wpTableTimeline:WorkPackageViewTimelineService, public wpTableTimeline:WorkPackageViewTimelineService) {
public wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService) {
super(I18n); super(I18n);
this.activateLabel = I18n.t('js.timelines.button_activate'); this.activateLabel = I18n.t('js.timelines.button_activate');
@ -102,15 +97,6 @@ export class WorkPackageTimelineButtonComponent extends AbstractWorkPackageButto
this.isMinLevel = current === this.minZoomLevel; this.isMinLevel = current === this.minZoomLevel;
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}); });
this.wpDisplayRepresentationService.live$()
.pipe(
untilComponentDestroyed(this)
)
.subscribe(() => {
this.disabled = this.wpDisplayRepresentationService.current === wpDisplayCardRepresentation;
this.cdRef.detectChanges();
});
} }
ngOnDestroy():void { ngOnDestroy():void {

@ -30,16 +30,4 @@
<op-icon icon-classes="icon-zoom-in button--icon"></op-icon> <op-icon icon-classes="icon-zoom-in button--icon"></op-icon>
</button> </button>
</li> </li>
<li>
<button class="button timeline-toolbar--button toolbar-icon"
[ngClass]="{ '-active': isActive }"
[attr.id]="buttonId"
[attr.title]="label"
[disabled]="disabled || (!isToggle() && isActive)"
(click)="toggleTimeline()">
<op-icon icon-classes="{{ iconClass }} button--icon"></op-icon>
</button>
</li>
</ul> </ul>

@ -26,65 +26,47 @@
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ // ++
import {AbstractWorkPackageButtonComponent} from '../wp-buttons.module';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, 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 {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper"; import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
import {StateService} from "@uirouter/core";
import { import {
WorkPackageViewDisplayRepresentationService, wpDisplayCardRepresentation, WorkPackageViewDisplayRepresentationService,
wpDisplayCardRepresentation,
wpDisplayListRepresentation wpDisplayListRepresentation
} from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; } from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; 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({ @Component({
template: ` template: `
<ul class="toolbar-button-group">
<li>
<button class="button" <button class="button"
type="button" id="wp-view-toggle-button"
[ngClass]="{ '-active': inListView }" wpViewDropdown>
[disabled]="inListView" <op-icon icon-classes="button--icon icon-view-{{view}}"></op-icon>
id="wp-view-toggle-button--list" <span class="button--text"
[attr.title]="listLabel" aria-hidden="true"
[attr.accesskey]="accessKey" [textContent]="text[view]">
(accessibleClick)="performAction($event)"> </span>
<op-icon icon-classes="{{ iconListView }} button--icon"></op-icon> <op-icon icon-classes="button--icon icon-small icon-pulldown"></op-icon>
</button> </button>
</li>
<li>
<button class="button"
[ngClass]="{ '-active': !inListView }"
id="wp-view-toggle-button--card"
[attr.title]="cardLabel"
[disabled]="!inListView"
(click)="performAction($event)">
<op-icon icon-classes="{{ iconCardView }} button--icon"></op-icon>
</button>
</li>
</ul>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'wp-view-toggle-button', selector: 'wp-view-toggle-button',
}) })
export class WorkPackageViewToggleButton extends AbstractWorkPackageButtonComponent implements OnInit, OnDestroy { export class WorkPackageViewToggleButton implements OnInit, OnDestroy {
public iconListView:string = 'icon-view-list'; public view:string;
public iconCardView:string = 'icon-view-card';
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; constructor(readonly I18n:I18nService,
public listLabel:string;
constructor(readonly $state:StateService,
readonly I18n:I18nService,
readonly cdRef:ChangeDetectorRef, readonly cdRef:ChangeDetectorRef,
readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService) { readonly wpDisplayRepresentationService:WorkPackageViewDisplayRepresentationService,
super(I18n); readonly wpTableTimeline:WorkPackageViewTimelineService) {
this.cardLabel = I18n.t('js.button_card_list');
this.listLabel = I18n.t('js.button_show_list');
} }
ngOnInit() { ngOnInit() {
@ -93,40 +75,26 @@ export class WorkPackageViewToggleButton extends AbstractWorkPackageButtonCompon
untilComponentDestroyed(this) untilComponentDestroyed(this)
) )
.subscribe(() => { .subscribe(() => {
this.inListView = this.wpDisplayRepresentationService.current !== wpDisplayCardRepresentation; this.detectView();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
}); });
} }
ngOnDestroy() { ngOnDestroy() {
// // Nothing to do
} }
public performAction(evt:Event):false { public detectView() {
if (this.inListView) { if (this.wpDisplayRepresentationService.current !== wpDisplayCardRepresentation) {
this.activateCardView(); if (this.wpTableTimeline.isVisible) {
this.view = 'timeline';
} else { } else {
this.activateListView(); this.view = wpDisplayListRepresentation;
} }
} else {
evt.preventDefault(); this.view = wpDisplayCardRepresentation;
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 }); DynamicBootstrapper.register({ selector: 'wp-view-toggle-button', cls: WorkPackageViewToggleButton });

@ -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 {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 {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 {WorkPackagesGridComponent} from "core-components/wp-grid/wp-grid.component";
import {WorkPackageViewDropdownMenuDirective} from "core-components/op-context-menu/handlers/wp-view-dropdown-menu.directive";
@NgModule({ @NgModule({
@ -275,6 +276,7 @@ import {WorkPackagesGridComponent} from "core-components/wp-grid/wp-grid.compone
WorkPackageCreateSettingsMenuDirective, WorkPackageCreateSettingsMenuDirective,
WorkPackageSingleContextMenuDirective, WorkPackageSingleContextMenuDirective,
WorkPackageQuerySelectDropdownComponent, WorkPackageQuerySelectDropdownComponent,
WorkPackageViewDropdownMenuDirective,
// Timeline // Timeline
WorkPackageTimelineButtonComponent, WorkPackageTimelineButtonComponent,

@ -35,15 +35,39 @@ module Components
def initialize; end def initialize; end
def switch_to_card_layout def switch_to_card_layout
expect(page).to have_button('wp-view-toggle-button--card', disabled: false) expect_button 'Card'
expect(page).to have_button('wp-view-toggle-button--list', disabled: true) select_view 'Card'
page.find('#wp-view-toggle-button--card').click
end end
def switch_to_list_layout def switch_to_list_layout
expect(page).to have_button('wp-view-toggle-button--card', disabled: true) expect_button 'Table'
expect(page).to have_button('wp-view-toggle-button--list', disabled: false) select_view 'Table'
page.find('#wp-view-toggle-button--list').click 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 end
end end

@ -32,7 +32,7 @@ require 'support/pages/work_packages/work_packages_table'
module Pages module Pages
class WorkPackagesTimeline < WorkPackagesTable class WorkPackagesTimeline < WorkPackagesTable
def toggle_timeline def toggle_timeline
find('#work-packages-timeline-toggle-button').click ::Components::WorkPackages::DisplayRepresentation.new.switch_to_gantt_layout
end end
def timeline_row_selector(wp_id) def timeline_row_selector(wp_id)
@ -52,7 +52,7 @@ module Pages
def expect_work_package_listed(*work_packages) def expect_work_package_listed(*work_packages)
super(*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 within(timeline_container) do
work_packages.each do |wp| work_packages.each do |wp|
expect(page).to have_selector(".wp-row-#{wp.id}-timeline", visible: true) 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) def expect_work_package_not_listed(*work_packages)
super(*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 within(timeline_container) do
work_packages.each do |wp| work_packages.each do |wp|
expect(page).to have_no_selector(".wp-row-#{wp.id}-timeline", visible: true) expect(page).to have_no_selector(".wp-row-#{wp.id}-timeline", visible: true)
@ -85,10 +85,10 @@ module Pages
def expect_timeline!(open: true) def expect_timeline!(open: true)
if open 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') expect(page).to have_selector('.wp-table-timeline--container .wp-timeline-cell')
else 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) expect(page).to have_no_selector('.wp-table-timeline--container .wp-timeline-cell', visible: true)
end end
end end

Loading…
Cancel
Save