Merge pull request #8435 from opf/feature/my_spent_time_config

allow hiding days on the logged time widget

[ci skip]
pull/8440/head
Oliver Günther 4 years ago committed by GitHub
commit 16fe9da921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      frontend/src/app/modules/calendar/te-calendar/te-calendar.component.ts
  2. 1
      frontend/src/app/modules/calendar/te-calendar/te-calendar.template.html
  3. 5
      frontend/src/app/modules/grids/openproject-grids.module.ts
  4. 38
      frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.html
  5. 85
      frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-configuration.modal.ts
  6. 81
      frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user-menu.component.ts
  7. 8
      frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.html
  8. 12
      frontend/src/app/modules/grids/widgets/time-entries/current-user/time-entries-current-user.component.ts
  9. 42
      modules/grids/app/representers/api/v3/grids/widgets/time_entry_calendar_options_representer.rb
  10. 2
      modules/grids/config/locales/js-en.yml
  11. 4
      modules/my_page/lib/my_page/grid_registration.rb
  12. 38
      modules/my_page/spec/features/my/time_entries_current_user_spec.rb

@ -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<CollectionResource<TimeEntryResource>>();
// 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<CollectionResource<TimeEntryResource>> };
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[];
}
}

@ -21,6 +21,7 @@
[header]="calendarHeader"
[defaultView]="calendarDefaultView"
[firstDay]="calendarFirstDay"
[hiddenDays]="hiddenDays"
[contentHeight]="calendarContentHeight"
[slotEventOverlap]="calendarSlotEventOverlap"
[slotLabelInterval]="calendarSlotLabelInterval"

@ -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]
}
},
{

@ -0,0 +1,38 @@
<div class="op-modal--portal ">
<div class="op-modal--modal-container"
data-indicator-name="modal"
tabindex="0">
<div class="op-modal--modal-header">
<a class="op-modal--modal-close-button">
<i
class="icon-close"
(click)="closeMe($event)"
[attr.title]="text.closePopup">
</i>
</a>
</div>
<h3 [textContent]="text.displayedDays"></h3>
<div class="form--field -trailing-label" *ngFor="let day of text.weekdays; let index = index">
<label class="form--label" [textContent]="day" [htmlFor]="'day_' + index"></label>
<span class="form--field-container">
<span class="form--check-box-container">
<input type="checkbox" [id]="'day_' + index" name="days" class="form--check-box" [(ngModel)]="days[index]">
</span>
</span>
</div>
<div class="tab-content"></div>
<div class="modal--form-actions">
<button class="button -highlight"
[textContent]="text.applyButton"
(click)="saveChanges()">
</button>
<button class="button"
[textContent]="text.cancelButton"
(click)="closeMe($event)">
</button>
</div>
</div>
</div>

@ -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();
}
}

@ -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<any> = 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 };
}
}

@ -2,13 +2,15 @@
[name]="widgetName"
[editable]="isEditable">
<widget-menu
[resource]="resource">
</widget-menu>
<widget-time-entries-current-user-menu
[resource]="resource"
(onConfigured)="updateConfiguration($event)">
</widget-time-entries-current-user-menu>
</widget-header>
<te-calendar
(entries)="updateEntries($event)"
[displayedDays]="displayedDays"
></te-calendar>
<ng-container>

@ -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<TimeEntryResource>) {
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 });
}

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

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

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

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

Loading…
Cancel
Save