Merge pull request #9977 from opf/feature/36322-Save-different-views-in-calendar-module

[36322] Save different views in calendar module
pull/10032/head
Oliver Günther 3 years ago committed by GitHub
commit dc479cbbe0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      Gemfile.lock
  2. 1
      Gemfile.modules
  3. 2
      app/views/homescreen/robots.text.erb
  4. 5
      config/initializers/menus.rb
  5. 5
      config/initializers/permissions.rb
  6. 2
      config/locales/en.yml
  7. 4
      config/locales/js-en.yml
  8. 5
      config/routes.rb
  9. 6
      docs/api/apiv3/paths/views_{identifier}.yml
  10. 2
      frontend/src/app/app.module.ts
  11. 2
      frontend/src/app/core/path-helper/path-helper.service.ts
  12. 2
      frontend/src/app/core/routing/openproject.routes.ts
  13. 27
      frontend/src/app/features/calendar/calendar.lazy-routes.ts
  14. 75
      frontend/src/app/features/calendar/calendar.routes.ts
  15. 34
      frontend/src/app/features/calendar/op-calendar.service.ts
  16. 33
      frontend/src/app/features/calendar/openproject-calendar.module.ts
  17. 0
      frontend/src/app/features/calendar/te-calendar/te-calendar.component.sass
  18. 2
      frontend/src/app/features/calendar/te-calendar/te-calendar.component.ts
  19. 0
      frontend/src/app/features/calendar/te-calendar/te-calendar.template.html
  20. 130
      frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts
  21. 125
      frontend/src/app/features/calendar/wp-calendar/wp-calendar.component.ts
  22. 2
      frontend/src/app/features/calendar/wp-calendar/wp-calendar.sass
  23. 17
      frontend/src/app/features/calendar/wp-calendar/wp-calendar.template.html
  24. 1
      frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts
  25. 19
      frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts
  26. 3
      frontend/src/app/features/work-packages/components/wp-list/wp-query-view.service.ts
  27. 27
      frontend/src/app/shared/components/calendar/wp-calendar-entry/wp-calendar-entry.component.html
  28. 14
      frontend/src/app/shared/components/calendar/wp-calendar/wp-calendar.template.html
  29. 2
      frontend/src/app/shared/components/grids/openproject-grids.module.ts
  30. 2
      frontend/src/app/shared/components/grids/widgets/time-entries/current-user/configuration-modal/services/configuration-modal/configuration-modal.service.ts
  31. 2
      frontend/src/app/shared/components/grids/widgets/time-entries/current-user/time-entries-current-user.component.ts
  32. 5
      frontend/src/app/shared/components/grids/widgets/wp-calendar/wp-calendar.component.html
  33. 3
      frontend/src/global_styles/content/modules/_calendar.sass
  34. 1
      frontend/src/global_styles/content/modules/_index.sass
  35. 1
      frontend/src/global_styles/vendor/_full_calendar.sass
  36. 2
      modules/backlogs/lib/open_project/backlogs/engine.rb
  37. 23
      modules/calendar/app/contracts/calendar/views/contract_strategy.rb
  38. 4
      modules/calendar/app/controllers/calendar/base_controller.rb
  39. 40
      modules/calendar/app/controllers/calendar/calendar_controller.rb
  40. 9
      modules/calendar/app/views/calendar/calendar/_menu.html.erb
  41. 0
      modules/calendar/app/views/calendar/calendar/index.html.erb
  42. 4
      modules/calendar/config/locales/en.yml
  43. 7
      modules/calendar/config/locales/js-en.yml
  44. 7
      modules/calendar/config/routes.rb
  45. 11
      modules/calendar/lib/open_project/calendar.rb
  46. 54
      modules/calendar/lib/open_project/calendar/engine.rb
  47. 1
      modules/calendar/lib/openproject-calendar.rb
  48. 13
      modules/calendar/openproject-calendar.gemspec
  49. 6
      modules/calendar/spec/controllers/calendar_controller_spec.rb
  50. 22
      modules/calendar/spec/features/calendars_spec.rb
  51. 143
      modules/calendar/spec/features/query_handling_spec.rb
  52. 16
      modules/calendar/spec/routing/calendar_routing_spec.rb
  53. 63
      modules/calendar/spec/support/pages/calendar.rb
  54. 1
      modules/dashboards/lib/dashboards/engine.rb
  55. 6
      modules/reporting/spec/features/menu_spec.rb
  56. 2
      modules/team_planner/lib/open_project/team_planner/engine.rb
  57. 14
      spec/contracts/views/create_contract_work_packages_table_spec.rb
  58. 8
      spec/factories/query_factory.rb
  59. 4
      spec/factories/view_factory.rb
  60. 2
      spec/features/homescreen/robots_spec.rb
  61. 39
      spec/requests/api/v3/views/create_resource_spec.rb
  62. 3
      spec_legacy/unit/lib/redmine_spec.rb

@ -68,6 +68,11 @@ PATH
specs:
budgets (1.0.0)
PATH
remote: modules/calendar
specs:
openproject-calendar (1.0.0)
PATH
remote: modules/costs
specs:
@ -1048,6 +1053,7 @@ DEPENDENCIES
openproject-backlogs!
openproject-bim!
openproject-boards!
openproject-calendar!
openproject-documents!
openproject-github_integration!
openproject-job_status!

@ -45,6 +45,7 @@ group :opf_plugins do
gem 'overviews', path: 'modules/overviews'
gem 'budgets', path: 'modules/budgets'
gem 'openproject-team_planner', path: 'modules/team_planner'
gem 'openproject-calendar', path: 'modules/calendar'
gem 'openproject-bim', path: 'modules/bim'
end

@ -34,5 +34,5 @@ Disallow: <%= project_work_packages_path(p) %>
Disallow: <%= project_activity_index_path(p) %>
<% end -%>
<% end %>
Disallow: /work_packages/calendar
Disallow: /calendar
Disallow: /activity

@ -436,11 +436,6 @@ Redmine::MenuManager.map :project_menu do |menu|
last: true,
caption: :label_all_open_wps
menu.push :calendar,
{ controller: '/work_packages/calendars', action: 'index' },
caption: :label_calendar,
icon: 'icon2 icon-calendar'
menu.push :news,
{ controller: '/news', action: 'index' },
caption: :label_news_plural,

@ -344,10 +344,5 @@ OpenProject::AccessControl.map do |map|
require: :loggedin
end
map.project_module :calendar, dependencies: :work_package_tracking do |cal|
cal.permission :view_calendar,
'work_packages/calendars': [:index]
end
map.project_module :activity
end

@ -2266,7 +2266,6 @@ en:
permission_select_custom_fields: "Select custom fields"
permission_select_project_modules: "Select project modules"
permission_manage_types: "Select types"
permission_view_calendar: "View calendar"
permission_view_changesets: "View repository revisions in OpenProject"
permission_view_commit_author_statistics: "View commit author statistics"
permission_view_work_package_watchers: "View watchers list"
@ -2306,7 +2305,6 @@ en:
project_module_activity: "Activity"
project_module_forums: "Forums"
project_module_calendar: "Calendar"
project_module_work_package_tracking: "Work package tracking"
project_module_news: "News"
project_module_repository: "Repository"

@ -106,10 +106,6 @@ en:
button_export-atom: "Download Atom"
button_create: "Create"
calendar:
title: 'Calendar'
too_many: 'There are %{count} work packages in total, but only %{max} can be shown.'
card:
add_new: 'Add new card'
highlighting:

@ -256,10 +256,6 @@ OpenProject::Application.routes.draw do
# work as a catchall for everything under /wiki
get 'wiki' => 'wiki#show'
namespace :work_packages do
resources :calendar, controller: 'calendars', only: [:index]
end
resources :work_packages, only: [] do
collection do
get '/report/:detail' => 'work_packages/reports#report_details'
@ -437,7 +433,6 @@ OpenProject::Application.routes.draw do
namespace :work_packages do
match 'auto_complete' => 'auto_completes#index', via: %i[get post]
resources :calendar, controller: 'calendars', only: [:index]
resource :bulk, controller: 'bulk', only: %i[edit update destroy]
# FIXME: this is kind of evil!! We need to remove this soonest and
# cover the functionality. Route is being used in work-package-service.js:331

@ -80,7 +80,11 @@ post:
Note that it is only allowed to provide properties or links supporting the write operation.
There are different subtypes of `Views` (e.g. `Views::WorkPackagesTable`) with each having its own
endpoint for creating that subtype e.g. `/api/v3/views/work_packages_table` for `Views::WorkPackagesTable` and `/api/v3/views/team_planner` for `Views::TeamPlanner`
endpoint for creating that subtype e.g.
* `/api/v3/views/work_packages_table` for `Views::WorkPackagesTable`
* `/api/v3/views/team_planner` for `Views::TeamPlanner`
* `/api/v3/views/work_packages_calendar` for `Views::WorkPackagesCalendar`
**Not yet implemented** To get the list of available subtypes and by that the endpoints for creating a subtype, use the
```

@ -46,7 +46,7 @@ import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openpro
import { OpenprojectRouterModule } from 'core-app/core/routing/openproject-router.module';
import { OpenprojectWorkPackageRoutesModule } from 'core-app/features/work-packages/openproject-work-package-routes.module';
import { BrowserModule } from '@angular/platform-browser';
import { OpenprojectCalendarModule } from 'core-app/shared/components/calendar/openproject-calendar.module';
import { OpenprojectCalendarModule } from 'core-app/features/calendar/openproject-calendar.module';
import { OpenprojectGlobalSearchModule } from 'core-app/core/global_search/openproject-global-search.module';
import { OpenprojectDashboardsModule } from 'core-app/features/dashboards/openproject-dashboards.module';
import { OpenprojectWorkPackageGraphsModule } from 'core-app/shared/components/work-package-graphs/openproject-work-package-graphs.module';

@ -133,7 +133,7 @@ export class PathHelperService {
}
public projectCalendarPath(projectId:string) {
return `${this.projectPath(projectId)}/work_packages/calendar`;
return `${this.projectPath(projectId)}/calendar`;
}
public projectMembershipsPath(projectId:string) {

@ -57,6 +57,7 @@ import {
redirectToMobileAlternative,
} from 'core-app/shared/helpers/routing/mobile-guard.helper';
import { TEAM_PLANNER_LAZY_ROUTES } from 'core-app/features/team-planner/team-planner/team-planner.lazy-routes';
import { CALENDAR_LAZY_ROUTES } from 'core-app/features/calendar/calendar.lazy-routes';
export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
{
@ -146,6 +147,7 @@ export const OPENPROJECT_ROUTES:Ng2StateDeclaration[] = [
...MY_ACCOUNT_LAZY_ROUTES,
...IAN_LAZY_ROUTES,
...TEAM_PLANNER_LAZY_ROUTES,
...CALENDAR_LAZY_ROUTES,
];
/**

@ -26,22 +26,13 @@
// See COPYRIGHT and LICENSE files for more details.
//++
import { Component, ViewChild } from '@angular/core';
import { WorkPackagesViewBase } from 'core-app/features/work-packages/routing/wp-view-base/work-packages-view.base';
import { WorkPackagesCalendarController } from 'core-app/shared/components/calendar/wp-calendar/wp-calendar.component';
import { Ng2StateDeclaration } from '@uirouter/angular';
@Component({
templateUrl: './wp-calendar-entry.component.html',
})
export class WorkPackagesCalendarEntryComponent extends WorkPackagesViewBase {
@ViewChild(WorkPackagesCalendarController, { static: true }) calendarElement:WorkPackagesCalendarController;
protected set loadingIndicator(promise:Promise<unknown>) {
this.loadingIndicatorService.indicator('calendar-entry').promise = promise;
}
public refresh(visibly:boolean, firstPage:boolean):Promise<unknown> {
return this.loadingIndicator = this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier);
}
}
export const CALENDAR_LAZY_ROUTES:Ng2StateDeclaration[] = [
{
name: 'calendar.**',
parent: 'optional_project',
url: '/calendar',
loadChildren: () => import('./openproject-calendar.module').then((m) => m.OpenprojectCalendarModule),
},
];

@ -0,0 +1,75 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2021 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 COPYRIGHT and LICENSE files for more details.
//++
import { Ng2StateDeclaration } from '@uirouter/angular';
import { makeSplitViewRoutes } from 'core-app/features/work-packages/routing/split-view-routes.template';
import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view.component';
import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routing/wp-base/wp--base.component';
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
import { WorkPackagesCalendarPageComponent } from 'core-app/features/calendar/wp-calendar-page/wp-calendar-page.component';
export const CALENDAR_ROUTES:Ng2StateDeclaration[] = [
{
name: 'calendar',
parent: 'optional_project',
url: '/calendar?query_id&query_props&cdate&cview',
redirectTo: 'calendar.page',
views: {
'!$default': { component: WorkPackagesBaseComponent },
},
params: {
query_id: { type: 'query', dynamic: true },
cdate: { type: 'string', dynamic: true },
cview: { type: 'string', dynamic: true },
// Use custom encoder/decoder that ensures validity of URL string
query_props: { type: 'opQueryString' },
},
},
{
name: 'calendar.page',
component: WorkPackagesCalendarPageComponent,
redirectTo: 'calendar.page.show',
data: {
bodyClasses: 'router--calendar',
},
},
{
name: 'calendar.page.show',
data: {
baseRoute: 'calendar.page.show',
},
views: {
'content-left': { component: WorkPackagesCalendarComponent },
},
},
...makeSplitViewRoutes(
'calendar.page.show',
undefined,
WorkPackageSplitViewComponent,
),
];

@ -28,10 +28,8 @@ import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/q
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { take } from 'rxjs/operators';
import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service';
import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource';
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import {
QueryProps,
QueryPropsFilter,
UrlParamsHelperService,
} from 'core-app/features/work-packages/components/wp-query/url-params-helper';
@ -201,7 +199,19 @@ export class OpCalendarService extends UntilDestroyedMixin {
// This is the case on initially loading the calendar with query_props present in the url params.
// There might also be a query_id but the settings persisted in it are overwritten by the props.
if (this.urlParams.query_props) {
queryProps = decodeURIComponent(this.urlParams.query_props || '');
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const oldQueryProps:{ [key:string]:unknown } = JSON.parse(this.urlParams.query_props);
// Update the date period of the calendar in the filter
const newQueryProps = {
...oldQueryProps,
f: [
...(oldQueryProps.f as QueryPropsFilter[]).filter((filter:QueryPropsFilter) => filter.n !== 'datesInterval'),
OpCalendarService.dateFilter(startDate, endDate),
],
};
queryProps = JSON.stringify(newQueryProps);
} else {
queryProps = OpCalendarService.defaultQueryProps(startDate, endDate);
}
@ -243,6 +253,7 @@ export class OpCalendarService extends UntilDestroyedMixin {
right: '',
},
initialDate: this.initialDate,
initialView: this.initialView,
datesSet: (dates) => this.updateDateParam(dates),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
eventClick: this.openSplitView.bind(this),
@ -254,6 +265,11 @@ export class OpCalendarService extends UntilDestroyedMixin {
private openSplitView(event:EventClickArg) {
const workPackage = event.event.extendedProps.workPackage as WorkPackageResource;
if (event.el) {
// do not display the tooltip on the wp show page
this.removeTooltip(event.el);
}
void this.$state.go(
`${splitViewRoute(this.$state)}.tabs`,
{ workPackageId: workPackage.id, tabIdentifier: 'overview' },
@ -347,10 +363,20 @@ export class OpCalendarService extends UntilDestroyedMixin {
return undefined;
}
private get initialView():string|undefined {
return this.urlParams.cview as string|undefined;
}
private updateDateParam(dates:DatesSetArg) {
void this.$state.go(
'.',
{ cdate: this.timezoneService.formattedISODate(dates.start) },
{
cdate: this.timezoneService.formattedISODate(dates.view.currentStart),
cview: dates.view.type,
},
{
custom: { notify: false },
},
);
}
}

@ -29,36 +29,21 @@
import { OPSharedModule } from 'core-app/shared/shared.module';
import { NgModule } from '@angular/core';
import { FullCalendarModule } from '@fullcalendar/angular';
import { WorkPackagesCalendarEntryComponent } from 'core-app/shared/components/calendar/wp-calendar-entry/wp-calendar-entry.component';
import { WorkPackagesCalendarController } from 'core-app/shared/components/calendar/wp-calendar/wp-calendar.component';
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
import { OpenprojectWorkPackagesModule } from 'core-app/features/work-packages/openproject-work-packages.module';
import { Ng2StateDeclaration, UIRouterModule } from '@uirouter/angular';
import { TimeEntryCalendarComponent } from 'core-app/shared/components/calendar/te-calendar/te-calendar.component';
import { UIRouterModule } from '@uirouter/angular';
import { TimeEntryCalendarComponent } from 'core-app/features/calendar/te-calendar/te-calendar.component';
import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module';
import { OpenprojectTimeEntriesModule } from 'core-app/shared/components/time_entries/openproject-time-entries.module';
const menuItemClass = 'calendar-menu-item';
export const CALENDAR_ROUTES:Ng2StateDeclaration[] = [
{
name: 'work-packages.calendar',
url: '/calendar',
component: WorkPackagesCalendarEntryComponent,
reloadOnSearch: false,
data: {
bodyClasses: 'router--work-packages-calendar',
menuItem: menuItemClass,
parent: 'work-packages',
},
},
];
import { WorkPackagesCalendarPageComponent } from 'core-app/features/calendar/wp-calendar-page/wp-calendar-page.component';
import { CALENDAR_ROUTES } from 'core-app/features/calendar/calendar.routes';
@NgModule({
imports: [
// Commons
OPSharedModule,
// Routes for /work_packages/calendar
// Routes for /calendar
UIRouterModule.forChild({ states: CALENDAR_ROUTES }),
// Work Package module
@ -75,12 +60,12 @@ export const CALENDAR_ROUTES:Ng2StateDeclaration[] = [
],
declarations: [
// Work package calendars
WorkPackagesCalendarEntryComponent,
WorkPackagesCalendarController,
WorkPackagesCalendarPageComponent,
WorkPackagesCalendarComponent,
TimeEntryCalendarComponent,
],
exports: [
WorkPackagesCalendarController,
WorkPackagesCalendarComponent,
TimeEntryCalendarComponent,
],
})

@ -39,7 +39,7 @@ import { FilterOperator } from 'core-app/shared/helpers/api-v3/api-v3-filter-bui
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';
import { OpCalendarService } from 'core-app/shared/components/calendar/op-calendar.service';
import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service';
interface CalendarViewEvent {
el:HTMLElement;

@ -0,0 +1,130 @@
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2021 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 COPYRIGHT and LICENSE files for more details.
//++
import {
ChangeDetectionStrategy,
Component,
ViewChild,
} from '@angular/core';
import { WorkPackagesCalendarComponent } from 'core-app/features/calendar/wp-calendar/wp-calendar.component';
import {
DynamicComponentDefinition,
PartitionedQuerySpacePageComponent,
ToolbarButtonComponentDefinition,
ViewPartitionState,
} from 'core-app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component';
import { WorkPackageFilterContainerComponent } from 'core-app/features/work-packages/components/filters/filter-container/filter-container.directive';
import { WorkPackageFilterButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-filter-button/wp-filter-button.component';
import { ZenModeButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component';
import { WorkPackageSettingsButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component';
import { QueryResource } from 'core-app/features/hal/resources/query-resource';
import { QueryParamListenerService } from 'core-app/features/work-packages/components/wp-query/query-param-listener.service';
@Component({
templateUrl: '../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html',
styleUrls: [
'../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.sass',
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
QueryParamListenerService,
],
})
export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePageComponent {
@ViewChild(WorkPackagesCalendarComponent, { static: true }) calendarElement:WorkPackagesCalendarComponent;
text = {
title: this.I18n.t('js.calendar.title'),
unsaved_title: this.I18n.t('js.calendar.unsaved_title'),
};
/** Go back using back-button */
backButtonCallback:() => void;
/** Current query title to render */
selectedTitle = this.text.unsaved_title;
filterContainerDefinition:DynamicComponentDefinition = {
component: WorkPackageFilterContainerComponent,
};
/** We need to pass the correct partition state to the view to manage the grid */
currentPartition:ViewPartitionState = '-split';
/** Show a toolbar */
showToolbar = true;
/** Toolbar is not editable */
titleEditingEnabled = false;
/** Savable */
showToolbarSaveButton = true;
/** Toolbar is always enabled */
toolbarDisabled = false;
/** Define the buttons shown in the toolbar */
toolbarButtonComponents:ToolbarButtonComponentDefinition[] = [
{
component: WorkPackageFilterButtonComponent,
},
{
component: ZenModeButtonComponent,
},
{
component: WorkPackageSettingsButtonComponent,
containerClasses: 'hidden-for-mobile',
show: ():boolean => this.authorisationService.can('query', 'updateImmediately'),
inputs: {
hideTableOptions: true,
},
},
];
/**
* We need to set the current partition to the grid to ensure
* either side gets expanded to full width if we're not in '-split' mode.
*
* @param state The current or entering state
*/
setPartition(state:{ data:{ partition?:ViewPartitionState } }):void {
this.currentPartition = state.data?.partition || '-split';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected staticQueryName(query:QueryResource):string {
return this.text.unsaved_title;
}
/**
* @protected
*/
protected loadInitialQuery():void {
// We never load the initial query as the calendar service does all that.
}
}

@ -20,7 +20,6 @@ import { StateService } from '@uirouter/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { ToastService } from 'core-app/shared/components/toaster/toast.service';
import { DomSanitizer } from '@angular/platform-browser';
import { WorkPackagesListChecksumService } from 'core-app/features/work-packages/components/wp-list/wp-list-checksum.service';
import { OpTitleService } from 'core-app/core/html/op-title.service';
import dayGridPlugin from '@fullcalendar/daygrid';
import {
@ -31,20 +30,19 @@ import { debounceTime } from 'rxjs/operators';
import { ConfigurationService } from 'core-app/core/config/configuration.service';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service';
import {
CalendarViewEvent,
OpCalendarService,
} from 'core-app/shared/components/calendar/op-calendar.service';
import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service';
import { Subject } from 'rxjs';
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
@Component({
templateUrl: './wp-calendar.template.html',
styleUrls: ['./wp-calendar.sass'],
selector: 'wp-calendar',
selector: 'op-wp-calendar',
providers: [
OpCalendarService,
],
})
export class WorkPackagesCalendarController extends UntilDestroyedMixin implements OnInit {
export class WorkPackagesCalendarComponent extends UntilDestroyedMixin implements OnInit {
@ViewChild(FullCalendarComponent) ucCalendar:FullCalendarComponent;
@ViewChild('ucCalendar', { read: ElementRef })
@ -52,11 +50,9 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
this.calendar.resizeObserver(v);
}
@Input() projectIdentifier:string;
@Input() static = false;
calendarOptions:CalendarOptions|undefined;
calendarOptions$ = new Subject<CalendarOptions>();
private alreadyLoaded = false;
@ -66,7 +62,6 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
readonly wpTableFilters:WorkPackageViewFiltersService,
readonly wpListService:WorkPackagesListService,
readonly querySpace:IsolatedQuerySpace,
readonly wpListChecksumService:WorkPackagesListChecksumService,
readonly schemaCache:SchemaCacheService,
readonly titleService:OpTitleService,
private element:ElementRef,
@ -75,6 +70,7 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
private sanitizer:DomSanitizer,
private configuration:ConfigurationService,
readonly calendar:OpCalendarService,
readonly currentProject:CurrentProjectService,
) {
super();
}
@ -112,7 +108,7 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
});
}
this.calendar.updateTimeframe(fetchInfo, this.projectIdentifier);
void this.calendar.updateTimeframe(fetchInfo, this.currentProject.identifier || undefined);
}
// eslint-disable-next-line @angular-eslint/use-lifecycle-interface
@ -122,64 +118,24 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
}
private initializeCalendar() {
void this.configuration.initialized
.then(() => {
this.calendarOptions = this.calendar.calendarOptions({
height: this.calendarHeight(),
headerToolbar: this.buildHeader(),
eventClick: this.toWPFullView.bind(this),
events: this.calendarEventsFunction.bind(this),
plugins: [dayGridPlugin],
initialView: (() => {
if (this.static) {
return 'dayGridWeek';
}
return undefined;
})(),
});
});
}
toWPFullView(event:CalendarViewEvent):void {
const { workPackage } = event.event.extendedProps;
if (event.el) {
// do not display the tooltip on the wp show page
this.calendar.removeTooltip(event.el);
}
// Ensure checksum is removed to allow queries to load
this.wpListChecksumService.clear();
// Ensure current calendar URL is pushed to history
window.history.pushState({}, this.titleService.current, window.location.href);
void this.$state.go(
'work-packages.show',
{ workPackageId: workPackage.id },
{ inherit: false },
);
}
private get calendarElement() {
return jQuery(this.element.nativeElement).find('[data-qa-selector="op-wp-calendar"]');
}
const additionalOptions:{ [key:string]:unknown } = {
height: '100%',
headerToolbar: this.buildHeader(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
events: this.calendarEventsFunction.bind(this),
plugins: [dayGridPlugin],
};
private calendarHeight():number {
if (this.static) {
let heightElement = jQuery(this.element.nativeElement);
while (!heightElement.height() && heightElement.parent()) {
heightElement = heightElement.parent();
}
const topOfCalendar = jQuery(this.element.nativeElement).position().top;
const topOfHeightElement = heightElement.position().top;
return heightElement.height()! - (topOfCalendar - topOfHeightElement);
additionalOptions.initialView = 'dayGridWeek';
}
// -12 for the bottom padding
return jQuery(window).height()! - this.calendarElement.offset()!.top - 12;
void this.configuration.initialized
.then(() => {
this.calendarOptions$.next(
this.calendar.calendarOptions(additionalOptions),
);
});
}
public buildHeader():false|ToolbarInput|undefined {
@ -193,40 +149,9 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
};
}
private setCalendarsDate():void {
const query = this.querySpace.query.value;
if (!query) {
return;
}
const datesIntervalFilter = _.find(query.filters || [], { id: 'datesInterval' }) as any;
let calendarDate:any = null;
let calendarUnit = 'dayGridMonth';
if (datesIntervalFilter) {
const lower = moment(datesIntervalFilter.values[0] as string);
const upper = moment(datesIntervalFilter.values[1] as string);
const diff = upper.diff(lower, 'days');
calendarDate = lower.add(diff / 2, 'days');
if (diff === 7) {
calendarUnit = 'dayGridWeek';
}
}
if (calendarDate) {
this.ucCalendar.getApi().changeView(calendarUnit, calendarDate.toDate());
} else {
this.ucCalendar.getApi().changeView(calendarUnit);
}
}
private setupWorkPackagesListener():void {
this.calendar.workPackagesListener$(() => {
this.alreadyLoaded = true;
this.setCalendarsDate();
this.ucCalendar.getApi().refetchEvents();
});
}
@ -255,4 +180,8 @@ export class WorkPackagesCalendarController extends UntilDestroyedMixin implemen
return events;
}
private get initialView():string|undefined {
return this.static ? 'dayGridWeek' : undefined;
}
}

@ -2,8 +2,8 @@
// Ensure some sane min-height
// to ensure fullcalendar has something to render
min-height: 200px
height: 100%
&--notification
font-style: italic
margin-top: 0.5rem

@ -0,0 +1,17 @@
<div class="op-wp-calendar"
data-qa-selector="op-wp-calendar"
[attr.data-indicator-name]="'table'">
<ng-container
*ngIf="(calendarOptions$ | async) as calendarOptions"
>
<full-calendar
#ucCalendar
*ngIf="calendarOptions"
[options]="calendarOptions"
></full-calendar>
</ng-container>
<div
*ngIf="static"
[textContent]="calendar.tooManyResultsText"
class="op-wp-calendar--notification"></div>
</div>

@ -104,7 +104,6 @@ export class TeamPlannerPageComponent extends PartitionedQuerySpacePageComponent
}
/**
* We only want to load the initial query if it is saved
* @protected
*/
protected loadInitialQuery():void {

@ -43,7 +43,7 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
import { ResourceLabelContentArg } from '@fullcalendar/resource-common';
import { OpCalendarService } from 'core-app/shared/components/calendar/op-calendar.service';
import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service';
import { WorkPackageCollectionResource } from 'core-app/features/hal/resources/wp-collection-resource';
@Component({
@ -128,7 +128,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
ngOnInit():void {
this.initializeCalendar();
this.projectIdentifier = this.currentProject.identifier ? this.currentProject.identifier : undefined;
this.projectIdentifier = this.currentProject.identifier || undefined;
this
.querySpace
@ -231,7 +231,6 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
},
events: this.calendarEventsFunction.bind(this) as unknown,
resources: [],
eventClick: this.openSplitView.bind(this) as unknown,
select: this.handleDateClicked.bind(this) as unknown,
resourceLabelContent: (data:ResourceLabelContentArg) => this.renderTemplate(this.resourceContent, data.resource.id, data),
resourceLabelWillUnmount: (data:ResourceLabelContentArg) => this.unrenderTemplate(data.resource.id),
@ -304,20 +303,6 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit,
}
}
private openSplitView(event:EventClickArg):void {
const workPackage = event.event.extendedProps.workPackage as WorkPackageResource;
if (event.el) {
// do not display the tooltip on the wp show page
this.calendar.removeTooltip(event.el);
}
void this.$state.go(
`${splitViewRoute(this.$state)}.tabs`,
{ workPackageId: workPackage.id, tabIdentifier: 'overview' },
);
}
private mapToCalendarEvents(workPackages:WorkPackageResource[]):EventInput[] {
return workPackages
.map((workPackage:WorkPackageResource):EventInput|undefined => {

@ -42,6 +42,9 @@ export class WorkPackagesQueryViewService {
if (this.$state.includes('bim')) {
return 'bim';
}
if (this.$state.includes('calendar')) {
return 'work_packages_calendar';
}
throw new Error('Not on a path defined for query views');
}

@ -1,27 +0,0 @@
<div class="work-packages-calendar-view--container loading-indicator--location"
data-indicator-name="calendar-entry">
<div class="toolbar-container -editable">
<div class="toolbar">
<div class="title-container">
<h2>
{{ I18n.t('js.calendar.title') }}
</h2>
</div>
<ul class="toolbar-items hide-when-print">
<li class="toolbar-item hidden-for-mobile">
<wp-filter-button>
</wp-filter-button>
</li>
<li class="toolbar-item hidden-for-mobile">
<zen-mode-toggle-button>
</zen-mode-toggle-button>
</li>
</ul>
</div>
</div>
<filter-container></filter-container>
<wp-calendar [projectIdentifier]="projectIdentifier"></wp-calendar>
</div>

@ -1,14 +0,0 @@
<!-- position: relative added in order for the loading indicator to be positioned correctly -->
<div class="op-wp-calendar loading-indicator--location"
data-qa-selector="op-wp-calendar"
[attr.data-indicator-name]="'table'"
style="position: relative">
<full-calendar #ucCalendar
*ngIf="calendarOptions"
[options]="calendarOptions">
</full-calendar>
<div
*ngIf="static"
[textContent]="calendar.tooManyResultsText"
class="op-wp-calendar--notification"></div>
</div>

@ -31,7 +31,7 @@ import { DynamicModule } from 'ng-dynamic-component';
import { HookService } from 'core-app/features/plugins/hook-service';
import { OPSharedModule } from 'core-app/shared/shared.module';
import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module';
import { OpenprojectCalendarModule } from 'core-app/shared/components/calendar/openproject-calendar.module';
import { OpenprojectCalendarModule } from 'core-app/features/calendar/openproject-calendar.module';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { DragDropModule } from '@angular/cdk/drag-drop';

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { DisplayedDays } from 'core-app/shared/components/calendar/te-calendar/te-calendar.component';
import { DisplayedDays } from 'core-app/features/calendar/te-calendar/te-calendar.component';
@Injectable()
export class TimeEntriesCurrentUserConfigurationModalService {

@ -6,7 +6,7 @@ import { CollectionResource } from 'core-app/features/hal/resources/collection-r
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
import { AbstractWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-widget.component';
import { DisplayedDays } from 'core-app/shared/components/calendar/te-calendar/te-calendar.component';
import { DisplayedDays } from 'core-app/features/calendar/te-calendar/te-calendar.component';
import { TimezoneService } from 'core-app/core/datetime/timezone.service';
@Component({

@ -9,7 +9,6 @@
</widget-header>
<ng-container wp-isolated-query-space>
<wp-calendar
[static]="true"
[projectIdentifier]="projectIdentifier"></wp-calendar>
<op-wp-calendar
[static]="true"></op-wp-calendar>
</ng-container>

@ -0,0 +1,3 @@
.router--calendar
#content
height: 100%

@ -4,6 +4,7 @@
@import 'avatars'
@import 'bim'
@import 'boards'
@import 'calendar'
@import 'costs'
@import 'documents'
@import '2fa'

@ -35,6 +35,7 @@
&:before,
&:after
padding-right: 4px
pointer-events: none
.fc-event-title-container
margin: 0 16px

@ -106,7 +106,7 @@ module OpenProject::Backlogs
:backlogs,
{ controller: '/rb_master_backlogs', action: :index },
caption: :project_module_backlogs,
before: :calendar,
after: :work_packages,
icon: 'icon2 icon-backlogs'
menu :project_menu,

@ -0,0 +1,23 @@
module ::Calendar
module Views
class ContractStrategy < ::BaseContract
validate :manageable
private
def manageable
return if model.query.blank?
errors.add(:base, :error_unauthorized) unless query_permissions?
end
def query_permissions?
user_allowed_on_query?(:view_calendar)
end
def user_allowed_on_query?(permission)
user.allowed_to?(permission, model.query.project, global: model.query.project.nil?)
end
end
end
end

@ -0,0 +1,4 @@
module ::Calendar
class BaseController < ::ApplicationController
end
end

@ -0,0 +1,40 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 COPYRIGHT and LICENSE files for more details.
#++
module ::Calendar
class CalendarController < ApplicationController
menu_item :calendar_view
before_action :find_optional_project
def index
render layout: 'angular/angular'
end
end
end

@ -0,0 +1,9 @@
<%=
angular_component_tag 'op-view-select',
inputs: {
projectId: (@project ? @project.id.to_s : ''),
menuItems: [parent_name, name],
baseRoute: 'calendar.page.show',
viewType: 'WorkPackagesCalendar',
}
%>

@ -0,0 +1,4 @@
# English strings go here
en:
permission_view_calendar: "View calendar"
project_module_calendar_view: "Calendar"

@ -0,0 +1,7 @@
# English strings go here
en:
js:
calendar:
title: 'Calendar'
too_many: 'There are %{count} work packages in total, but only %{max} can be shown.'
unsaved_title: 'Unnamed calendar'

@ -0,0 +1,7 @@
OpenProject::Application.routes.draw do
scope 'projects/:project_id', as: 'project' do
get '/calendar(/*state)', to: 'calendar/calendar#index', as: :calendar
end
get '/calendar(/*state)', to: 'calendar/calendar#index', as: :calendar
end

@ -1,5 +1,3 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH
@ -28,11 +26,8 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class WorkPackages::CalendarsController < ApplicationController
menu_item :calendar
before_action :find_optional_project
def index
render layout: 'angular/angular'
module OpenProject
module Calendar
require 'open_project/calendar/engine'
end
end

@ -0,0 +1,54 @@
# OpenProject Calendar module
#
# Copyright (C) 2021 OpenProject GmbH
#
# 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.
module OpenProject::Calendar
class Engine < ::Rails::Engine
engine_name :openproject_calendar
include OpenProject::Plugins::ActsAsOpEngine
register 'openproject-calendar',
author_url: 'https://www.openproject.org',
bundled: true,
settings: {},
name: 'OpenProject Calendar' do
project_module :calendar_view, dependencies: :work_package_tracking do
permission :view_calendar,
{ 'calendar/calendar': %i[index] }
end
menu :project_menu,
:calendar_view,
{ controller: '/calendar/calendar', action: 'index' },
caption: :label_calendar,
icon: 'icon2 icon-calendar',
after: :work_packages
menu :project_menu,
:calendar_menu,
{ controller: '/calendar/calendar', action: 'index' },
parent: :calendar_view,
partial: 'calendar/calendar/menu',
last: true,
caption: :label_calendar
end
add_view :WorkPackagesCalendar,
contract_strategy: 'Calendar::Views::ContractStrategy'
end
end

@ -0,0 +1 @@
require 'open_project/calendar'

@ -0,0 +1,13 @@
# encoding: UTF-8
Gem::Specification.new do |s|
s.name = 'openproject-calendar'
s.version = '1.0.0'
s.authors = 'OpenProject GmbH'
s.email = 'info@openproject.com'
s.summary = 'OpenProject Calendar'
s.description = 'Provides calendar views'
s.license = 'GPLv3'
s.files = Dir['{app,config,db,lib}/**/*']
end

@ -28,7 +28,7 @@
require 'spec_helper'
describe WorkPackages::CalendarsController, type: :controller do
describe Calendar::CalendarController, type: :controller do
let(:project) do
FactoryBot.build_stubbed(:project).tap do |p|
allow(Project)
@ -42,7 +42,7 @@ describe WorkPackages::CalendarsController, type: :controller do
FactoryBot.build_stubbed(:user).tap do |user|
allow(user)
.to receive(:allowed_to?) do |permission, p, global:|
permission[:controller] == 'work_packages/calendars' &&
permission[:controller] == 'calendar/calendar' &&
permission[:action] == 'index' &&
(p.nil? || p == project)
end
@ -57,7 +57,7 @@ describe WorkPackages::CalendarsController, type: :controller do
it { is_expected.to be_successful }
it { is_expected.to render_template('work_packages/calendars/index') }
it { is_expected.to render_template('calendar/calendar/index') }
end
context 'cross-project' do

@ -66,19 +66,21 @@ describe 'Work package calendars', type: :feature, js: true do
due_date: Date.today.at_beginning_of_month.next_month + 18.days)
end
let(:filters) { ::Components::WorkPackages::Filters.new }
let(:current_wp_split_screen) { Pages::SplitWorkPackage.new(current_work_package, project) }
before do
login_as(user)
end
it 'navigates to today, allows filtering, switching the view and retrains the state' do
pending 'To be fixed in https://github.com/opf/openproject/pull/9977'
visit project_path(project)
within '#main-menu' do
click_link 'Calendar'
end
loading_indicator_saveguard
# should open the calendar with the current month displayed
expect(page)
.to have_selector '.fc-event-title', text: current_work_package.subject
@ -169,30 +171,26 @@ describe 'Work package calendars', type: :feature, js: true do
expect(page)
.to have_no_selector '.fc-event-title', text: another_future_work_package.subject
# click goes to work package show page
# click goes to work package split screen
page.find('.fc-event-title', text: current_work_package.subject).click
expect(page)
.to have_selector('.subject-header', text: current_work_package.subject)
current_wp_split_screen.expect_open
# Going back in browser history will lead us back to the calendar
# Regression #29664
page.go_back
# click goes to work package show page
expect(page)
.to have_selector('.fc-event-title', text: current_work_package.subject, wait: 20)
current_wp_split_screen.expect_closed
# click goes to work package show page again
# click goes to work package split screen page again
page.find('.fc-event-title', text: current_work_package.subject).click
expect(page)
.to have_selector('.subject-header', text: current_work_package.subject)
current_wp_split_screen.expect_open
# click back goes back to calendar
page.find('.work-packages-back-button').click
current_wp_split_screen.close
expect(page)
.to have_selector '.fc-event-title', text: current_work_package.subject, wait: 20
current_wp_split_screen.expect_closed
end
end

@ -0,0 +1,143 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 COPYRIGHT and LICENSE files for more details.
#++
require 'spec_helper'
require_relative '../support/pages/calendar'
require_relative '../../../../spec/features/views/shared_examples'
describe 'Calendar query handling', type: :feature, js: true do
shared_let(:type_task) { FactoryBot.create(:type_task) }
shared_let(:type_bug) { FactoryBot.create(:type_bug) }
shared_let(:project) do
FactoryBot.create(:project,
enabled_module_names: %w[work_package_tracking calendar_view],
types: [type_task, type_bug])
end
shared_let(:user) do
FactoryBot.create :user,
member_in_project: project,
member_with_permissions: %w[
view_work_packages
edit_work_packages
save_queries
save_public_queries
view_calendar
]
end
shared_let(:task) do
FactoryBot.create :work_package,
project: project,
type: type_task,
assigned_to: user,
start_date: Time.zone.today - 1.day,
due_date: Time.zone.today + 1.day,
subject: 'A task for the user'
end
shared_let(:bug) do
FactoryBot.create :work_package,
project: project,
type: type_bug,
assigned_to: user,
start_date: Time.zone.today - 1.day,
due_date: Time.zone.today + 1.day,
subject: 'A bug for the user'
end
shared_let(:saved_query) do
FactoryBot.create(:query_with_view_work_packages_calendar,
project: project,
public: true)
end
let(:calendar_page) { ::Pages::Calendar.new project }
let(:work_package_page) { ::Pages::WorkPackagesTable.new project }
let(:query_title) { ::Components::WorkPackages::QueryTitle.new }
let(:query_menu) { ::Components::WorkPackages::QueryMenu.new }
let(:filters) { calendar_page.filters }
current_user { user }
before do
login_as user
calendar_page.visit!
loading_indicator_saveguard
calendar_page.expect_event task
calendar_page.expect_event bug
end
it 'allows saving the calendar' do
filters.expect_filter_count("1")
filters.open
filters.add_filter_by('Type', 'is', [type_bug.name])
filters.expect_filter_count("2")
calendar_page.expect_event bug
calendar_page.expect_event task, present: false
query_title.expect_changed
query_title.press_save_button
calendar_page.expect_and_dismiss_toaster(message: I18n.t('js.notice_successful_create'))
end
it 'shows only calendar queries' do
# Go to calendar where a query is already shown
query_menu.expect_menu_entry saved_query.name
# Change filter
filters.open
filters.add_filter_by('Type', 'is', [type_bug.name])
filters.expect_filter_count("2")
# Save current filters
query_title.expect_changed
query_title.rename 'I am your Query'
calendar_page.expect_and_dismiss_toaster(message: I18n.t('js.notice_successful_create'))
# The saved query appears in the side menu...
query_menu.expect_menu_entry 'I am your Query'
query_menu.expect_menu_entry saved_query.name
# .. but not in the work packages module
work_package_page.visit!
query_menu.expect_menu_entry_not_visible 'I am your Query'
end
it_behaves_like 'module specific query view management' do
let(:module_page) { calendar_page }
let(:default_name) { 'Unnamed calendar' }
end
end

@ -28,15 +28,15 @@
require 'spec_helper'
describe WorkPackages::CalendarsController, type: :routing do
it 'should connect GET /work_packages/calendar to work_package/calendar#index' do
expect(get('/work_packages/calendar')).to route_to(controller: 'work_packages/calendars',
action: 'index')
describe Calendar::CalendarController, type: :routing do
it 'connects GET /calendar to calendar#index' do
expect(get('/calendar')).to route_to(controller: 'calendar/calendar',
action: 'index')
end
it 'should connect GET /project/1/work_packages/calendar to work_package/calendar#index' do
expect(get('/projects/1/work_packages/calendar')).to route_to(controller: 'work_packages/calendars',
action: 'index',
project_id: '1')
it 'connects GET /project/1/calendar to calendar#index' do
expect(get('/projects/1/calendar')).to route_to(controller: 'calendar/calendar',
action: 'index',
project_id: '1')
end
end

@ -0,0 +1,63 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2021 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 COPYRIGHT and LICENSE files for more details.
#++
require 'support/pages/page'
module Pages
class Calendar < ::Pages::Page
attr_reader :project,
:filters
def initialize(project)
super()
@project = project
@filters = ::Components::WorkPackages::Filters.new
end
def path
project_calendar_path(project)
end
def expect_title(title = 'Unnamed calendar')
expect(page).to have_selector '.editable-toolbar-title--fixed', text: title
end
def expect_event(work_package, present: true)
expect(page).to have_conditional_selector(present, '.fc-event', text: work_package.subject)
end
def open_split_view(work_package)
page
.find('.fc-event', text: work_package.subject)
.click
::Pages::SplitWorkPackage.new(work_package, project)
end
end
end

@ -14,7 +14,6 @@ module Dashboards
{ controller: '/dashboards/dashboards', action: 'show' },
caption: :'dashboards.label',
after: :work_packages,
before: :calendar,
icon: 'icon2 icon-status',
badge: 'label_menu_badge.alpha')
end

@ -50,7 +50,7 @@ describe 'project menu', type: :feature do
# `url_for controller: 'cost_reports'` will yield different results ...
#
# when on `/projects/ponyo/work_packages`: `/projects/ponyo/cost_reports` (correct)
# when on `/projects/ponyo/work_packages/calendar`: `/work_packages/cost_reports?project_id=ponyo`
# when on `/projects/ponyo/calendar`: `/work_packages/cost_reports?project_id=ponyo`
#
# This is only relevant for project menu entries, not global ones (`project_id` param is nil)*.
# Meaning that you have to make sure to force the absolute URL in a project menu entry
@ -80,7 +80,7 @@ describe 'project menu', type: :feature do
end
context "when on the project's calendar" do
let(:current_path) { '/projects/ponyo/work_packages/calendar' }
let(:current_path) { '/projects/ponyo/calendar' }
it_behaves_like 'it leads to the project costs reports'
end
@ -111,7 +111,7 @@ describe 'project menu', type: :feature do
end
context "when on the project's calendar" do
let(:current_path) { '/projects/ponyo/work_packages/calendar' }
let(:current_path) { '/projects/ponyo/calendar' }
it_behaves_like 'it leads to the cost reports'
end

@ -40,7 +40,7 @@ module OpenProject::TeamPlanner
:team_planner_view,
{ controller: '/team_planner/team_planner', action: :index },
caption: :'team_planner.label_team_planner',
after: :backlogs,
after: :work_packages,
icon: 'icon2 icon-calendar',
badge: 'label_menu_badge.pre_alpha'

@ -55,6 +55,20 @@ describe Views::CreateContract do
it_behaves_like 'contract is invalid', type: :inclusion
end
context 'with a work_packages_calendar view with the user having the permission to view_calendar' do
let(:permissions) { %i[view_work_packages save_queries view_calendar] }
let(:view_type) { 'work_packages_calendar' }
it_behaves_like 'contract is valid'
end
context 'with a work_packages_calendar view with the user not having the permission to view_calendar' do
let(:permissions) { %i[view_work_packages save_queries] }
let(:view_type) { 'work_packages_calendar' }
it_behaves_like 'contract is invalid', base: :error_unauthorized
end
end
end
end

@ -64,6 +64,14 @@ FactoryBot.define do
end
end
factory :query_with_view_work_packages_calendar do
sequence(:name) { |n| "Calendar query #{n}" }
callback(:after_create) do |query|
FactoryBot.create(:view_work_packages_calendar, query: query)
end
end
callback(:after_build) { |query| query.add_default_filter }
end
end

@ -37,4 +37,8 @@ FactoryBot.define do
factory :view_team_planner, parent: :view do
type { 'team_planner' }
end
factory :view_work_packages_calendar, parent: :view do
type { 'work_packages_calendar' }
end
end

@ -36,7 +36,7 @@ describe 'robots.txt', type: :feature do
end
it 'disallows global paths and paths from public project' do
expect(page).to have_content('Disallow: /work_packages/calendar')
expect(page).to have_content('Disallow: /calendar')
expect(page).to have_content('Disallow: /activity')
expect(page).to have_content("Disallow: /projects/#{project.identifier}/repository")

@ -107,6 +107,45 @@ describe ::API::V3::Views::ViewsAPI,
end
end
describe 'POST /api/v3/views/work_packages_calendar' do
let(:send_request) do
post api_v3_paths.views_type('work_packages_calendar'), body
end
context 'with a user allowed to save the query and see the calendar' do
let(:additional_setup) do
role.update_attribute(:permissions, role.permissions + [:view_calendar])
end
it 'returns 201 CREATED' do
expect(response.status)
.to eq(201)
end
it 'returns the view' do
expect(response.body)
.to be_json_eql('Views::WorkPackagesCalendar'.to_json)
.at_path('_type')
expect(response.body)
.to be_json_eql(View.last.id.to_json)
.at_path('id')
end
end
context 'with a user allowed to save the query but not to view calendars' do
it_behaves_like 'unauthorized access'
end
context 'with a user not allowed to see the query' do
let(:additional_setup) do
role.update_attribute(:permissions, [])
end
it_behaves_like 'unauthorized access'
end
end
describe 'POST /api/v3/views/bogus' do
let(:send_request) do
post api_v3_paths.views_type('bogus'), body

@ -74,12 +74,11 @@ describe Redmine do
end
it 'should project_menu' do
assert_number_of_items_in_menu :project_menu, 13
assert_number_of_items_in_menu :project_menu, 12
assert_menu_contains_item_named :project_menu, :overview
assert_menu_contains_item_named :project_menu, :activity
assert_menu_contains_item_named :project_menu, :roadmap
assert_menu_contains_item_named :project_menu, :work_packages
assert_menu_contains_item_named :project_menu, :calendar
assert_menu_contains_item_named :project_menu, :news
assert_menu_contains_item_named :project_menu, :forums
assert_menu_contains_item_named :project_menu, :repository

Loading…
Cancel
Save