Merge pull request #7481 from opf/feature/30575-Include-Cards-View-into-work-packages-module

[30575] Include Cards View into work packages module

[ci skip]
pull/7493/head
Oliver Günther 5 years ago committed by GitHub
commit 5e4ec13e3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/contracts/queries/base_contract.rb
  2. 2
      app/services/api/v3/parse_query_params_service.rb
  3. 6
      app/services/update_query_from_params_service.rb
  4. 2
      config/locales/js-en.yml
  5. 5
      db/migrate/20190716071941_add_display_representation_to_query.rb
  6. 28
      frontend/src/app/components/wp-buttons/wp-details-view-button/wp-details-view-button.component.ts
  7. 16
      frontend/src/app/components/wp-buttons/wp-timeline-toggle-button/wp-timeline-toggle-button.component.ts
  8. 136
      frontend/src/app/components/wp-buttons/wp-view-toggle-button/work-package-view-toggle-button.component.ts
  9. 4
      frontend/src/app/components/wp-card-view/wp-card-view-horizontal.sass
  10. 19
      frontend/src/app/components/wp-card-view/wp-card-view-vertical.sass
  11. 2
      frontend/src/app/components/wp-card-view/wp-card-view.component.html
  12. 23
      frontend/src/app/components/wp-card-view/wp-card-view.component.sass
  13. 22
      frontend/src/app/components/wp-card-view/wp-card-view.component.ts
  14. 2
      frontend/src/app/components/wp-fast-table/builders/highlighting/highlighting-mode.const.ts
  15. 73
      frontend/src/app/components/wp-fast-table/state/work-package-display-representation.service.ts
  16. 74
      frontend/src/app/components/wp-grid/wp-grid.component.ts
  17. 12
      frontend/src/app/components/wp-list/wp-states-initialization.service.ts
  18. 9
      frontend/src/app/components/wp-query/url-params-helper.ts
  19. 2
      frontend/src/app/modules/boards/board/configuration-modal/tabs/highlighting-tab.component.html
  20. 2
      frontend/src/app/modules/boards/board/configuration-modal/tabs/highlighting-tab.component.ts
  21. 1
      frontend/src/app/modules/hal/resources/query-resource.ts
  22. 10
      frontend/src/app/modules/work_packages/openproject-work-packages.module.ts
  23. 3
      frontend/src/app/modules/work_packages/query-space/isolated-query-space.ts
  24. 2
      frontend/src/app/modules/work_packages/query-space/wp-isolated-query-space.directive.ts
  25. 6
      frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.sass
  26. 33
      frontend/src/app/modules/work_packages/routing/wp-list/wp-list.component.ts
  27. 14
      frontend/src/app/modules/work_packages/routing/wp-list/wp.list.component.html
  28. 3
      frontend/src/app/modules/work_packages/routing/wp-view-base/work-packages-view.base.ts
  29. 3
      lib/api/v3/queries/query_representer.rb
  30. 7
      lib/api/v3/queries/schemas/query_schema_representer.rb
  31. 4
      modules/boards/spec/features/board_highlighting_spec.rb
  32. 109
      spec/features/work_packages/display_representations/switch_display_representations_spec.rb
  33. 14
      spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb
  34. 14
      spec/services/api/v3/parse_query_params_service_spec.rb
  35. 13
      spec/services/update_query_from_params_service_spec.rb
  36. 50
      spec/support/components/work_packages/display_representation.rb
  37. 6
      spec/support/components/work_packages/table_configuration/highlighting.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

@ -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

@ -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

@ -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"

@ -0,0 +1,5 @@
class AddDisplayRepresentationToQuery < ActiveRecord::Migration[5.2]
def change
add_column :queries, :display_representation, :text
end
end

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

@ -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 {

@ -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: `
<ul class="toolbar-button-group">
<li>
<button class="button"
type="button"
[ngClass]="{ '-active': inListView }"
[disabled]="inListView"
id="wp-view-toggle-button--list"
[attr.title]="listLabel"
[attr.accesskey]="accessKey"
(accessibleClick)="performAction($event)">
<op-icon icon-classes="{{ iconListView }} button--icon"></op-icon>
</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,
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 });

@ -0,0 +1,4 @@
.wp-cards-container.-horizontal
display: grid
grid-template-columns: repeat(auto-fit, minmax(100px, 300px))
grid-column-gap: 10px

@ -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

@ -1,5 +1,5 @@
<div #container
class="wp-cards-container">
[ngClass]="'wp-cards-container -' + orientation">
<div *ngIf="inReference"
class="wp-inline-create--reference-container">
<ndc-dynamic [ndcDynamicComponent]="referenceClass"

@ -1,19 +1,8 @@
@import 'helpers'
.wp-cards-container
.wp-card
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
user-select: none
width: 100%
border: 1px solid var(--widget-box-block-border-color)
@ -31,17 +20,14 @@
&.-new
padding-right: 25px
// Take care that the shadow of the last element is still visible
&:last-of-type
margin-bottom: 3px
.wp-card--content:not(.-new)
display: grid
grid-template-columns: auto 1fr auto
grid-template-rows: auto auto auto
grid-template-rows: auto 1fr auto
grid-row-gap: 10px
grid-template-areas: "type type type" "subject subject subject" "attributeTag avatar idlink"
overflow: hidden
flex-grow: 1
.wp-card--type
grid-area: type
@ -95,3 +81,6 @@ wp-edit-field
margin: -10px -10px 10px
width: calc(100% + 10px + 10px)
max-width: calc(100% + 10px + 10px)
flex-basis: 200px
object-fit: cover

@ -37,9 +37,11 @@ import {PathHelperService} from "core-app/modules/common/path-helper/path-helper
import {filter} from 'rxjs/operators';
import {CausedUpdatesService} from "core-app/modules/boards/board/caused-updates/caused-updates.service";
export type CardViewOrientation = 'horizontal'|'vertical';
@Component({
selector: 'wp-card-view',
styleUrls: ['./wp-card-view.component.sass'],
styleUrls: ['./wp-card-view.component.sass', './wp-card-view-horizontal.sass', './wp-card-view-vertical.sass'],
templateUrl: './wp-card-view.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
@ -49,8 +51,16 @@ export class WorkPackageCardViewComponent implements OnInit {
@Input() public highlightingMode:CardHighlightingMode;
@Input() public workPackageAddedHandler:(wp:WorkPackageResource) => Promise<unknown>;
@Input() public showStatusButton:boolean = true;
@Input() public orientation:CardViewOrientation = 'vertical';
/** Whether cards are removable */
@Input() public cardsRemovable:boolean = false;
/** Container reference */
@ViewChild('container') public container:ElementRef;
@Output() onMoved = new EventEmitter<void>();
public trackByHref = AngularTrackingHelpers.trackByHref;
public trackByHref = AngularTrackingHelpers.trackByHrefAndProperty('lockVersion');
public query:QueryResource;
private _workPackages:WorkPackageResource[];
public columns:QueryColumn[];
@ -71,12 +81,6 @@ export class WorkPackageCardViewComponent implements OnInit {
onReferenced: (wp:WorkPackageResource) => this.addWorkPackageToQuery(wp, 0)
};
/** Whether cards are removable */
@Input() public cardsRemovable:boolean = false;
/** Container reference */
@ViewChild('container') public container:ElementRef;
/** Whether the card view has an active inline created wp */
public activeInlineCreateWp?:WorkPackageResource;
@ -209,6 +213,8 @@ export class WorkPackageCardViewComponent implements OnInit {
const newOrder = this.reorderService.move(this.currentOrder, wpId, toIndex);
this.updateOrder(newOrder);
this.onMoved.emit();
},
onRemoved: (card:HTMLElement) => {
const wpId:string = card.dataset.workPackageId!;

@ -1,3 +1,3 @@
export type HighlightingMode = 'status'|'priority'|'type'|'inline'|'none';
export type CardHighlightingMode = 'priority'|'type'|'none'|'entire-card';
export type CardHighlightingMode = 'priority'|'type'|'none'|'inline';

@ -0,0 +1,73 @@
// -- 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 {QueryResource} from 'core-app/modules/hal/resources/query-resource';
import {WorkPackageQueryStateService} from './wp-table-base.service';
import {States} from 'core-components/states.service';
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {Injectable} from '@angular/core';
import {InputState} from "reactivestates";
export const wpDisplayListRepresentation:string = 'list';
export const wpDisplayCardRepresentation:string = 'card';
export type wpDisplayRepresentation = 'list'|'card';
@Injectable()
export class WorkPackageDisplayRepresentationService extends WorkPackageQueryStateService<string|null> {
public constructor(readonly states:States,
readonly querySpace:IsolatedQuerySpace) {
super(querySpace);
}
public get state():InputState<string|null> {
return this.querySpace.displayRepresentation;
}
public hasChanged(query:QueryResource) {
return this.current !== query.displayRepresentation;
}
valueFromQuery(query:QueryResource) {
return query.displayRepresentation || null;
}
public applyToQuery(query:QueryResource) {
const current = this.current;
query.displayRepresentation = current === null ? undefined : current;
return true;
}
public get current():string|null {
return this.state.getValueOr(null);
}
public setDisplayRepresentation(representation:string) {
this.update(representation);
}
}

@ -0,0 +1,74 @@
// -- 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 {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from "@angular/core";
import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table/state/wp-table-highlighting.service";
import {CardViewOrientation} from "core-components/wp-card-view/wp-card-view.component";
import {takeUntil} from "rxjs/operators";
import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/isolated-query-space";
import {HighlightingMode} from "core-components/wp-fast-table/builders/highlighting/highlighting-mode.const";
import {componentDestroyed, untilComponentDestroyed} from "ng2-rx-componentdestroyed";
@Component({
selector: 'wp-grid',
template: `
<wp-card-view [dragOutOfHandler]="canDragOutOf"
[dragInto]="false"
[cardsRemovable]="false"
[highlightingMode]="highlightingMode"
[showStatusButton]="true"
[orientation]="gridOrientation">
</wp-card-view>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkPackagesGridComponent implements OnInit, OnDestroy {
public canDragOutOf = () => { return false; };
public gridOrientation:CardViewOrientation = 'horizontal';
public highlightingMode:HighlightingMode = 'none';
constructor(readonly wpTableHighlight:WorkPackageTableHighlightingService,
readonly querySpace:IsolatedQuerySpace,
readonly cdRef:ChangeDetectorRef) {
}
ngOnInit() {
this.querySpace.highlighting.values$()
.pipe(
takeUntil(this.querySpace.stopAllSubscriptions),
untilComponentDestroyed(this)
)
.subscribe(() => {
this.highlightingMode = this.wpTableHighlight.current.mode;
this.cdRef.detectChanges();
});
}
ngOnDestroy():void {
}
}

@ -20,6 +20,7 @@ import {IsolatedQuerySpace} from "core-app/modules/work_packages/query-space/iso
import {Injectable} from '@angular/core';
import {QuerySchemaResource} from 'core-app/modules/hal/resources/query-schema-resource';
import {WorkPackageTableHighlightingService} from "core-components/wp-fast-table/state/wp-table-highlighting.service";
import {WorkPackageDisplayRepresentationService} from "core-components/wp-fast-table/state/work-package-display-representation.service";
@Injectable()
export class WorkPackageStatesInitializationService {
@ -38,7 +39,8 @@ export class WorkPackageStatesInitializationService {
protected wpTableAdditionalElements:WorkPackageTableAdditionalElementsService,
protected wpCacheService:WorkPackageCacheService,
protected wpListChecksumService:WorkPackagesListChecksumService,
protected authorisationService:AuthorisationService) {
protected authorisationService:AuthorisationService,
protected wpDisplayRepresentation:WorkPackageDisplayRepresentationService) {
}
/**
@ -92,6 +94,12 @@ export class WorkPackageStatesInitializationService {
this.states.schemas.get(schema.href as string).putValue(schema);
});
}
// Ensure we're setting the current results to the query
// This is due to 9.X still loading results from /api/v3/work_packages
// for a previously loaded query.
query.results = results;
this.querySpace.query.putValue(query);
this.querySpace.rows.putValue(results.elements);
@ -126,6 +134,7 @@ export class WorkPackageStatesInitializationService {
this.wpTableTimeline.initialize(query, results);
this.wpTableHierarchies.initialize(query, results);
this.wpTableHighlighting.initialize(query, results);
this.wpDisplayRepresentation.initialize(query, results);
this.authorisationService.initModelAuth('query', query.$links);
this.authorisationService.initModelAuth('work_packages', results.$links);
@ -140,6 +149,7 @@ export class WorkPackageStatesInitializationService {
this.wpTableTimeline.applyToQuery(query);
this.wpTableHighlighting.applyToQuery(query);
this.wpTableHierarchies.applyToQuery(query);
this.wpDisplayRepresentation.applyToQuery(query);
}
public clearStates() {

@ -77,6 +77,7 @@ export class UrlParamsHelperService {
paramsData = this.encodeFilters(paramsData, query.filters);
paramsData.pa = additional.page;
paramsData.pp = additional.perPage;
paramsData.dr = query.displayRepresentation;
return JSON.stringify(paramsData);
}
@ -187,6 +188,10 @@ export class UrlParamsHelperService {
}
}
if (properties.dr) {
queryData.displayRepresentation = properties.dr;
}
if (properties.hl) {
queryData.highlightingMode = properties.hl;
}
@ -258,6 +263,10 @@ export class UrlParamsHelperService {
queryData.highlightedAttributes = query.highlightedAttributes.map(el => el.href);
}
if (query.displayRepresentation) {
queryData.displayRepresentation = query.displayRepresentation;
}
queryData.showHierarchies = !!query.showHierarchies;
queryData.groupBy = _.get(query.groupBy, 'id', '');

@ -7,7 +7,7 @@
<label class="option-label">
<input type="radio"
[(ngModel)]="entireCardMode"
(change)="updateMode('entire-card')"
(change)="updateMode('inline')"
[value]="true"
name="entire_card_switch">
<span [textContent]="text.highlighting_mode.entire_card_by"></span>

@ -46,7 +46,7 @@ export class BoardHighlightingTabComponent implements TabComponent {
}
public updateMode(mode:CardHighlightingMode) {
if (mode === 'entire-card') {
if (mode === 'inline') {
this.highlightingMode = this.lastEntireCardAttribute;
} else {
this.highlightingMode = mode;

@ -66,6 +66,7 @@ export class QueryResource extends HalResource {
public timelineZoomLevel:TimelineZoomLevel;
public highlightingMode:HighlightingMode;
public highlightedAttributes:HalResource[]|undefined;
public displayRepresentation:string|undefined;
public timelineLabels:TimelineLabels;
public showHierarchies:boolean;
public public:boolean;

@ -156,6 +156,9 @@ import {WorkPackageDmService} from "core-app/modules/hal/dm-services/work-packag
import {WorkPackageRelationsService} from "core-components/wp-relations/wp-relations.service";
import {OpenprojectBcfModule} from "core-app/modules/bcf/openproject-bcf.module";
import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-relations-create/wp-relations-autocomplete/wp-relations-autocomplete.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";
@NgModule({
imports: [
@ -237,6 +240,8 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
WorkPackageInlineCreateComponent,
WpRelationInlineAddExistingComponent,
WorkPackagesGridComponent,
WorkPackagesTableController,
WorkPackageTablePaginationComponent,
@ -361,6 +366,7 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
// Card view
WorkPackageCardViewComponent,
WorkPackageViewToggleButton,
],
entryComponents: [
// Split view
@ -380,9 +386,13 @@ import {WorkPackageRelationsAutocomplete} from "core-components/wp-relations/wp-
// Inline create
WpRelationInlineAddExistingComponent,
// View representations
WorkPackagesBaseComponent,
WorkPackagesListComponent,
WorkPackagesGridComponent,
// WP new
WorkPackageNewFullViewComponent,
WorkPackageNewSplitViewComponent,

@ -20,6 +20,7 @@ import {QueryFilterInstanceResource} from "core-app/modules/hal/resources/query-
import {QueryGroupByResource} from "core-app/modules/hal/resources/query-group-by-resource";
import {QuerySortByResource} from "core-app/modules/hal/resources/query-sort-by-resource";
import {WorkPackageTableRefreshRequest} from "core-components/wp-table/wp-table-refresh-request.service";
import {wpDisplayRepresentation} from "core-components/wp-fast-table/state/work-package-display-representation.service";
@Injectable()
export class IsolatedQuerySpace extends StatesGroup {
@ -63,6 +64,8 @@ export class IsolatedQuerySpace extends StatesGroup {
hierarchies = input<WorkPackageTableHierarchies>();
// Highlighting mode
highlighting = input<WorkPackageTableHighlight>();
// Display of the WP results
displayRepresentation = input<wpDisplayRepresentation|null>();
// State to be updated when the table is up to date
rendered = input<RenderedRow[]>();

@ -60,6 +60,7 @@ import {WpRelationInlineCreateService} from "core-components/wp-relations/embedd
import {WorkPackagesListChecksumService} from "core-components/wp-list/wp-list-checksum.service";
import {debugLog} from "core-app/helpers/debug_output";
import {PortalCleanupService} from "core-app/modules/fields/display/display-portal/portal-cleanup.service";
import {WorkPackageDisplayRepresentationService} from "core-components/wp-fast-table/state/work-package-display-representation.service";
/**
* Directive to open a work package query 'space', an isolated injector hierarchy
@ -93,6 +94,7 @@ import {PortalCleanupService} from "core-app/modules/fields/display/display-port
WorkPackageTableAdditionalElementsService,
WorkPackageTableFocusService,
WorkPackageTableHighlightingService,
WorkPackageDisplayRepresentationService,
WorkPackageService,
WorkPackageRelationsHierarchyService,
WorkPackageFiltersService,

@ -4,3 +4,9 @@
@include overlay-background
background-color: #FFFFFF
opacity: 0.5
.work-packages--card-view-container
width: 100%
overflow: auto
padding-bottom: 5px
@include styled-scroll-bar

@ -32,11 +32,18 @@ import {QueryResource} from 'core-app/modules/hal/resources/query-resource';
import {OpTitleService} from "core-components/html/op-title.service";
import {WorkPackagesViewBase} from "core-app/modules/work_packages/routing/wp-view-base/work-packages-view.base";
import {take} from "rxjs/operators";
import {DragAndDropService} from "core-app/modules/boards/drag-and-drop/drag-and-drop.service";
import {CausedUpdatesService} from "core-app/modules/boards/board/caused-updates/caused-updates.service";
import {wpDisplayCardRepresentation} from "core-components/wp-fast-table/state/work-package-display-representation.service";
@Component({
selector: 'wp-list',
templateUrl: './wp.list.component.html',
styleUrls: ['./wp-list.component.sass']
styleUrls: ['./wp-list.component.sass'],
providers: [
DragAndDropService,
CausedUpdatesService,
]
})
export class WorkPackagesListComponent extends WorkPackagesViewBase implements OnDestroy {
text = {
@ -68,6 +75,9 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
/** An overlay over the table shown for example when the filters are invalid */
showResultOverlay = false;
/** Switch between list and card view */
private _showListView:boolean = true;
private readonly titleService:OpTitleService = this.injector.get(OpTitleService);
ngOnInit() {
@ -90,12 +100,19 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
}
});
// Update the title whenever the query changes
this.querySpace.query.values$().pipe(
untilComponentDestroyed(this)
).subscribe((query) => {
// Update the title whenever the query changes
this.updateTitle(query);
this.currentQuery = query;
// Update the visible representation
if (this.wpDisplayRepresentation.valueFromQuery(query) === wpDisplayCardRepresentation) {
this.showListView = false;
} else {
this.showListView = true;
}
});
}
@ -165,6 +182,14 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
this.showResultOverlay = !completed;
}
public set showListView(val:boolean) {
this._showListView = val;
}
public get showListView():boolean {
return this._showListView;
}
protected updateQueryOnParamsChanges() {
// Listen for param changes
this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => {
@ -203,6 +228,8 @@ export class WorkPackagesListComponent extends WorkPackagesViewBase implements O
return this.loadingIndicator =
this.wpListService
.loadCurrentQueryFromParams(this.projectIdentifier)
.then(() => this.querySpace.rendered.valuesPromise());
.then(() => {
this.querySpace.rendered.valuesPromise();
});
}
}

@ -25,6 +25,10 @@
<wp-details-view-button>
</wp-details-view-button>
</li>
<li class="toolbar-item hidden-for-mobile">
<wp-view-toggle-button>
</wp-view-toggle-button>
</li>
<li class="toolbar-item hidden-for-mobile">
<wp-timeline-toggle-button>
</wp-timeline-toggle-button>
@ -62,13 +66,19 @@
<div class="work-packages-split-view--tabletimeline-side loading-indicator--location"
data-indicator-name="table">
<div class="result-overlay"
*ngIf="showResultOverlay"></div>
*ngIf="showResultOverlay && showListView"></div>
<!-- TABLE + TIMELINE horizontal split -->
<wp-table *ngIf="tableInformationLoaded"
<wp-table *ngIf="tableInformationLoaded && showListView"
[projectIdentifier]="projectIdentifier"
class="work-packages-split-view--tabletimeline-content"></wp-table>
<!-- GRID representation of the WP -->
<div *ngIf="!showListView"
class="work-packages--card-view-container">
<wp-grid></wp-grid>
</div>
<!-- Footer -->
<div class="work-packages-split-view--tabletimeline-footer hide-when-print"
*ngIf="tableInformationLoaded">

@ -56,6 +56,7 @@ import {WorkPackageQueryStateService} from "core-components/wp-fast-table/state/
import {debugLog} from "core-app/helpers/debug_output";
import {QueryDmService} from "core-app/modules/hal/dm-services/query-dm.service";
import {WorkPackageStatesInitializationService} from "core-components/wp-list/wp-states-initialization.service";
import {WorkPackageDisplayRepresentationService} from "core-components/wp-fast-table/state/work-package-display-representation.service";
export abstract class WorkPackagesViewBase implements OnInit, OnDestroy {
@ -81,6 +82,7 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy {
readonly wpStaticQueries:WorkPackageStaticQueriesService = this.injector.get(WorkPackageStaticQueriesService);
readonly QueryDm:QueryDmService = this.injector.get(QueryDmService);
readonly wpStatesInitialization:WorkPackageStatesInitializationService = this.injector.get(WorkPackageStatesInitializationService);
readonly wpDisplayRepresentation:WorkPackageDisplayRepresentationService = this.injector.get(WorkPackageDisplayRepresentationService);
constructor(protected injector:Injector) {
}
@ -124,6 +126,7 @@ export abstract class WorkPackagesViewBase implements OnInit, OnDestroy {
this.setupChangeObserver(this.wpTableHierarchies);
this.setupChangeObserver(this.wpTableColumns);
this.setupChangeObserver(this.wpTableHighlighting);
this.setupChangeObserver(this.wpDisplayRepresentation);
}
/**

@ -300,6 +300,9 @@ module API
property :timeline_labels
# Visible representation of the results
property :display_representation
# Highlighting properties
property :highlighting_mode,
render_nil: false

@ -138,6 +138,13 @@ module API
has_default: true,
visibility: false
schema :display_representation,
type: 'String',
required: false,
writable: true,
has_default: true,
visibility: false
schema :show_hierarchies,
type: 'Boolean',
required: false,

@ -87,12 +87,12 @@ describe 'Work Package boards spec', type: :feature, js: true do
expect(page).to have_selector('.__hl_inline_type_' + type2.id.to_s)
# Highlight whole card by priority
board_page.change_board_highlighting 'entire-card', 'Priority'
board_page.change_board_highlighting 'inline', 'Priority'
expect(page).to have_selector('.__hl_background_priority_' + priority.id.to_s)
expect(page).to have_selector('.__hl_background_priority_' + priority2.id.to_s)
# Highlight whole card by type
board_page.change_board_highlighting 'entire-card', 'Type'
board_page.change_board_highlighting 'inline', 'Type'
expect(page).to have_selector('.__hl_background_type_' + type.id.to_s)
expect(page).to have_selector('.__hl_background_type_' + type2.id.to_s)

@ -0,0 +1,109 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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'
describe 'Work package timeline navigation',
with_ee: %i[conditional_highlighting],
js: true do
let(:user) { FactoryBot.create(:admin) }
let(:project) { FactoryBot.create(:project) }
let(:wp_table) { Pages::WorkPackagesTable.new(project) }
let(:highlighting) { ::Components::WorkPackages::Highlighting.new }
let(:display_representation) { ::Components::WorkPackages::DisplayRepresentation.new }
let(:priority1) { FactoryBot.create :issue_priority, color: FactoryBot.create(:color, hexcode: '#123456') }
let(:priority2) { FactoryBot.create :issue_priority, color: FactoryBot.create(:color, hexcode: '#332211') }
let(:status) { FactoryBot.create :status, color: FactoryBot.create(:color, hexcode: '#654321') }
let(:wp_1) do
FactoryBot.create :work_package,
project: project,
priority: priority1,
status: status
end
let(:wp_2) do
FactoryBot.create :work_package,
project: project,
priority: priority2,
status: status
end
before do
wp_1
wp_2
allow(EnterpriseToken).to receive(:show_banners?).and_return(false)
login_as(user)
wp_table.visit!
wp_table.expect_work_package_listed wp_1, wp_2
# Enable card representation
display_representation.switch_to_card_layout
expect(page).to have_selector(".wp-card[data-work-package-id='#{wp_1.id}']")
expect(page).to have_selector(".wp-card[data-work-package-id='#{wp_2.id}']")
end
it 'can switch the representations and keep the configuration settings' do
# Enable highlighting
highlighting.switch_entire_row_highlight "Priority"
within ".wp-card[data-work-package-id='#{wp_1.id}']" do
expect(page).to have_selector(".wp-card--highlighting.__hl_background_priority_#{priority1.id}")
end
within ".wp-card[data-work-package-id='#{wp_2.id}']" do
expect(page).to have_selector(".wp-card--highlighting.__hl_background_priority_#{priority2.id}")
end
# Switch back to list representation & Highlighting is kept
display_representation.switch_to_list_layout
wp_table.expect_work_package_listed wp_1, wp_2
expect(page).to have_selector("#{wp_table.row_selector(wp_1)}.__hl_background_priority_#{priority1.id}")
expect(page).to have_selector("#{wp_table.row_selector(wp_2)}.__hl_background_priority_#{priority2.id}")
# Change attribute
highlighting.switch_entire_row_highlight "Status"
expect(page).to have_selector("#{wp_table.row_selector(wp_1)}.__hl_background_status_#{status.id}")
expect(page).to have_selector("#{wp_table.row_selector(wp_2)}.__hl_background_status_#{status.id}")
# Switch back to card representation & Highlighting is kept, too
display_representation.switch_to_card_layout
within ".wp-card[data-work-package-id='#{wp_1.id}']" do
expect(page).to have_selector(".wp-card--highlighting.__hl_background_status_#{status.id}")
end
within ".wp-card[data-work-package-id='#{wp_2.id}']" do
expect(page).to have_selector(".wp-card--highlighting.__hl_background_status_#{status.id}")
end
end
it 'saves the representation in the query' do
# After refresh the WP are still disaplyed as cards
page.driver.browser.navigate.refresh
expect(page).to have_selector(".wp-card[data-work-package-id='#{wp_1.id}']")
expect(page).to have_selector(".wp-card[data-work-package-id='#{wp_2.id}']")
end
end

@ -336,6 +336,20 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do
it_behaves_like 'has no visibility property'
end
describe 'display_representation' do
let(:path) { 'displayRepresentation' }
it_behaves_like 'has basic schema properties' do
let(:type) { 'String' }
let(:name) { Query.human_attribute_name('display_representation') }
let(:required) { false }
let(:writable) { true }
let(:has_default) { true }
end
it_behaves_like 'has no visibility property'
end
describe 'columns' do
let(:path) { 'columns' }

@ -142,6 +142,20 @@ describe ::API::V3::ParseQueryParamsService,
end
end
context 'with display_representation' do
it_behaves_like 'transforms' do
let(:params) { { displayRepresentation: 'cards' } }
let(:expected) { { display_representation:'cards' } }
end
end
context 'without display_representation' do
it_behaves_like 'transforms' do
let(:params) { { displayRepresentation: nil } }
let(:expected) { {} }
end
end
context 'with sort' do
context 'as sortBy in comma separated value' do
it_behaves_like 'transforms' do

@ -112,6 +112,19 @@ describe UpdateQueryFromParamsService,
end
end
context 'display representation' do
let(:params) do
{ display_representation: 'list' }
end
it 'sets the display_representation' do
subject
expect(query.display_representation)
.to eq('list')
end
end
context 'highlighting mode', with_ee: %i[conditional_highlighting] do
let(:params) do
{ highlighting_mode: 'status' }

@ -0,0 +1,50 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2018 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-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.
#++
module Components
module WorkPackages
class DisplayRepresentation
include Capybara::DSL
include RSpec::Matchers
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
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
end
end
end
end

@ -72,7 +72,11 @@ module Components
def open_modal
@opened = true
::Components::WorkPackages::TableConfigurationModal.new.open_and_switch_to 'Highlighting'
::Components::WorkPackages::SettingsMenu.new.open_and_choose 'Configure view'
retry_block do
find(".tab-show", text: 'Highlighting', wait: 10).click
end
end
def assume_opened

Loading…
Cancel
Save