diff --git a/app/contracts/queries/base_contract.rb b/app/contracts/queries/base_contract.rb index 6794647b61..23db2a6560 100644 --- a/app/contracts/queries/base_contract.rb +++ b/app/contracts/queries/base_contract.rb @@ -44,6 +44,7 @@ module Queries attribute :highlighting_mode attribute :highlighted_attributes attribute :show_hierarchies + attribute :display_representation attribute :column_names # => columns attribute :filters diff --git a/app/services/api/v3/parse_query_params_service.rb b/app/services/api/v3/parse_query_params_service.rb index a69cbcf520..aba01ea972 100644 --- a/app/services/api/v3/parse_query_params_service.rb +++ b/app/services/api/v3/parse_query_params_service.rb @@ -56,6 +56,8 @@ module API parsed_params[:highlighted_attributes] = highlighted_attributes_from_params(params) + parsed_params[:display_representation] = params[:displayRepresentation] + parsed_params[:show_hierarchies] = boolearize(params[:showHierarchies]) allow_empty = params.keys + skip_empty diff --git a/app/services/update_query_from_params_service.rb b/app/services/update_query_from_params_service.rb index ba6aa9babc..d7d5687935 100644 --- a/app/services/update_query_from_params_service.rb +++ b/app/services/update_query_from_params_service.rb @@ -49,6 +49,8 @@ class UpdateQueryFromParamsService apply_highlighting(params) + apply_display_representation(params) + disable_hierarchy_when_only_grouped_by(params) if valid_subset @@ -106,6 +108,10 @@ class UpdateQueryFromParamsService query.highlighted_attributes = params[:highlighted_attributes] if params.key?(:highlighted_attributes) end + def apply_display_representation(params) + query.display_representation = params[:display_representation] if params.key?(:display_representation) + end + def disable_hierarchy_when_only_grouped_by(params) if params.key?(:group_by) && !params.key?(:show_hierarchies) query.show_hierarchies = false diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 5858632ff7..96d3a5fc61 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -84,6 +84,8 @@ en: button_open_details: "Open details view" button_close_details: "Close details view" button_open_fullscreen: "Open fullscreen view" + button_show_cards: "Show card view" + button_show_list: "Show list view" button_quote: "Quote" button_save: "Save" button_settings: "Settings" diff --git a/db/migrate/20190716071941_add_display_representation_to_query.rb b/db/migrate/20190716071941_add_display_representation_to_query.rb new file mode 100644 index 0000000000..99b494bb2d --- /dev/null +++ b/db/migrate/20190716071941_add_display_representation_to_query.rb @@ -0,0 +1,5 @@ +class AddDisplayRepresentationToQuery < ActiveRecord::Migration[5.2] + def change + add_column :queries, :display_representation, :text + end +end 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 3cad1195ad..dc1c8352f7 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,15 +30,20 @@ import {KeepTabService} from '../../wp-single-view-tabs/keep-tab/keep-tab.servic import {States} from '../../states.service'; import {WorkPackageTableFocusService} from 'core-components/wp-fast-table/state/wp-table-focus.service'; import {StateService} from '@uirouter/core'; -import {Component} from '@angular/core'; +import {ChangeDetectorRef, Component, OnDestroy, OnInit} 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 { + WorkPackageDisplayRepresentationService, + wpDisplayCardRepresentation +} from "core-components/wp-fast-table/state/work-package-display-representation.service"; @Component({ templateUrl: '../wp-button.template.html', selector: 'wp-details-view-button', }) -export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageButtonComponent { +export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageButtonComponent implements OnInit, OnDestroy { public projectIdentifier:string; public accessKey:number = 8; public activeState:string = 'work-packages.list.details'; @@ -55,7 +60,9 @@ export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageBu readonly I18n:I18nService, public states:States, public wpTableFocus:WorkPackageTableFocusService, - public keepTab:KeepTabService) { + public keepTab:KeepTabService, + public wpDisplayRepresentationService:WorkPackageDisplayRepresentationService, + public cdRef:ChangeDetectorRef) { super(I18n); @@ -63,6 +70,21 @@ export class WorkPackageDetailsViewButtonComponent extends AbstractWorkPackageBu this.deactivateLabel = I18n.t('js.button_close_details'); } + public ngOnInit() { + this.wpDisplayRepresentationService.state.values$() + .pipe( + untilComponentDestroyed(this) + ) + .subscribe(() => { + this.disabled = this.wpDisplayRepresentationService.current === wpDisplayCardRepresentation; + this.cdRef.detectChanges(); + }); + } + + public ngOnDestroy() { + // Nothing to do + } + public get label():string { if (this.isActive()) { return this.deactivateLabel; 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 66e92a9733..1c92bda153 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 @@ -32,6 +32,10 @@ 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 {componentDestroyed, untilComponentDestroyed} from "ng2-rx-componentdestroyed"; +import { + WorkPackageDisplayRepresentationService, + wpDisplayCardRepresentation +} from "core-components/wp-fast-table/state/work-package-display-representation.service"; export interface TimelineButtonText extends ButtonControllerText { zoomOut:string; @@ -64,7 +68,8 @@ export class WorkPackageTimelineButtonComponent extends AbstractWorkPackageButto constructor(readonly I18n:I18nService, readonly cdRef:ChangeDetectorRef, - public wpTableTimeline:WorkPackageTableTimelineService) { + public wpTableTimeline:WorkPackageTableTimelineService, + public wpDisplayRepresentationService:WorkPackageDisplayRepresentationService) { super(I18n); this.activateLabel = I18n.t('js.timelines.button_activate'); @@ -95,6 +100,15 @@ export class WorkPackageTimelineButtonComponent extends AbstractWorkPackageButto this.isMinLevel = current === this.minZoomLevel; this.cdRef.detectChanges(); }); + + this.wpDisplayRepresentationService.state.values$() + .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-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 new file mode 100644 index 0000000000..5901e9c29b --- /dev/null +++ b/frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts @@ -0,0 +1,136 @@ +// -- 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 {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 { + WorkPackageDisplayRepresentationService, wpDisplayCardRepresentation, + wpDisplayListRepresentation +} from "core-components/wp-fast-table/state/work-package-display-representation.service"; +import {untilComponentDestroyed} from "ng2-rx-componentdestroyed"; + + +@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-image2'; + + public inListView:boolean = true; + + public cardLabel:string; + public listLabel:string; + + constructor(readonly $state:StateService, + readonly I18n:I18nService, + readonly cdRef:ChangeDetectorRef, + readonly wpDisplayRepresentationService:WorkPackageDisplayRepresentationService) { + super(I18n); + + this.cardLabel = I18n.t('js.button_card_list'); + this.listLabel = I18n.t('js.button_show_list'); + } + + ngOnInit() { + this.wpDisplayRepresentationService.state.values$() + .pipe( + untilComponentDestroyed(this) + ) + .subscribe(() => { + this.inListView = this.wpDisplayRepresentationService.current !== wpDisplayCardRepresentation; + this.cdRef.detectChanges(); + }); + } + + ngOnDestroy() { + // + } + + public isActive():boolean { + return false; + } + + public performAction(evt:Event):false { + if (this.inListView) { + this.activateCardView(); + } else { + this.activateListView(); + } + + 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/components/wp-card-view/wp-card-view-horizontal.sass b/frontend/src/app/components/wp-card-view/wp-card-view-horizontal.sass new file mode 100644 index 0000000000..b9c1ea5df4 --- /dev/null +++ b/frontend/src/app/components/wp-card-view/wp-card-view-horizontal.sass @@ -0,0 +1,4 @@ +.wp-cards-container.-horizontal + display: grid + grid-template-columns: repeat(auto-fit, minmax(100px, 300px)) + grid-column-gap: 10px diff --git a/frontend/src/app/components/wp-card-view/wp-card-view-vertical.sass b/frontend/src/app/components/wp-card-view/wp-card-view-vertical.sass new file mode 100644 index 0000000000..21d2f7725c --- /dev/null +++ b/frontend/src/app/components/wp-card-view/wp-card-view-vertical.sass @@ -0,0 +1,19 @@ +@import 'helpers' + +.wp-cards-container.-vertical + display: flex + flex-direction: column + // Ensure 100% height for drag & drop area + height: 100% + // Some minor left/right padding + padding: 0 15px + border-radius: 2px + // We let the scrollbar be always there, so that we can align the button above with the card view + // independently of whether we scroll or not. + overflow-y: scroll + @include styled-scroll-bar + + .wp-card + // Take care that the shadow of the last element is still visible + &:last-of-type + margin-bottom: 3px diff --git a/frontend/src/app/components/wp-card-view/wp-card-view.component.html b/frontend/src/app/components/wp-card-view/wp-card-view.component.html index 213f966f63..bb1fdd3eab 100644 --- a/frontend/src/app/components/wp-card-view/wp-card-view.component.html +++ b/frontend/src/app/components/wp-card-view/wp-card-view.component.html @@ -1,5 +1,5 @@
+ [ngClass]="'wp-cards-container -' + orientation">