From ed1a0c87de46b917c43c465ab7313c7aaa76afdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 18 Aug 2017 15:26:22 +0200 Subject: [PATCH] [25999] Make timeline labels configurable https://community.openproject.com/wp/25999 --- .../work_packages/timelines/_timelines.sass | 1 + .../timelines/elements/_bar.sass | 49 ------ .../timelines/elements/_labels.sass | 80 +++++++++ .../timelines/elements/_milestone.sass | 37 +---- app/contracts/queries/base_contract.rb | 1 + app/models/query/timelines.rb | 10 ++ .../update_query_from_params_service.rb | 1 + config/boot.rb | 2 +- config/locales/en.yml | 1 + config/locales/js-en.yml | 11 ++ ...0818063404_add_timeline_labels_to_query.rb | 5 + docs/api/apiv3/endpoints/queries.apib | 12 +- .../hal-resources/query-resource.service.ts | 8 + .../hal-resources/schema-resource.service.ts | 4 + .../settings-menu/settings-menu.controller.ts | 9 + .../settings-menu/settings-menu.service.html | 9 + .../timelines-modal.controller.ts | 75 +++++++++ .../timelines-modal.service.html | 45 +++++ .../timelines-modal.service.ts | 40 +++++ .../wp-edit-form/display-field-renderer.ts | 19 ++- .../state/wp-table-columns.service.ts | 6 + .../state/wp-table-timeline.service.ts | 42 ++++- .../wp-fast-table/wp-table-timeline.ts | 32 ++-- .../components/wp-query/url-params-helper.ts | 7 + .../timeline/cells/timeline-cell-renderer.ts | 125 ++++++++------ .../cells/timeline-milestone-cell-renderer.ts | 74 +++------ .../cells/wp-timeline-cell-mouse-handler.ts | 12 +- .../timeline/cells/wp-timeline-cell.ts | 16 +- ...ages-helper.js => work-packages-helper.ts} | 31 ++-- .../options-dropdown-menu-controller-test.js | 3 + lib/api/v3/queries/query_representer.rb | 2 + .../schemas/query_schema_representer.rb | 7 + .../timeline/timeline_labels_spec.rb | 156 ++++++++++++++++++ .../query_representer_generation_spec.rb | 19 +++ .../schemas/query_schema_representer_spec.rb | 14 ++ spec/models/query_spec.rb | 11 ++ .../timelines/configuration_modal.rb | 66 ++++++++ .../components/timelines/timeline_row.rb | 76 +++++++++ spec/support/pages/work_packages_timeline.rb | 6 +- 39 files changed, 890 insertions(+), 234 deletions(-) create mode 100644 app/assets/stylesheets/content/work_packages/timelines/elements/_labels.sass create mode 100644 db/migrate/20170818063404_add_timeline_labels_to_query.rb create mode 100644 frontend/app/components/modals/timelines-modal/timelines-modal.controller.ts create mode 100644 frontend/app/components/modals/timelines-modal/timelines-modal.service.html create mode 100644 frontend/app/components/modals/timelines-modal/timelines-modal.service.ts rename frontend/app/work_packages/helpers/{work-packages-helper.js => work-packages-helper.ts} (82%) create mode 100644 spec/features/work_packages/timeline/timeline_labels_spec.rb create mode 100644 spec/support/components/timelines/configuration_modal.rb create mode 100644 spec/support/components/timelines/timeline_row.rb diff --git a/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass b/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass index 2fd3a9c228..7cb1c86aee 100644 --- a/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass +++ b/app/assets/stylesheets/content/work_packages/timelines/_timelines.sass @@ -3,6 +3,7 @@ @import 'elements/_hover' @import 'elements/_milestone' @import 'elements/_relation' +@import 'elements/_labels' @import '_timelines_header' @import '_timelines_static_elements' diff --git a/app/assets/stylesheets/content/work_packages/timelines/elements/_bar.sass b/app/assets/stylesheets/content/work_packages/timelines/elements/_bar.sass index 4255a305a5..2717142025 100644 --- a/app/assets/stylesheets/content/work_packages/timelines/elements/_bar.sass +++ b/app/assets/stylesheets/content/work_packages/timelines/elements/_bar.sass @@ -30,55 +30,6 @@ max-width: 20% height: 100% - .labelLeft - pointer-events: none - //display: inline-block - display: none - padding: 2px 5px - top: 0 - white-space: nowrap - position: absolute - left: -85px - width: 75px - background-color: white - border: 1px solid #d4d4d4 - border-radius: 5px - height: 16px - font-size: 12px - - .containerRight - pointer-events: none - display: inline-block - position: absolute - margin: 0 0 0 0 - padding: 0 0 0 0 - top: -2px - left: 100% - white-space: nowrap - height: 16px - - .labelRight.not-empty - pointer-events: none - //display: inline-block - display: none - background-color: white - border: 1px solid #d4d4d4 - border-radius: 5px - padding: 2px 5px 2px 5px - margin-left: 10px - font-size: 12px - height: 16px - - .labelFarRight - pointer-events: none - height: 16px - display: inline-block - white-space: nowrap - font-style: italic - margin-left: 5px - padding: 5px 5px 2px 5px - font-size: 13px - &.-readonly cursor: not-allowed !important diff --git a/app/assets/stylesheets/content/work_packages/timelines/elements/_labels.sass b/app/assets/stylesheets/content/work_packages/timelines/elements/_labels.sass new file mode 100644 index 0000000000..1dd250028b --- /dev/null +++ b/app/assets/stylesheets/content/work_packages/timelines/elements/_labels.sass @@ -0,0 +1,80 @@ +.timeline-element + + // Label style + .-label-style.not-empty + background-color: white + border: 1px solid #d4d4d4 + border-radius: 5px + height: 16px + font-size: 12px + padding: 2px 5px + + .labelLeft.not-empty, + .labelHoverLeft.not-empty + pointer-events: none + top: 0 + white-space: nowrap + // Position container left of bar + position: absolute + left: 0px + // Then translate by its own width + some margin + transform: translateX(calc(-100% - 10px)) + font-size: 12px + + .containerRight + pointer-events: none + display: inline-block + position: absolute + margin: 0 0 0 0 + padding: 0 0 0 0 + top: -2px + left: 100% + white-space: nowrap + line-height: 10px + font-size: 12px + height: 16px + + .labelRight.not-empty + display: inline-block + pointer-events: none + margin-left: 20px + font-size: 12px + + .labelFarRight + height: 16px + display: inline-block + white-space: nowrap + font-style: italic + margin-left: 15px + padding: 5px 5px 2px 5px + font-size: 13px + + // label hover right needs different position + // since its not part of containerRight + .labelHoverRight + pointer-events: none + display: none + top: 0 + white-space: nowrap + // Position container right of bar + position: absolute + right: 0px + // Then translate by its own width + some margin + transform: translateX(calc(100% + 10px)) + font-size: 12px + + &.-editable + cursor: ew-resize + + .show-on-hover + display: none + +// Hide or show elements on hover +.wp-timeline-cell.row-hovered + .show-on-hover + display: inline-block + + .hide-on-hover + display: none + + diff --git a/app/assets/stylesheets/content/work_packages/timelines/elements/_milestone.sass b/app/assets/stylesheets/content/work_packages/timelines/elements/_milestone.sass index eea174d484..5344e8a925 100644 --- a/app/assets/stylesheets/content/work_packages/timelines/elements/_milestone.sass +++ b/app/assets/stylesheets/content/work_packages/timelines/elements/_milestone.sass @@ -7,42 +7,13 @@ transform: rotate(45deg) transform-origin: center center - .containerRight - pointer-events: none - display: inline-block - position: absolute - margin: 0 0 0 0 - padding: 0 0 0 0 - top: -2px - left: 100% - white-space: nowrap - line-height: 10px - height: 16px - - .labelRight.not-empty - pointer-events: none - display: none - //display: inline-block - background-color: white - border: 1px solid #d4d4d4 - border-radius: 5px - padding: 2px 5px 2px 5px - margin-left: 20px - margin-right: -15px - font-size: 12px - - .labelFarRight - height: 16px - display: inline-block - white-space: nowrap - font-style: italic - margin-left: 15px - padding: 5px 5px 2px 5px - font-size: 13px - &.-editable cursor: ew-resize + // Add more margin to hover right + .labelHoverRight + transform: translateX(calc(100% + 20px)) + .active-selection-mode .timeline-element.milestone diff --git a/app/contracts/queries/base_contract.rb b/app/contracts/queries/base_contract.rb index f6ec60893c..973f090cb4 100644 --- a/app/contracts/queries/base_contract.rb +++ b/app/contracts/queries/base_contract.rb @@ -39,6 +39,7 @@ module Queries attribute :display_sums # => sums attribute :timeline_visible attribute :timeline_zoom_level + attribute :timeline_labels attribute :show_hierarchies attribute :column_names # => columns diff --git a/app/models/query/timelines.rb b/app/models/query/timelines.rb index 0d26e871dd..532244e7f4 100644 --- a/app/models/query/timelines.rb +++ b/app/models/query/timelines.rb @@ -33,5 +33,15 @@ module Query::Timelines included do enum timeline_zoom_level: %i(days weeks months quarters years) validates :timeline_zoom_level, inclusion: { in: timeline_zoom_levels.keys } + + serialize :timeline_labels, Hash + validate :valid_timeline_labels + + def valid_timeline_labels + return unless timeline_labels.present? + + valid_keys = %w(farRight left right) == timeline_labels.keys.map(&:to_s).sort + errors.add :timeline_labels, :invalid unless valid_keys + end end end diff --git a/app/services/update_query_from_params_service.rb b/app/services/update_query_from_params_service.rb index 8fa1c63b6a..a8275103d1 100644 --- a/app/services/update_query_from_params_service.rb +++ b/app/services/update_query_from_params_service.rb @@ -87,6 +87,7 @@ class UpdateQueryFromParamsService def apply_timeline(params) query.timeline_visible = params[:timeline_visible] if params.key?(:timeline_visible) query.timeline_zoom_level = params[:timeline_zoom_level] if params.key?(:timeline_zoom_level) + query.timeline_labels = params[:timeline_labels] if params.key?(:timeline_labels) end def apply_hierarchy(params) diff --git a/config/boot.rb b/config/boot.rb index 98e12b90b1..52e46e7009 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -40,7 +40,7 @@ require 'bundler/setup' # Set up gems listed in the Gemfile. # Rails is not yet loaded here if ENV['RAILS_ENV'] == 'development' - $stderr.puts "Starting with bootstnap." + $stderr.puts "Starting with bootsnap." require 'bootsnap' diff --git a/config/locales/en.yml b/config/locales/en.yml index e0e16e6bc6..36181fd00b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -368,6 +368,7 @@ en: relations_of_type_column: "%{type} relations" group_by: "Group results by" filters: "Filters" + timeline_labels: "Timeline labels" repository: url: "URL" role: diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index c36dc60eef..02ce1c2d25 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -342,6 +342,17 @@ en: text_query_destroy_confirmation: "Are you sure you want to delete the selected query?" text_attachment_destroy_confirmation: "Are you sure you want to delete the attachment?" timelines: + gantt_chart: 'Gantt chart' + labels: + bar: 'Bar labels' + left: 'Left' + right: 'Right' + farRight: 'Far right' + showNone: '-- No label --' + description: > + Select the attributes you want to be shown in the respective positions of the gantt chart at all times. + Note that when hovering an element, its date labels will be shown instead of these attributes. + button_activate: 'Activate timeline mode' button_deactivate: 'Deactivate timeline mode' cancel: Cancel diff --git a/db/migrate/20170818063404_add_timeline_labels_to_query.rb b/db/migrate/20170818063404_add_timeline_labels_to_query.rb new file mode 100644 index 0000000000..07f15589d5 --- /dev/null +++ b/db/migrate/20170818063404_add_timeline_labels_to_query.rb @@ -0,0 +1,5 @@ +class AddTimelineLabelsToQuery < ActiveRecord::Migration[5.0] + def change + add_column :queries, :timeline_labels, :text + end +end diff --git a/docs/api/apiv3/endpoints/queries.apib b/docs/api/apiv3/endpoints/queries.apib index a7843a701e..0d1a55f93b 100644 --- a/docs/api/apiv3/endpoints/queries.apib +++ b/docs/api/apiv3/endpoints/queries.apib @@ -35,6 +35,7 @@ Please note, that all the properties listed above will also be embedded when ind | filters | A set of QueryFilters which will be applied to the work packages to determine the resulting work packages | []QueryFilterInstance | | READ | | sums | Should sums (of supported properties) be shown? | Boolean | | READ | | timelineVisible | Should the timeline mode be shown? | Boolean | | READ | +| timelineLabels | Which labels are shown in the timeline, empty when default | QueryTimelineLabels | | READ | | timelineZoomLevel| Which zoom level should the timeline be rendered in? | String | days, weeks, months, quarters, years | READ | | showHierarchies | Should the hierarchy mode be enabled? | Boolean | | READ | | public | Can users besides the owner see the query? | Boolean | | READ | @@ -102,7 +103,7 @@ If the values are nonprimitive (e.g. User, Project), they will be listed as obje ``` -## Query [/api/v3/queries/{id}{?offset,pageSize,filters,sortBy,groupBy,showSums,timelineVisible,timelineZoomLevel,showHierarchies}] +## Query [/api/v3/queries/{id}{?offset,pageSize,filters,sortBy,groupBy,showSums,timelineVisible,timelineLabels,timelineZoomLevel,showHierarchies}] + Model + Body @@ -261,6 +262,7 @@ Retreive an individual query as identified by the id parameter. Then end point a + groupBy (optional, string, `status`) ... The column to group by. The grouping criteria is applied to the to the querie's result collection of work packages overriding the query's persisted group criteria. + showSums = `false` (optional, boolean, `true`) ... Indicates whether properties should be summed up if they support it. The showSums parameter is applied to the to the querie's result collection of work packages overriding the query's persisted sums property. + timelineVisible = `false` (optional, boolean, `true`) ... Indicates whether the timeline should be shown. + + timelineLabels = `{}` (optional: object, `{}`) ... Overridden labels in the timeline view + showHierarchies = `true` (optional, boolean, `true`) ... Indicates whether the hierarchy mode should be enabled. + Response 200 (application/hal+json) @@ -435,6 +437,7 @@ Delete the query identified by the id parameter "sums": false, "timelineVisible": false, "timelineZoomLevel": "days", + "timelineLabels": {}, "showHierarchies": true, "starred": false, "_embedded": { @@ -1452,6 +1455,13 @@ For more details and all possible responses see the general specification of [Fo "hasDefault": true, "writable": true }, + "timelineLabels": { + "type": "QueryTimelineLabels", + "name": "Timeline labels", + "required": false, + "hasDefault": true, + "writable": true + }, "showHierarchies": { "type": "Boolean", "name": "Show hierarchies", diff --git a/frontend/app/components/api/api-v3/hal-resources/query-resource.service.ts b/frontend/app/components/api/api-v3/hal-resources/query-resource.service.ts index 0b2de42da8..1babc95f1f 100644 --- a/frontend/app/components/api/api-v3/hal-resources/query-resource.service.ts +++ b/frontend/app/components/api/api-v3/hal-resources/query-resource.service.ts @@ -46,6 +46,13 @@ interface QueryResourceEmbedded { export type TimelineZoomLevel = 'days' | 'weeks' | 'months' | 'quarters' | 'years'; +export interface TimelineLabels { + left:string|null; + right:string|null; + farRight:string|null; +} + + export class QueryResource extends HalResource { public $embedded:QueryResourceEmbedded; public id:number; @@ -58,6 +65,7 @@ export class QueryResource extends HalResource { public sums:boolean; public timelineVisible:boolean; public timelineZoomLevel:TimelineZoomLevel; + public timelineLabels:TimelineLabels; public showHierarchies:boolean; public public:boolean; public project:ProjectResource; diff --git a/frontend/app/components/api/api-v3/hal-resources/schema-resource.service.ts b/frontend/app/components/api/api-v3/hal-resources/schema-resource.service.ts index 7c3afb90b1..b2b6a5b754 100644 --- a/frontend/app/components/api/api-v3/hal-resources/schema-resource.service.ts +++ b/frontend/app/components/api/api-v3/hal-resources/schema-resource.service.ts @@ -41,6 +41,10 @@ export class SchemaResource extends HalResource { return states.schemas.get(this.href as string); } + public get availableAttributes() { + return _.keys(this.$source).filter(name => name.indexOf('_') !== 0); + } + public $initialize(source:any) { super.$initialize(source); diff --git a/frontend/app/components/context-menus/settings-menu/settings-menu.controller.ts b/frontend/app/components/context-menus/settings-menu/settings-menu.controller.ts index 73164a130a..62cc1a7d07 100644 --- a/frontend/app/components/context-menus/settings-menu/settings-menu.controller.ts +++ b/frontend/app/components/context-menus/settings-menu/settings-menu.controller.ts @@ -35,6 +35,7 @@ import {QueryResource} from '../../api/api-v3/hal-resources/query-resource.servi import {QueryFormResource} from '../../api/api-v3/hal-resources/query-form-resource.service'; import {States} from '../../states.service'; +import {WorkPackageTableTimelineService} from '../../wp-fast-table/state/wp-table-timeline.service'; interface IMyScope extends ng.IScope { displaySumsLabel:string; @@ -71,10 +72,12 @@ function SettingsDropdownMenuController($scope:IMyScope, shareModal:any, sortingModal:any, groupingModal:any, + timelinesModal:any, contextMenu:ContextMenuService, wpTableHierarchies:WorkPackageTableHierarchiesService, wpTableSum:WorkPackageTableSumService, wpTableGroupBy:WorkPackageTableGroupByService, + wpTableTimeline:WorkPackageTableTimelineService, wpListService:WorkPackagesListService, states:States, AuthorisationService:any, @@ -122,6 +125,7 @@ function SettingsDropdownMenuController($scope:IMyScope, form = formUpdate; + $scope.timelinesVisible = wpTableTimeline.isVisible; $scope.displayHierarchies = wpTableHierarchies.isEnabled; $scope.displaySums = wpTableSum.isEnabled; $scope.isGrouped = wpTableGroupBy.isEnabled; @@ -247,6 +251,11 @@ function SettingsDropdownMenuController($scope:IMyScope, return AuthorisationService.cannot('query', 'updateImmediately'); }; + $scope.showTimelinesModal = function (event:JQueryEventObject) { + event.stopPropagation(); + showModal.call(timelinesModal); + }; + function showModal(this:any) { closeAnyContextMenu(); this.activate(); diff --git a/frontend/app/components/context-menus/settings-menu/settings-menu.service.html b/frontend/app/components/context-menus/settings-menu/settings-menu.service.html index 7a096fd4da..c596dde54c 100644 --- a/frontend/app/components/context-menus/settings-menu/settings-menu.service.html +++ b/frontend/app/components/context-menus/settings-menu/settings-menu.service.html @@ -97,5 +97,14 @@ {{ queryCustomFields.name }} +
  • + + + {{ I18n.t('js.timelines.gantt_chart') }} ... + +
  • diff --git a/frontend/app/components/modals/timelines-modal/timelines-modal.controller.ts b/frontend/app/components/modals/timelines-modal/timelines-modal.controller.ts new file mode 100644 index 0000000000..43508a5204 --- /dev/null +++ b/frontend/app/components/modals/timelines-modal/timelines-modal.controller.ts @@ -0,0 +1,75 @@ +//-- 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 {wpControllersModule} from '../../../angular-modules'; +import {WorkPackageTableTimelineService} from '../../wp-fast-table/state/wp-table-timeline.service'; +import {WorkPackageTableColumnsService} from '../../wp-fast-table/state/wp-table-columns.service'; + +function TimelinesModalController(this:any, + timelinesModal:any, + $scope:any, + wpTableTimeline:WorkPackageTableTimelineService, + wpTableColumns:WorkPackageTableColumnsService, + I18n:op.I18n) { + this.name = 'Timelines'; + this.closeMe = timelinesModal.deactivate; + + $scope.text = { + apply: I18n.t('js.modals.button_apply'), + cancel: I18n.t('js.modals.button_cancel'), + close: I18n.t('js.close_popup_title'), + title: I18n.t('js.timelines.gantt_chart'), + labels: { + description: I18n.t('js.timelines.labels.description'), + bar: I18n.t('js.timelines.labels.bar'), + none: I18n.t('js.timelines.filter.noneSelection'), + left: I18n.t('js.timelines.labels.left'), + right: I18n.t('js.timelines.labels.right'), + farRight: I18n.t('js.timelines.labels.farRight') + } + }; + + // Current label models + const labels = wpTableTimeline.labels; + $scope.labels = _.clone(labels); + + // Available labels + const availableColumns = wpTableColumns + .allPropertyColumns + .sort((a, b) => a.name.localeCompare(b.name)); + + $scope.availableAttributes = [{ id: '', name: $scope.text.labels.none }].concat(availableColumns); + + // Save + $scope.updateLabels = () => { + wpTableTimeline.updateLabels($scope.labels); + timelinesModal.deactivate(); + }; +} + +wpControllersModule.controller('TimelinesModalController', TimelinesModalController); diff --git a/frontend/app/components/modals/timelines-modal/timelines-modal.service.html b/frontend/app/components/modals/timelines-modal/timelines-modal.service.html new file mode 100644 index 0000000000..47543e7eb1 --- /dev/null +++ b/frontend/app/components/modals/timelines-modal/timelines-modal.service.html @@ -0,0 +1,45 @@ +
    + +
    diff --git a/frontend/app/components/modals/timelines-modal/timelines-modal.service.ts b/frontend/app/components/modals/timelines-modal/timelines-modal.service.ts new file mode 100644 index 0000000000..f890855b49 --- /dev/null +++ b/frontend/app/components/modals/timelines-modal/timelines-modal.service.ts @@ -0,0 +1,40 @@ +//-- 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 {wpControllersModule} from '../../../angular-modules'; + +function timelinesModalService(btfModal:any) { + return btfModal({ + controller: 'TimelinesModalController', + controllerAs: '$ctrl', + afterFocusOn: '#work-packages-settings-button', + templateUrl: '/components/modals/timelines-modal/timelines-modal.service.html' + }); +} + +wpControllersModule.factory('timelinesModal', timelinesModalService); diff --git a/frontend/app/components/wp-edit-form/display-field-renderer.ts b/frontend/app/components/wp-edit-form/display-field-renderer.ts index deab9b333f..0e798f0a1c 100644 --- a/frontend/app/components/wp-edit-form/display-field-renderer.ts +++ b/frontend/app/components/wp-edit-form/display-field-renderer.ts @@ -24,6 +24,18 @@ export class DisplayFieldRenderer { } public render(workPackage:WorkPackageResourceInterface, name:string, placeholder = cellEmptyPlaceholder):HTMLSpanElement { + const [field, span] = this.renderFieldValue(workPackage, name, placeholder); + + if (field === null) { + return span; + } + + this.setSpanAttributes(span, field, name, workPackage); + + return span; + } + + public renderFieldValue(workPackage:WorkPackageResourceInterface, name:string, placeholder = cellEmptyPlaceholder):[DisplayField|null, HTMLSpanElement] { const span = document.createElement('span'); const schemaName = workPackage.getSchemaName(name); const fieldSchema = workPackage.schema[schemaName]; @@ -31,18 +43,15 @@ export class DisplayFieldRenderer { // If the work package does not have that field, return an empty // span (e.g., for the table). if (!fieldSchema) { - return span; + return [null, span]; } const field = this.getField(workPackage, fieldSchema, schemaName); - - this.setSpanAttributes(span, field, name, workPackage); - field.render(span, this.getText(field, placeholder)); span.setAttribute('title', this.getLabel(field, workPackage)); span.setAttribute('aria-label', this.getAriaLabel(field, workPackage)); - return span; + return [field, span]; } public getField(workPackage:WorkPackageResourceInterface, fieldSchema:op.FieldSchema, name:string):DisplayField { diff --git a/frontend/app/components/wp-fast-table/state/wp-table-columns.service.ts b/frontend/app/components/wp-fast-table/state/wp-table-columns.service.ts index e84651506e..7c1f2de958 100644 --- a/frontend/app/components/wp-fast-table/state/wp-table-columns.service.ts +++ b/frontend/app/components/wp-fast-table/state/wp-table-columns.service.ts @@ -255,6 +255,12 @@ export class WorkPackageTableColumnsService extends WorkPackageTableBaseService return this.availableState.getValueOr([]); } + public get allPropertyColumns():QueryColumn[] { + return this + .all + .filter((column:QueryColumn) => column._type === queryColumnTypes.PROPERTY); + } + /** * Get columns not yet selected */ diff --git a/frontend/app/components/wp-fast-table/state/wp-table-timeline.service.ts b/frontend/app/components/wp-fast-table/state/wp-table-timeline.service.ts index 161a912f0f..0a4f4bd71c 100644 --- a/frontend/app/components/wp-fast-table/state/wp-table-timeline.service.ts +++ b/frontend/app/components/wp-fast-table/state/wp-table-timeline.service.ts @@ -30,11 +30,12 @@ import { TableStateStates, WorkPackageQueryStateService, WorkPackageTableBaseService } from "./wp-table-base.service"; -import {QueryResource} from "../../api/api-v3/hal-resources/query-resource.service"; +import {QueryResource, TimelineLabels} from "../../api/api-v3/hal-resources/query-resource.service"; import {opServicesModule} from "../../../angular-modules"; import {States} from "../../states.service"; import {WorkPackageTableTimelineState} from "./../wp-table-timeline"; import {zoomLevelOrder} from "../../wp-table/timeline/wp-timeline"; +import {WorkPackageResourceInterface} from '../../api/api-v3/hal-resources/work-package-resource.service'; export class WorkPackageTableTimelineService extends WorkPackageTableBaseService implements WorkPackageQueryStateService { protected stateName = 'timelineVisible' as TableStateStates; @@ -44,7 +45,7 @@ export class WorkPackageTableTimelineService extends WorkPackageTableBaseService } public initialize(query:QueryResource) { - let current = new WorkPackageTableTimelineState(query.timelineVisible, query.timelineZoomLevel); + let current = new WorkPackageTableTimelineState(query); this.state.putValue(current); } @@ -52,13 +53,15 @@ export class WorkPackageTableTimelineService extends WorkPackageTableBaseService public hasChanged(query:QueryResource) { const visibilityChanged = this.isVisible !== query.timelineVisible; const zoomLevelChanged = this.zoomLevel !== query.timelineZoomLevel; + const labelsChanged = !_.isEqual(this.current.labels, query.timelineLabels); - return visibilityChanged || zoomLevelChanged; + return visibilityChanged || zoomLevelChanged || labelsChanged; } public applyToQuery(query:QueryResource) { query.timelineVisible = this.isVisible; query.timelineZoomLevel = this.zoomLevel; + query.timelineLabels = this.current.labels; return false; } @@ -79,7 +82,36 @@ export class WorkPackageTableTimelineService extends WorkPackageTableBaseService return this.current.zoomLevel; } - public updateZoom(delta: number) { + public get labels() { + if (_.isEmpty(this.current.labels)) { + return this.current.defaultLabels; + } + + return this.current.labels; + } + + public updateLabels(labels:TimelineLabels) { + let currentState = this.current; + currentState.labels = labels; + this.state.putValue(currentState); + } + + public getNormalizedLabels(workPackage:WorkPackageResourceInterface) { + let labels:TimelineLabels = _.clone(this.current.defaultLabels); + + _.each(this.current.labels, (attribute:string, position:keyof TimelineLabels) => { + // Set to null to explicitly disable + if (attribute === '') { + labels[position] = null; + } else if (workPackage.schema[attribute]) { + labels[position] = attribute; + } + }); + + return labels; + } + + public updateZoom(delta:number) { let currentState = this.current; let idx = zoomLevelOrder.indexOf(this.current.zoomLevel); idx += delta; @@ -90,7 +122,7 @@ export class WorkPackageTableTimelineService extends WorkPackageTableBaseService } } - private get current():WorkPackageTableTimelineState { + public get current():WorkPackageTableTimelineState { return this.state.value as WorkPackageTableTimelineState; } diff --git a/frontend/app/components/wp-fast-table/wp-table-timeline.ts b/frontend/app/components/wp-fast-table/wp-table-timeline.ts index 918782aa6b..acc43acf62 100644 --- a/frontend/app/components/wp-fast-table/wp-table-timeline.ts +++ b/frontend/app/components/wp-fast-table/wp-table-timeline.ts @@ -26,26 +26,28 @@ // See doc/COPYRIGHT.rdoc for more details. // ++ -import {WorkPackageTableBaseState} from "./wp-table-base"; -import {TimelineZoomLevel} from '../api/api-v3/hal-resources/query-resource.service'; +import {WorkPackageTableBaseState} from './wp-table-base'; +import { + QueryResourceInterface, TimelineLabels, + TimelineZoomLevel +} from '../api/api-v3/hal-resources/query-resource.service'; export class WorkPackageTableTimelineState extends WorkPackageTableBaseState { - constructor(public visible:boolean, public zoomLevel:TimelineZoomLevel) { + public visible:boolean; + public zoomLevel:TimelineZoomLevel; + public labels:TimelineLabels; + + constructor(query:QueryResourceInterface) { super(); + this.visible = query.timelineVisible; + this.zoomLevel = query.timelineZoomLevel; + this.labels = query.timelineLabels; } public get current():boolean { return this.isVisible; } - public get extractedCompareValue():any { - return this.isVisible; - } - - public get currentQueryValue () { - return this.isVisible; - } - public toggle() { this.visible = !this.visible; } @@ -53,4 +55,12 @@ export class WorkPackageTableTimelineState extends WorkPackageTableBaseState { @@ -208,12 +208,12 @@ export function registerWorkPackageMouseHandler(this: void, dateStates = {}; // const renderInfo = getRenderInfo(); - if (cancelled) { + if (cancelled || renderInfo.changeset.empty) { renderInfo.changeset.clear(); - renderer.update(bar, renderInfo); + renderer.update(bar, labels, renderInfo); renderer.onMouseDownEnd(labels, renderInfo.changeset); workPackageTimeline.refreshView(); - } else if (!renderInfo.changeset.empty) { + } else { // Persist the changes saveWorkPackage(renderInfo.changeset) .finally(() => { diff --git a/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts b/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts index 5c62c50295..72ce51ac63 100644 --- a/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts +++ b/frontend/app/components/wp-table/timeline/cells/wp-timeline-cell.ts @@ -41,15 +41,21 @@ import {$injectFields} from '../../../angular/angular-injector-bridge.functions' export const classNameLeftLabel = 'labelLeft'; export const classNameRightContainer = 'containerRight'; export const classNameRightLabel = 'labelRight'; +export const classNameLeftHoverLabel = 'labelHoverLeft'; +export const classNameRightHoverLabel = 'labelHoverRight'; +export const classNameHoverStyle = '-label-style'; export const classNameFarRightLabel = 'labelFarRight'; export const classNameShowOnHover = 'show-on-hover'; +export const classNameHideOnHover = 'hide-on-hover'; export class WorkPackageCellLabels { - constructor(public readonly labelCenter:HTMLDivElement | null, - public readonly labelLeft:HTMLDivElement | null, - public readonly labelRight:HTMLDivElement | null, - public readonly labelFarRight:HTMLDivElement | null) { + constructor(public readonly center:HTMLDivElement | null, + public readonly left:HTMLDivElement, + public readonly leftHover:HTMLDivElement | null, + public readonly right:HTMLDivElement, + public readonly rightHover:HTMLDivElement | null, + public readonly farRight:HTMLDivElement) { } } @@ -180,7 +186,9 @@ export class WorkPackageTimelineCell { // Render the upgrade from renderInfo const shouldBeDisplayed = renderer.update( this.wpElement as HTMLDivElement, + this.labels, renderInfo); + if (!shouldBeDisplayed) { this.clear(); } diff --git a/frontend/app/work_packages/helpers/work-packages-helper.js b/frontend/app/work_packages/helpers/work-packages-helper.ts similarity index 82% rename from frontend/app/work_packages/helpers/work-packages-helper.js rename to frontend/app/work_packages/helpers/work-packages-helper.ts index 447d893650..edfe265600 100644 --- a/frontend/app/work_packages/helpers/work-packages-helper.js +++ b/frontend/app/work_packages/helpers/work-packages-helper.ts @@ -26,12 +26,14 @@ // See doc/COPYRIGHT.rdoc for more details. //++ -module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { +import {HalResource} from '../../components/api/api-v3/hal-resources/hal-resource.service'; + +module.exports = function(TimezoneService:any, currencyFilter:any, CustomFieldHelper:any) { var WorkPackagesHelper = { - getRowObjectContent: function(object, option) { + getRowObjectContent: function(object:any, option:any) { var content = object[option]; - var displayContent = function(content) { + var displayContent = function(content:any) { return content.name || content.subject || content.title || content.value || ''; }; @@ -51,7 +53,7 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { } }, - augmentWorkPackageWithData: function(workPackage, attributeName, isCustomValue, data) { + augmentWorkPackageWithData: function(workPackage:any, attributeName:any, isCustomValue:any, data:any) { if (isCustomValue && data) { if (workPackage.custom_values) { workPackage.custom_values.push(data); @@ -63,10 +65,10 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { } }, - getFormattedCustomValue: function(object, customField) { + getFormattedCustomValue: function(object:any, customField:any) { if (!object.custom_values) { return null; } - var values = object.custom_values.filter(function(customValue){ + var values = object.custom_values.filter(function(customValue:any){ return customValue && customValue.custom_field_id === customField.id; }); @@ -75,7 +77,7 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { } }, - getColumnDataId: function(object, column) { + getColumnDataId: function(object:any, column:any) { var custom_field_id = column.name.match(/^cf_(\d+)$/); if (custom_field_id) { @@ -88,9 +90,9 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { } }, - getCFColumnDataId: function(object, custom_field_id) { + getCFColumnDataId: function(object:any, custom_field_id:any) { - var custom_value = _.find(object.custom_values, function(elem) { + var custom_value = _.find(object.custom_values, function(elem:any) { return elem && (elem.custom_field_id === custom_field_id); }); @@ -102,7 +104,7 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { } }, - getStaticColumnDataId: function(object, column) { + getStaticColumnDataId: function(object:any, column:any) { switch (column.name) { case 'parent': return object.parent_id; @@ -116,13 +118,13 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { } }, - getFormattedColumnData: function(object, column) { + getFormattedColumnData: function(object:any, column:any) { var value = WorkPackagesHelper.getRowObjectContent(object, column.name); return WorkPackagesHelper.formatValue(value, column.meta_data.data_type); }, - formatValue: function(value, dataType) { + formatValue: function(value:any, dataType:any) { switch(dataType) { case 'datetime': var dateTime; @@ -143,11 +145,14 @@ module.exports = function(TimezoneService, currencyFilter, CustomFieldHelper) { case 'Date': return TimezoneService.formattedDate(value); default: + if (value instanceof HalResource) { + return value.name; + } return value; } }, - parseDateTime: function(value) { + parseDateTime: function(value:any) { return new Date(Date.parse(value.replace(/(A|P)M$/, ''))); } }; diff --git a/frontend/tests/unit/tests/work_packages/controllers/menus/options-dropdown-menu-controller-test.js b/frontend/tests/unit/tests/work_packages/controllers/menus/options-dropdown-menu-controller-test.js index efa1c3e0f7..5e90930c08 100644 --- a/frontend/tests/unit/tests/work_packages/controllers/menus/options-dropdown-menu-controller-test.js +++ b/frontend/tests/unit/tests/work_packages/controllers/menus/options-dropdown-menu-controller-test.js @@ -113,6 +113,9 @@ describe('optionsDropdown Directive', function() { }, table: { + timelineVisible: { + value: {} + }, stopAllSubscriptions: [false] } }; diff --git a/lib/api/v3/queries/query_representer.rb b/lib/api/v3/queries/query_representer.rb index e9d397c90f..294bae41b2 100644 --- a/lib/api/v3/queries/query_representer.rb +++ b/lib/api/v3/queries/query_representer.rb @@ -245,6 +245,8 @@ module API property :timeline_zoom_level + property :timeline_labels + attr_accessor :results, :params diff --git a/lib/api/v3/queries/schemas/query_schema_representer.rb b/lib/api/v3/queries/schemas/query_schema_representer.rb index 127e489f2b..75675bfa33 100644 --- a/lib/api/v3/queries/schemas/query_schema_representer.rb +++ b/lib/api/v3/queries/schemas/query_schema_representer.rb @@ -112,6 +112,13 @@ module API has_default: true, visibility: false + schema :timeline_labels, + type: 'QueryTimelineLabels', + required: false, + writable: true, + has_default: true, + visibility: false + schema :show_hierarchies, type: 'Boolean', required: false, diff --git a/spec/features/work_packages/timeline/timeline_labels_spec.rb b/spec/features/work_packages/timeline/timeline_labels_spec.rb new file mode 100644 index 0000000000..ffd6c8d704 --- /dev/null +++ b/spec/features/work_packages/timeline/timeline_labels_spec.rb @@ -0,0 +1,156 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +RSpec.feature 'Work package timeline labels', + with_settings: { date_format: '%Y-%m-%d' }, + js: true, + selenium: true do + let(:user) { FactoryGirl.create(:admin) } + let(:type) { FactoryGirl.create(:type_bug) } + let(:milestone_type) { FactoryGirl.create(:type, is_milestone: true) } + + let(:project) { FactoryGirl.create(:project, types: [type, milestone_type]) } + let(:query_menu) { Components::WorkPackages::QueryMenu.new } + let(:settings_menu) { Components::WorkPackages::SettingsMenu.new } + let(:config_modal) { Components::Timelines::ConfigurationModal.new } + let(:wp_timeline) { Pages::WorkPackagesTimeline.new(project) } + + let(:custom_field) do + FactoryGirl.create( + :list_wp_custom_field, + name: "Ingredients", + multi_value: true, + types: [type], + projects: [project], + possible_values: ["ham", "onions", "pineapple", "mushrooms"] + ) + end + + def custom_value_for(str) + custom_field.custom_options.find { |co| co.value == str }.try(:id) + end + + let(:work_package) do + FactoryGirl.create :work_package, + project: project, + type: type, + assigned_to: user, + start_date: '2017-08-21', + due_date: '2017-08-25', + subject: 'My subject', + custom_field_values: { custom_field.id => custom_value_for('onions') } + end + + let(:milestone_work_package) do + FactoryGirl.create :work_package, + project: project, + type: milestone_type, + start_date: '2017-08-30', + due_date: '2017-08-30', + subject: 'My milestone' + end + + before do + custom_field + milestone_work_package + work_package + login_as(user) + + wp_timeline.visit! + wp_timeline.expect_timeline! open: false + wp_timeline.toggle_timeline + end + + it 'shows and allows to configure labels' do + # Check default labels (bar type) + row = wp_timeline.timeline_row work_package.id + row.expect_labels left: nil, + right: nil, + farRight: 'My subject' + + row.expect_hovered_labels left: '2017-08-21', right: '2017-08-25' + + # Check default labels (milestone) + row = wp_timeline.timeline_row milestone_work_package.id + row.expect_labels left: nil, + right: nil, + farRight: 'My milestone' + row.expect_hovered_labels left: nil, right: '2017-08-30' + + # Modify label configuration + config_modal.open! + config_modal.expect_labels! left: '(none)', + right: '(none)', + farRight: 'Subject' + + config_modal.update_labels left: 'Assignee', + right: 'Type', + farRight: 'Status' + + # Check overriden labels + row = wp_timeline.timeline_row work_package.id + row.expect_labels left: user.name, + right: type.name, + farRight: work_package.status.name + + # Check default labels (milestone) + row = wp_timeline.timeline_row milestone_work_package.id + row.expect_labels left: '-', + right: milestone_type.name, + farRight: milestone_work_package.status.name + + # Save the query + settings_menu.open_and_save_query 'changed labels' + wp_timeline.expect_title 'changed labels' + + # Check the query + query = Query.last + expect(query.timeline_labels).to eq 'left' => 'assignee', + 'right' => 'type', + 'farRight' => 'status' + + # Revisit page + wp_timeline.visit_query query + wp_timeline.expect_work_package_listed(work_package, milestone_work_package) + wp_timeline.expect_timeline!(open: true) + + # Check overridden labels + row = wp_timeline.timeline_row work_package.id + row.expect_labels left: user.name, + right: type.name, + farRight: work_package.status.name + + # Check overridden labels (milestone) + row = wp_timeline.timeline_row milestone_work_package.id + row.expect_labels left: '-', + right: milestone_type.name, + farRight: milestone_work_package.status.name + end +end diff --git a/spec/lib/api/v3/queries/query_representer_generation_spec.rb b/spec/lib/api/v3/queries/query_representer_generation_spec.rb index 1b756aad34..4e9beba41f 100644 --- a/spec/lib/api/v3/queries/query_representer_generation_spec.rb +++ b/spec/lib/api/v3/queries/query_representer_generation_spec.rb @@ -472,6 +472,10 @@ describe ::API::V3::Queries::QueryRepresenter do is_expected.to be_json_eql(query.timeline_zoom_level.to_json).at_path('timelineZoomLevel') end + it 'should show the default timelineLabels' do + is_expected.to be_json_eql(query.timeline_labels.to_json).at_path('timelineLabels') + end + it 'should indicate whether the query is publicly visible' do is_expected.to be_json_eql(query.is_public.to_json).at_path('public') end @@ -643,6 +647,21 @@ describe ::API::V3::Queries::QueryRepresenter do end end + describe 'when labels are overridden' do + let(:query) do + FactoryGirl.build_stubbed(:query, project: project).tap do |query| + query.timeline_labels = expected + end + end + let(:expected) do + { 'left' => 'assignee', 'right' => 'status', 'farRight' => 'type' } + end + + it do + is_expected.to be_json_eql(expected.to_json).at_path('timelineLabels') + end + end + describe 'when timeline zoom level is changed' do let(:query) do FactoryGirl.build_stubbed(:query, project: project).tap do |query| diff --git a/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb b/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb index 3bb6329f71..c65001a432 100644 --- a/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb +++ b/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb @@ -220,6 +220,20 @@ describe ::API::V3::Queries::Schemas::QuerySchemaRepresenter do it_behaves_like 'has no visibility property' end + describe 'timelineLabels' do + let(:path) { 'timelineLabels' } + + it_behaves_like 'has basic schema properties' do + let(:type) { 'QueryTimelineLabels' } + let(:name) { Query.human_attribute_name('timeline_labels') } + let(:required) { false } + let(:writable) { true } + let(:has_default) { true } + end + + it_behaves_like 'has no visibility property' + end + describe 'show hierarchies' do let(:path) { 'showHierarchies' } diff --git a/spec/models/query_spec.rb b/spec/models/query_spec.rb index 34185e46af..6b6edfdebb 100644 --- a/spec/models/query_spec.rb +++ b/spec/models/query_spec.rb @@ -62,6 +62,17 @@ describe Query, type: :model do query.timeline_visible = true expect(query.timeline_visible).to be_truthy end + + it 'validates the timeline labels hash keys' do + expect(query.timeline_labels).to eq({}) + expect(query).to be_valid + + query.timeline_labels = { 'left' => 'foobar', 'xyz' => 'bar' } + expect(query).not_to be_valid + + query.timeline_labels = { 'left' => 'foobar', 'right' => 'bar', 'farRight' => 'blub' } + expect(query).to be_valid + end end describe 'hierarchies' do diff --git a/spec/support/components/timelines/configuration_modal.rb b/spec/support/components/timelines/configuration_modal.rb new file mode 100644 index 0000000000..88846fe2c5 --- /dev/null +++ b/spec/support/components/timelines/configuration_modal.rb @@ -0,0 +1,66 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module Components + module Timelines + class ConfigurationModal + include Capybara::DSL + include RSpec::Matchers + + attr_reader :settings_menu + + def initialize + @settings_menu = ::Components::WorkPackages::SettingsMenu.new + end + + def open! + @settings_menu.open_and_choose 'Gantt chart ...' + end + + def get_select(position) + page.find("#modal-timelines-label-#{position}") + end + + def expect_labels!(left:, right:, farRight:) + expect(page).to have_select('modal-timelines-label-left', selected: left) + expect(page).to have_select('modal-timelines-label-right', selected: right) + expect(page).to have_select('modal-timelines-label-farRight', selected: farRight) + end + + def update_labels(left:, right:, farRight:) + get_select(:left).find('option', text: left).select_option + get_select(:right).find('option', text: right).select_option + get_select(:farRight).find('option', text: farRight).select_option + + page.within '.ng-modal-window' do + click_on 'Apply' + end + end + end + end +end diff --git a/spec/support/components/timelines/timeline_row.rb b/spec/support/components/timelines/timeline_row.rb new file mode 100644 index 0000000000..c9abe4b9b9 --- /dev/null +++ b/spec/support/components/timelines/timeline_row.rb @@ -0,0 +1,76 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2017 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module Components + module Timelines + class TimelineRow + include Capybara::DSL + include RSpec::Matchers + + attr_reader :container + + def initialize(container) + @container = container + end + + def hover! + @container.hover + end + + def expect_hovered_labels(left:, right:) + hover! + + unless left.nil? + expect(container).to have_selector(".labelHoverLeft.not-empty", text: left) + end + unless right.nil? + expect(container).to have_selector(".labelHoverRight.not-empty", text: right) + end + + expect(container).to have_selector(".labelLeft", visible: false) + expect(container).to have_selector(".labelRight", visible: false) + expect(container).to have_selector(".labelFarRight", visible: false) + end + + def expect_labels(left:, right:, farRight:) + { + labelLeft: left, + labelRight: right, + labelFarRight: farRight + }.each do |className, text| + if text.nil? + expect(container).to have_selector(".#{className}", visible: :all) + expect(container).to have_no_selector(".#{className}.not-empty", wait: 0) + else + expect(container).to have_selector(".#{className}.not-empty", text: text) + end + end + end + end + end +end diff --git a/spec/support/pages/work_packages_timeline.rb b/spec/support/pages/work_packages_timeline.rb index 444631cf1b..d779ed5f98 100644 --- a/spec/support/pages/work_packages_timeline.rb +++ b/spec/support/pages/work_packages_timeline.rb @@ -36,7 +36,7 @@ module Pages end def timeline_row_selector(wp_id) - "#wp-timeline-row-#{wp_id}" + ".wp-row-#{wp_id}-timeline" end def timeline_container @@ -81,6 +81,10 @@ module Pages end end + def timeline_row(wp_id) + ::Components::Timelines::TimelineRow.new page.find(timeline_row_selector(wp_id)) + end + def zoom_in page.find('#work-packages-timeline-zoom-in-button').click end