From 7a1bebae2b523e7eec882dc3ffbc896f0a7f0372 Mon Sep 17 00:00:00 2001 From: ulferts Date: Tue, 2 Jun 2020 13:24:48 +0200 Subject: [PATCH] allow hiding days on the logged time widget --- .../te-calendar/te-calendar.component.ts | 17 ++++ .../te-calendar/te-calendar.template.html | 1 + .../modules/grids/openproject-grids.module.ts | 5 ++ ...ries-current-user-configuration.modal.html | 38 +++++++++ ...ntries-current-user-configuration.modal.ts | 85 +++++++++++++++++++ ...ime-entries-current-user-menu.component.ts | 81 ++++++++++++++++++ .../time-entries-current-user.component.html | 8 +- .../time-entries-current-user.component.ts | 12 +++ ...time_entry_calendar_options_representer.rb | 42 +++++++++ modules/grids/config/locales/js-en.yml | 2 + .../my_page/lib/my_page/grid_registration.rb | 4 + .../my/time_entries_current_user_spec.rb | 38 +++++++-- 12 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.html create mode 100644 frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.ts create mode 100644 frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts create mode 100644 modules/grids/app/representers/api/v3/grids/widgets/time_entry_calendar_options_representer.rb diff --git a/frontend/src/app/modules/calendar/te-calendar/te-calendar.component.ts b/frontend/src/app/modules/calendar/te-calendar/te-calendar.component.ts index 06fc4622b8..f0953c27f5 100644 --- a/frontend/src/app/modules/calendar/te-calendar/te-calendar.component.ts +++ b/frontend/src/app/modules/calendar/te-calendar/te-calendar.component.ts @@ -54,6 +54,9 @@ interface CalendarMoveEvent { view:View; } +// An array of all the days that are displayed. The zero index represents Monday. +export type DisplayedDays = [boolean, boolean, boolean, boolean, boolean, boolean, boolean]; + const TIME_ENTRY_CLASS_NAME = 'te-calendar--time-entry'; const DAY_SUM_CLASS_NAME = 'te-calendar--day-sum'; const ADD_ENTRY_CLASS_NAME = 'te-calendar--add-entry'; @@ -76,6 +79,9 @@ export class TimeEntryCalendarComponent implements OnInit, AfterViewInit { @ViewChild(FullCalendarComponent) ucCalendar:FullCalendarComponent; @Input() projectIdentifier:string; @Input() static:boolean = false; + @Input() set displayedDays(days:DisplayedDays) { + this.setHiddenDays(days); + } @Output() entries = new EventEmitter>(); // Not used by the calendar but rather is the maximum/minimum of the graph. @@ -104,6 +110,7 @@ export class TimeEntryCalendarComponent implements OnInit, AfterViewInit { protected memoizedTimeEntries:{ start:Date, end:Date, entries:Promise> }; public memoizedCreateAllowed:boolean = false; + public hiddenDays:number[] = []; public text = { logTime: this.i18n.t('js.button_log_time') @@ -588,4 +595,14 @@ export class TimeEntryCalendarComponent implements OnInit, AfterViewInit { return 1; } + + protected setHiddenDays(displayedDays:DisplayedDays) { + this.hiddenDays = Array.from(displayedDays, (value, index) => { + if (!value) { + return (index + 1) % 7; + } else { + return null; + } + }).filter((value) => value !== null) as number[]; + } } diff --git a/frontend/src/app/modules/calendar/te-calendar/te-calendar.template.html b/frontend/src/app/modules/calendar/te-calendar/te-calendar.template.html index 3e410a0514..3e3c8dc0b3 100644 --- a/frontend/src/app/modules/calendar/te-calendar/te-calendar.template.html +++ b/frontend/src/app/modules/calendar/te-calendar/te-calendar.template.html @@ -21,6 +21,7 @@ [header]="calendarHeader" [defaultView]="calendarDefaultView" [firstDay]="calendarFirstDay" + [hiddenDays]="hiddenDays" [contentHeight]="calendarContentHeight" [slotEventOverlap]="calendarSlotEventOverlap" [slotLabelInterval]="calendarSlotLabelInterval" diff --git a/frontend/src/app/modules/grids/openproject-grids.module.ts b/frontend/src/app/modules/grids/openproject-grids.module.ts index f82731f619..3b84665491 100644 --- a/frontend/src/app/modules/grids/openproject-grids.module.ts +++ b/frontend/src/app/modules/grids/openproject-grids.module.ts @@ -64,6 +64,8 @@ import {OpenprojectAttachmentsModule} from "core-app/modules/attachments/openpro import {WidgetMembersComponent} from "core-app/modules/grids/widgets/members/members.component"; import {WidgetProjectStatusComponent} from "core-app/modules/grids/widgets/project-status/project-status.component"; import {OpenprojectTimeEntriesModule} from "core-app/modules/time_entries/openproject-time-entries.module"; +import {WidgetTimeEntriesCurrentUserMenuComponent} from "core-app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component"; +import { TimeEntriesCurrentUserConfigurationModalComponent } from './widgets/time-entries/current-user/time-entries-current-user-configuration.modal'; @NgModule({ imports: [ @@ -127,6 +129,8 @@ import {OpenprojectTimeEntriesModule} from "core-app/modules/time_entries/openpr WidgetMenuComponent, WidgetWpTableMenuComponent, WidgetWpGraphMenuComponent, + WidgetTimeEntriesCurrentUserMenuComponent, + TimeEntriesCurrentUserConfigurationModalComponent, AddGridWidgetModal, @@ -261,6 +265,7 @@ export function registerWidgets(injector:Injector) { title: i18n.t(`js.grid.widgets.time_entries_current_user.title`), properties: { name: i18n.t('js.grid.widgets.time_entries_current_user.title'), + days: [true, true, true, true, true, true, true] } }, { diff --git a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.html b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.html new file mode 100644 index 0000000000..a5d873b30d --- /dev/null +++ b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.html @@ -0,0 +1,38 @@ +
+
+ + +

+ +
+ + + + + + +
+ +
+ +
+
diff --git a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.ts b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.ts new file mode 100644 index 0000000000..f127fbbc39 --- /dev/null +++ b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.ts @@ -0,0 +1,85 @@ +import { + ApplicationRef, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + ElementRef, + Inject, + InjectionToken, + Injector, + OnDestroy, + OnInit, + Optional, + ViewChild +} from '@angular/core'; +import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types'; +import {ConfigurationService} from 'core-app/modules/common/config/configuration.service'; +import {OpModalComponent} from 'core-components/op-modals/op-modal.component'; +import { + ActiveTabInterface, + TabComponent, + TabInterface, + TabPortalOutlet +} from 'core-components/wp-table/configuration-modal/tab-portal-outlet'; +import {LoadingIndicatorService} from 'core-app/modules/common/loading-indicator/loading-indicator.service'; +import {I18nService} from "core-app/modules/common/i18n/i18n.service"; +import {OpModalLocalsToken} from "core-components/op-modals/op-modal.service"; +import {ComponentType} from "@angular/cdk/portal"; +import {WpGraphConfigurationService} from "core-app/modules/work-package-graphs/configuration/wp-graph-configuration.service"; +import {WpGraphConfiguration} from "core-app/modules/work-package-graphs/configuration/wp-graph-configuration"; +import {WorkPackageNotificationService} from "core-app/modules/work_packages/notifications/work-package-notification.service"; + +@Component({ + templateUrl: './time-entries-current-user-configuration.modal.html', +}) +export class TimeEntriesCurrentUserConfigurationModalComponent extends OpModalComponent implements OnInit { + + /* Close on escape? */ + public closeOnEscape = true; + + /* Close on outside click */ + public closeOnOutsideClick = true; + + public $element:JQuery; + + public text = { + displayedDays: this.I18n.t('js.grid.widgets.time_entries_current_user.displayed_days'), + closePopup: this.I18n.t('js.close_popup_title'), + + applyButton: this.I18n.t('js.modals.button_apply'), + cancelButton: this.I18n.t('js.modals.button_cancel'), + + weekdays: moment.weekdays() + }; + + public firstDayOfWeek:number; + public firstDayOffset = this.configuration.startOfWeek(); + + // All days of the week, zero based on Monday. + public options:{ days:boolean[] }; + public days:boolean[]; + + constructor(@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, + readonly I18n:I18nService, + readonly injector:Injector, + readonly appRef:ApplicationRef, + readonly loadingIndicator:LoadingIndicatorService, + readonly notificationService:WorkPackageNotificationService, + readonly cdRef:ChangeDetectorRef, + readonly configuration:ConfigurationService, + readonly elementRef:ElementRef) { + super(locals, cdRef, elementRef); + } + + ngOnInit() { + this.days = this.locals.options.days || Array.from({ length: 7 }, () => true ); + + let momentFirstDayOffset = 1 + moment.localeData().firstDayOfWeek() % 7; + this.text.weekdays = moment.localeData().weekdays().slice(momentFirstDayOffset).concat(moment.localeData().weekdays().slice(0, momentFirstDayOffset)); + } + + public saveChanges():void { + this.options = { days: this.days }; + this.service.close(); + } +} diff --git a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts new file mode 100644 index 0000000000..b7233a1867 --- /dev/null +++ b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts @@ -0,0 +1,81 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2020 the OpenProject GmbH +// +// 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 docs/COPYRIGHT.rdoc for more details. +//++ + +import {Component, Output, EventEmitter, Injector} from '@angular/core'; +import {WpGraphConfigurationModalComponent} from "core-app/modules/work-package-graphs/configuration-modal/wp-graph-configuration.modal"; +import {WidgetWpSetMenuComponent} from "core-app/modules/grids/widgets/menu/wp-set-menu.component"; +import {OpModalComponent} from "core-components/op-modals/op-modal.component"; +import {OpModalService} from "core-components/op-modals/op-modal.service"; +import {I18nService} from "core-app/modules/common/i18n/i18n.service"; +import {GridRemoveWidgetService} from "core-app/modules/grids/grid/remove-widget.service"; +import {GridAreaService} from "core-app/modules/grids/grid/area.service"; +import {WidgetAbstractMenuComponent} from "core-app/modules/grids/widgets/menu/widget-abstract-menu.component"; +import {TimeEntriesCurrentUserConfigurationModalComponent} from "core-app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal"; + +@Component({ + selector: 'widget-time-entries-current-user-menu', + templateUrl: '../../menu/widget-menu.component.html' +}) +export class WidgetTimeEntriesCurrentUserMenuComponent extends WidgetAbstractMenuComponent { + @Output() + onConfigured:EventEmitter = new EventEmitter(); + + protected menuItemList = [ + this.removeItem, + this.configureItem + ]; + + constructor(private readonly injector:Injector, + private readonly opModalService:OpModalService, + readonly i18n:I18nService, + protected readonly remove:GridRemoveWidgetService, + readonly layout:GridAreaService) { + super(i18n, + remove, + layout); + } + + protected get configureItem() { + return { + linkText: this.i18n.t('js.grid.configure'), + onClick: () => { + this.opModalService.show(TimeEntriesCurrentUserConfigurationModalComponent, this.injector, this.locals) + .closingEvent.subscribe((modal:TimeEntriesCurrentUserConfigurationModalComponent) => { + if (modal.options) { + this.onConfigured.emit(modal.options); + } + }); + return true; + } + }; + } + + protected get locals() { + return { options: this.resource.options }; + } +} diff --git a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.html b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.html index f9b17c6114..36d9b0657b 100644 --- a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.html +++ b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.html @@ -2,13 +2,15 @@ [name]="widgetName" [editable]="isEditable"> - - + + diff --git a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.ts b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.ts index bb0e7c5fed..eca02dfbda 100644 --- a/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.ts +++ b/frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.ts @@ -5,6 +5,7 @@ import {TimezoneService} from "core-components/datetime/timezone.service"; import {I18nService} from "core-app/modules/common/i18n/i18n.service"; import {PathHelperService} from "core-app/modules/common/path-helper/path-helper.service"; import {AbstractWidgetComponent} from "core-app/modules/grids/widgets/abstract-widget.component"; +import {DisplayedDays} from "core-app/modules/calendar/te-calendar/te-calendar.component"; @Component({ templateUrl: './time-entries-current-user.component.html', @@ -12,6 +13,7 @@ import {AbstractWidgetComponent} from "core-app/modules/grids/widgets/abstract-w }) export class WidgetTimeEntriesCurrentUserComponent extends AbstractWidgetComponent { public entries:TimeEntryResource[] = []; + public displayedDays:DisplayedDays; constructor(protected readonly injector:Injector, readonly timezone:TimezoneService, @@ -21,6 +23,10 @@ export class WidgetTimeEntriesCurrentUserComponent extends AbstractWidgetCompone super(i18n, injector); } + public ngOnInit() { + this.displayedDays = this.resource.options.days as DisplayedDays; + } + public updateEntries(entries:CollectionResource) { this.entries = entries.elements; @@ -43,6 +49,12 @@ export class WidgetTimeEntriesCurrentUserComponent extends AbstractWidgetCompone return false; } + public updateConfiguration(options:{ days:DisplayedDays }) { + this.resourceChanged.emit(this.setChangesetOptions(options)); + // Need to copy to trigger change detection + this.displayedDays = [...options.days] as DisplayedDays; + } + protected formatNumber(value:number):string { return this.i18n.toNumber(value, { precision: 2 }); } diff --git a/modules/grids/app/representers/api/v3/grids/widgets/time_entry_calendar_options_representer.rb b/modules/grids/app/representers/api/v3/grids/widgets/time_entry_calendar_options_representer.rb new file mode 100644 index 0000000000..0f76f27c6f --- /dev/null +++ b/modules/grids/app/representers/api/v3/grids/widgets/time_entry_calendar_options_representer.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2020 the OpenProject GmbH +# +# 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 API + module V3 + module Grids + module Widgets + class TimeEntryCalendarOptionsRepresenter < DefaultOptionsRepresenter + property :days, + getter: ->(represented:, **) { + represented['days'] || {} + } + end + end + end + end +end diff --git a/modules/grids/config/locales/js-en.yml b/modules/grids/config/locales/js-en.yml index 18d0ce09b8..1ade1110ce 100644 --- a/modules/grids/config/locales/js-en.yml +++ b/modules/grids/config/locales/js-en.yml @@ -3,6 +3,7 @@ en: grid: add_widget: 'Add widget' remove: 'Remove widget' + configure: 'Configure widget' upsale: text: "Some widgets, like the work package graph widget, are only available in the " link: 'enterprise edition.' @@ -39,6 +40,7 @@ en: no_results: 'No subprojects.' time_entries_current_user: title: 'My spent time' + displayed_days: 'Days displayed in the widget:' time_entries_list: title: 'Spent time (last 7 days)' no_results: 'No time entries for the last 7 days.' diff --git a/modules/my_page/lib/my_page/grid_registration.rb b/modules/my_page/lib/my_page/grid_registration.rb index 4e5e7e08a5..ee99b56c53 100644 --- a/modules/my_page/lib/my_page/grid_registration.rb +++ b/modules/my_page/lib/my_page/grid_registration.rb @@ -28,6 +28,10 @@ module MyPage widget_strategy 'work_packages_watched', &wp_table_strategy_proc widget_strategy 'work_packages_created', &wp_table_strategy_proc + widget_strategy 'time_entries_current_user' do + options_representer '::API::V3::Grids::Widgets::TimeEntryCalendarOptionsRepresenter' + end + widget_strategy 'custom_text' do # Requiring a permission here as one is required to assign attachments. # Should be replaced by a global permission to have a my page diff --git a/modules/my_page/spec/features/my/time_entries_current_user_spec.rb b/modules/my_page/spec/features/my/time_entries_current_user_spec.rb index 15a7d3c315..87f586380a 100644 --- a/modules/my_page/spec/features/my/time_entries_current_user_spec.rb +++ b/modules/my_page/spec/features/my/time_entries_current_user_spec.rb @@ -249,10 +249,26 @@ describe 'My page time entries current user widget spec', type: :feature, js: tr expect(page) .to have_content "Total: 12.00" + ## Hiding weekdays + entries_area.click_menu_item I18n.t('js.grid.configure') + + uncheck 'Monday' # the day visible_time_entry is logged for + + click_button 'Apply' + + within entries_area.area do + expect(page) + .not_to have_selector('.fc-day-header', text: 'Mon') + expect(page) + .not_to have_selector('.fc-duration', text: "6 h") + end + ## Removing the time entry within entries_area.area do - find(".fc-content-skeleton td:nth-of-type(6) .fc-event-container .te-calendar--time-entry").click + # to place the tooltip at a different spot + find(".fc-content-skeleton td:nth-of-type(5) .fc-event-container .te-calendar--time-entry").hover + find(".fc-content-skeleton td:nth-of-type(5) .fc-event-container .te-calendar--time-entry").click end time_logging_modal.is_visible true @@ -263,15 +279,27 @@ describe 'My page time entries current user widget spec', type: :feature, js: tr within entries_area.area do expect(page) - .not_to have_selector(".fc-content-skeleton td:nth-of-type(6) .fc-event-container .te-calendar--time-entry") + .not_to have_selector(".fc-content-skeleton td:nth-of-type(5) .fc-event-container .te-calendar--time-entry") end - expect(page) - .to have_content "Total: 10.00" - expect(TimeEntry.where(id: other_visible_time_entry.id)) .not_to be_exist + ## Reloading keeps the configuration + visit root_path + my_page.visit! + + within entries_area.area do + expect(page) + .to have_content(/#{Regexp.escape(I18n.t('js.grid.widgets.time_entries_current_user.title'))}/i) + + expect(page) + .to have_selector(".fc-event-container .te-calendar--time-entry", count: 1) + + expect(page) + .not_to have_selector('.fc-day-header', text: 'Mon') + end + # Removing the widget entries_area.remove