diff --git a/app/components/open_project/common/submenu_component.html.erb b/app/components/open_project/common/submenu_component.html.erb
index 659a2f60a496..35e4f8e2a954 100644
--- a/app/components/open_project/common/submenu_component.html.erb
+++ b/app/components/open_project/common/submenu_component.html.erb
@@ -3,6 +3,7 @@
<% end %>
+
+ <% if @create_btn_options.present? %>
+
+ <% end %>
diff --git a/app/components/open_project/common/submenu_component.rb b/app/components/open_project/common/submenu_component.rb
index d786f62c1ad7..e0cdcf88ea15 100644
--- a/app/components/open_project/common/submenu_component.rb
+++ b/app/components/open_project/common/submenu_component.rb
@@ -31,10 +31,11 @@
module OpenProject
module Common
class SubmenuComponent < ApplicationComponent
- def initialize(sidebar_menu_items: nil, searchable: false)
+ def initialize(sidebar_menu_items: nil, searchable: false, create_btn_options: nil)
super()
@sidebar_menu_items = sidebar_menu_items
@searchable = searchable
+ @create_btn_options = create_btn_options
end
def render?
diff --git a/app/menus/submenu.rb b/app/menus/submenu.rb
new file mode 100644
index 000000000000..e2866c67922f
--- /dev/null
+++ b/app/menus/submenu.rb
@@ -0,0 +1,111 @@
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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.
+# ++
+class Submenu
+ include Rails.application.routes.url_helpers
+ attr_reader :view_type, :project, :params
+
+ def initialize(view_type:, project: nil, params: nil)
+ @view_type = view_type
+ @project = project
+ @params = params
+ end
+
+ def menu_items
+ [
+ OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_starred_queries"), children: starred_queries),
+ OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_default_queries"), children: default_queries),
+ OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_global_queries"), children: global_queries),
+ OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_custom_queries"), children: custom_queries)
+ ]
+ end
+
+ def starred_queries
+ base_query
+ .where("starred" => "t")
+ .pluck(:id, :name)
+ .map { |id, name| menu_item(query_params(id), name) }
+ end
+
+ def default_queries
+ raise NotImplementedError
+ end
+
+ def global_queries
+ base_query
+ .where("starred" => "f")
+ .where("public" => "t")
+ .pluck(:id, :name)
+ .map { |id, name| menu_item(query_params(id), name) }
+ end
+
+ def custom_queries
+ base_query
+ .where("starred" => "f")
+ .where("public" => "f")
+ .pluck(:id, :name)
+ .map { |id, name| menu_item(query_params(id), name) }
+ end
+
+ def base_query
+ base_query ||= Query
+ .visible(User.current)
+ .includes(:project)
+ .joins(:views)
+ .where("views.type" => view_type)
+
+ if project.present?
+ base_query.where("queries.project_id" => project.id)
+ else
+ base_query.where("queries.project_id" => nil)
+ end
+ end
+
+ def query_params(id)
+ { query_id: id }
+ end
+
+ def menu_item(query_params, name)
+ OpenProject::Menu::MenuItem.new(title: name,
+ href: query_path(query_params),
+ selected: selected?(query_params))
+ end
+
+ def selected?(query_params)
+ query_params.each_key do |filter_key|
+ if params[filter_key] != query_params[filter_key].to_s
+ return false
+ end
+ end
+
+ true
+ end
+
+ def query_path(query_params)
+ raise NotImplementedError
+ end
+end
diff --git a/frontend/src/app/core/setup/global-dynamic-components.const.ts b/frontend/src/app/core/setup/global-dynamic-components.const.ts
index fab9e31a012a..a4ad79800833 100644
--- a/frontend/src/app/core/setup/global-dynamic-components.const.ts
+++ b/frontend/src/app/core/setup/global-dynamic-components.const.ts
@@ -147,10 +147,6 @@ import {
opTeamPlannerSidemenuSelector,
TeamPlannerSidemenuComponent,
} from 'core-app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component';
-import {
- CalendarSidemenuComponent,
- opCalendarSidemenuSelector,
-} from 'core-app/features/calendar/sidemenu/calendar-sidemenu.component';
import {
OpModalOverlayComponent,
opModalOverlaySelector,
@@ -218,7 +214,6 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [
{ selector: wpOverviewGraphSelector, cls: WorkPackageOverviewGraphComponent },
{ selector: opViewSelectSelector, cls: ViewSelectComponent },
{ selector: opTeamPlannerSidemenuSelector, cls: TeamPlannerSidemenuComponent },
- { selector: opCalendarSidemenuSelector, cls: CalendarSidemenuComponent },
{ selector: triggerActionsEntryComponentSelector, cls: TriggerActionsEntryComponent, embeddable: true },
{ selector: editableQueryPropsSelector, cls: EditableQueryPropsComponent },
{ selector: backupSelector, cls: BackupComponent },
diff --git a/frontend/src/app/features/bim/ifc_models/pages/viewer/styles/tabs.sass b/frontend/src/app/features/bim/ifc_models/pages/viewer/styles/tabs.sass
index c955b49b712a..cf318a0125fd 100644
--- a/frontend/src/app/features/bim/ifc_models/pages/viewer/styles/tabs.sass
+++ b/frontend/src/app/features/bim/ifc_models/pages/viewer/styles/tabs.sass
@@ -138,7 +138,7 @@ $pill-padding-left: 8px
// Workaround to hide the actual text of the link and show our icons instead
color: var(--main-menu-bg-color)
- .main-menu &
+ .main-menu &:not(.Button)
// reset main menu indents
padding: initial
diff --git a/frontend/src/app/features/calendar/calendar.routes.ts b/frontend/src/app/features/calendar/calendar.routes.ts
index fa2adbd0d726..0eb385cfc33d 100644
--- a/frontend/src/app/features/calendar/calendar.routes.ts
+++ b/frontend/src/app/features/calendar/calendar.routes.ts
@@ -33,6 +33,13 @@ import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routi
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 sidemenuId = 'calendar_sidemenu';
+export const sideMenuOptions = {
+ sidemenuId,
+ hardReloadOnBaseRoute: true,
+ defaultQuery: 'new',
+};
+
export const CALENDAR_ROUTES:Ng2StateDeclaration[] = [
{
name: 'calendar',
@@ -56,12 +63,14 @@ export const CALENDAR_ROUTES:Ng2StateDeclaration[] = [
redirectTo: 'calendar.page.show',
data: {
bodyClasses: 'router--calendar',
+ sideMenuOptions,
},
},
{
name: 'calendar.page.show',
data: {
baseRoute: 'calendar.page.show',
+ sideMenuOptions,
},
views: {
'content-left': { component: WorkPackagesCalendarComponent },
diff --git a/frontend/src/app/features/calendar/openproject-calendar.module.ts b/frontend/src/app/features/calendar/openproject-calendar.module.ts
index b805f628e12e..0a89425a355c 100644
--- a/frontend/src/app/features/calendar/openproject-calendar.module.ts
+++ b/frontend/src/app/features/calendar/openproject-calendar.module.ts
@@ -38,7 +38,6 @@ import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openp
import { OpenprojectTimeEntriesModule } from 'core-app/shared/components/time_entries/openproject-time-entries.module';
import { WorkPackagesCalendarPageComponent } from 'core-app/features/calendar/wp-calendar-page/wp-calendar-page.component';
import { CALENDAR_ROUTES } from 'core-app/features/calendar/calendar.routes';
-import { CalendarSidemenuComponent } from './sidemenu/calendar-sidemenu.component';
import { QueryGetIcalUrlModalComponent } from 'core-app/shared/components/modals/get-ical-url-modal/query-get-ical-url.modal';
@NgModule({
@@ -69,7 +68,6 @@ import { QueryGetIcalUrlModalComponent } from 'core-app/shared/components/modals
WorkPackagesCalendarPageComponent,
WorkPackagesCalendarComponent,
TimeEntryCalendarComponent,
- CalendarSidemenuComponent,
QueryGetIcalUrlModalComponent,
],
exports: [
diff --git a/frontend/src/app/features/calendar/sidemenu/calendar-sidemenu.component.html b/frontend/src/app/features/calendar/sidemenu/calendar-sidemenu.component.html
deleted file mode 100644
index d88f3b1f2e2c..000000000000
--- a/frontend/src/app/features/calendar/sidemenu/calendar-sidemenu.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
diff --git a/frontend/src/app/features/calendar/sidemenu/calendar-sidemenu.component.ts b/frontend/src/app/features/calendar/sidemenu/calendar-sidemenu.component.ts
deleted file mode 100644
index 340983619356..000000000000
--- a/frontend/src/app/features/calendar/sidemenu/calendar-sidemenu.component.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import {
- ChangeDetectionStrategy,
- Component,
- ElementRef,
- HostBinding,
- Input,
-} from '@angular/core';
-import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin';
-import { CurrentUserService } from 'core-app/core/current-user/current-user.service';
-import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
-import { I18nService } from 'core-app/core/i18n/i18n.service';
-import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs';
-
-export const opCalendarSidemenuSelector = 'op-calendar-sidemenu';
-
-@Component({
- selector: opCalendarSidemenuSelector,
- templateUrl: './calendar-sidemenu.component.html',
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class CalendarSidemenuComponent extends UntilDestroyedMixin {
- @HostBinding('class.op-sidebar') className = true;
-
- @Input() menuItems:string[] = [];
-
- @Input() projectId:string|undefined;
-
- canCreateCalendar$ = this
- .currentUserService
- .hasCapabilities$(
- 'calendars/create',
- this.currentProjectService.id || null,
- )
- .pipe(this.untilDestroyed());
-
- text = {
- calendar: this.I18n.t('js.calendar.title'),
- create_new_calendar: this.I18n.t('js.calendar.create_new'),
- };
-
- createButton = {
- title: this.text.calendar,
- uiSref: 'calendar.page.show',
- uiParams: {
- query_id: null,
- query_props: '',
- },
- };
-
- constructor(
- readonly elementRef:ElementRef,
- readonly currentUserService:CurrentUserService,
- readonly currentProjectService:CurrentProjectService,
- readonly I18n:I18nService,
- ) {
- super();
-
- populateInputsFromDataset(this);
- }
-}
diff --git a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts
index 6298811f228c..7c682c59487a 100644
--- a/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts
+++ b/frontend/src/app/features/work-packages/components/wp-list/wp-list.service.ts
@@ -431,10 +431,21 @@ export class WorkPackagesListService {
}
private navigateToDefaultQuery(query:QueryResource):void {
- const { hardReloadOnBaseRoute } = this.$state.$current.data as { hardReloadOnBaseRoute?:boolean };
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ const sideMenuOptions = this.$state.$current.data?.sideMenuOptions as { hardReloadOnBaseRoute?:boolean, defaultQuery?:string };
+ const hardReloadOnBaseRoute = sideMenuOptions?.hardReloadOnBaseRoute;
if (hardReloadOnBaseRoute) {
const url = new URL(window.location.href);
+ const defaultQuery = sideMenuOptions.defaultQuery;
+
+ // If there is a default query passed, we replace the hard coded ids with the default query
+ // e.g. calendars/:id, team_planner/:id, ...
+ // Otherwise, we will just delete the search params
+ if (defaultQuery) {
+ url.pathname = url.pathname.replace(/\d+$/, defaultQuery);
+ }
+
url.search = '';
window.location.href = url.href;
} else {
@@ -451,7 +462,8 @@ export class WorkPackagesListService {
}
private reloadSidemenu(selectedQueryId:string|null):void {
- const menuIdentifier:string|undefined = this.$state.current.data.sidemenuId;
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
+ const menuIdentifier:string|undefined = this.$state.current.data.sideMenuOptions?.sidemenuId;
if (menuIdentifier) {
const menu = (document.getElementById(menuIdentifier) as HTMLElement&TurboElement);
diff --git a/frontend/src/app/features/work-packages/routing/work-packages-gantt-routes.ts b/frontend/src/app/features/work-packages/routing/work-packages-gantt-routes.ts
index dfb76626e101..646a6e054606 100644
--- a/frontend/src/app/features/work-packages/routing/work-packages-gantt-routes.ts
+++ b/frontend/src/app/features/work-packages/routing/work-packages-gantt-routes.ts
@@ -36,6 +36,10 @@ import { makeSplitViewRoutes } from 'core-app/features/work-packages/routing/spl
export const menuItemClass = 'gantt-menu-item';
export const sidemenuId = 'gantt_menu';
+export const sideMenuOptions = {
+ sidemenuId,
+ hardReloadOnBaseRoute: true,
+};
export const WORK_PACKAGES_GANTT_ROUTES:Ng2StateDeclaration[] = [
{
@@ -49,8 +53,7 @@ export const WORK_PACKAGES_GANTT_ROUTES:Ng2StateDeclaration[] = [
data: {
bodyClasses: 'router--work-packages-base',
menuItem: menuItemClass,
- sidemenuId,
- hardReloadOnBaseRoute: true,
+ sideMenuOptions,
},
params: {
query_id: { type: 'query', dynamic: true },
@@ -68,8 +71,7 @@ export const WORK_PACKAGES_GANTT_ROUTES:Ng2StateDeclaration[] = [
data: {
// This has to be empty to avoid inheriting the parent bodyClasses
bodyClasses: '',
- sidemenuId,
- hardReloadOnBaseRoute: true,
+ sideMenuOptions,
},
},
{
@@ -83,8 +85,7 @@ export const WORK_PACKAGES_GANTT_ROUTES:Ng2StateDeclaration[] = [
bodyClasses: 'router--work-packages-partitioned-split-view',
menuItem: menuItemClass,
partition: '-left-only',
- sidemenuId,
- hardReloadOnBaseRoute: true,
+ sideMenuOptions,
},
},
...makeSplitViewRoutes(
diff --git a/frontend/src/global_styles/layout/_main_menu.sass b/frontend/src/global_styles/layout/_main_menu.sass
index ccd16383f440..6bb12990f9ec 100644
--- a/frontend/src/global_styles/layout/_main_menu.sass
+++ b/frontend/src/global_styles/layout/_main_menu.sass
@@ -71,7 +71,7 @@ $arrow-left-width: 40px
overflow: auto
@include styled-scroll-bar
- a:focus
+ a:not(.Button):focus
color: var(--main-menu-font-color)
ul
@@ -105,11 +105,11 @@ $arrow-left-width: 40px
// explicitly reset to zero to avoid selector precedence problems
padding-left: 0
- .main-menu--children li a
+ .main-menu--children li a:not(.Button)
// children have no icon so we need to push them right.
padding-left: 24px
- a
+ a:not(.Button)
text-decoration: none
line-height: var(--main-menu-item-height)
position: relative
@@ -143,7 +143,7 @@ $arrow-left-width: 40px
.toggler:hover
@include main-menu-hover
- a
+ a:not(.Button)
border: 1px solid transparent
&.selected, &.selected + a
@@ -158,10 +158,10 @@ $arrow-left-width: 40px
& ~ .toggler
@include main-menu-hover
- a:not(.toggler)
+ a:not(.Button):not(.toggler)
@extend .small-12
- a:not(:only-child):first-of-type
+ a:not(.Button):not(:only-child):first-of-type
flex: 0 0 calc(100% - 40px)
max-width: calc(100% - 40px)
@@ -259,7 +259,7 @@ a.main-menu--parent-node
&.menu_root
> li
.main-item-wrapper
- a:not(:only-child)
+ a:not(.Button):not(:only-child)
@extend .small-12
.ellipsis
@@ -284,7 +284,7 @@ a.main-menu--parent-node
font-family: var(--body-font-family)
font-style: normal
- a, a:link
+ a:not(.Button), a:not(.Button):link
&:not(.searchable-menu--item-link):not(.searchable-menu--category-icon)
color: var(--main-menu-font-color)
display: inline
diff --git a/lookbook/previews/open_project/common/submenu_component_preview.rb b/lookbook/previews/open_project/common/submenu_component_preview.rb
index bda7c9239052..b8d4bce36f26 100644
--- a/lookbook/previews/open_project/common/submenu_component_preview.rb
+++ b/lookbook/previews/open_project/common/submenu_component_preview.rb
@@ -5,7 +5,7 @@ class SubmenuComponentPreview < Lookbook::Preview
# @label Default
def default
render_with_template(template: "open_project/common/submenu_preview/playground",
- locals: { sidebar_menu_items: menu_items, searchable: false })
+ locals: { sidebar_menu_items: menu_items, searchable: false, create_btn_options: nil })
end
# @label Searchable
@@ -13,7 +13,15 @@ def default
# It will be fine in production.
def searchable
render_with_template(template: "open_project/common/submenu_preview/playground",
- locals: { sidebar_menu_items: menu_items, searchable: true })
+ locals: { sidebar_menu_items: menu_items, searchable: true, create_btn_options: nil })
+ end
+
+ # @label With create Button
+ # `create_btn_options: { href: "/#", text: "User"}`
+ def with_create_button
+ render_with_template(template: "open_project/common/submenu_preview/playground",
+ locals: { sidebar_menu_items: menu_items, searchable: true,
+ create_btn_options: { href: "/#", text: "User" } })
end
private
diff --git a/lookbook/previews/open_project/common/submenu_preview/playground.html.erb b/lookbook/previews/open_project/common/submenu_preview/playground.html.erb
index ccdbaaa27888..2aead4a5728b 100644
--- a/lookbook/previews/open_project/common/submenu_preview/playground.html.erb
+++ b/lookbook/previews/open_project/common/submenu_preview/playground.html.erb
@@ -1,6 +1,8 @@
<%= render OpenProject::Common::SubmenuComponent.new(
sidebar_menu_items: sidebar_menu_items,
- searchable: searchable)
+ searchable: searchable,
+ create_btn_options: create_btn_options
+ )
%>
diff --git a/modules/calendar/app/controllers/calendar/menus_controller.rb b/modules/calendar/app/controllers/calendar/menus_controller.rb
new file mode 100644
index 000000000000..b32d394b53d6
--- /dev/null
+++ b/modules/calendar/app/controllers/calendar/menus_controller.rb
@@ -0,0 +1,42 @@
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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 MenusController < ApplicationController
+ before_action :find_project_by_project_id,
+ :authorize
+
+ def show
+ @submenu_menu_items = ::Calendar::Menu.new(project: @project, params:).menu_items
+ @create_btn_options = if User.current.allowed_in_project?(:manage_calendars, @project)
+ { href: new_project_calendars_path(@project), text: I18n.t("label_calendar") }
+ end
+
+ render layout: nil
+ end
+ end
+end
diff --git a/modules/calendar/app/menus/calendar/menu.rb b/modules/calendar/app/menus/calendar/menu.rb
new file mode 100644
index 000000000000..b0c7d257d3e9
--- /dev/null
+++ b/modules/calendar/app/menus/calendar/menu.rb
@@ -0,0 +1,56 @@
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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 Menu < Submenu
+ attr_reader :view_type, :project
+
+ def initialize(project: nil, params: nil)
+ @view_type = "work_packages_calendar"
+ @project = project
+ @params = params
+
+ super(view_type:, project:, params:)
+ end
+
+ def default_queries
+ []
+ end
+
+ def selected?(query_params)
+ query_params[:id].to_s == params[:id]
+ end
+
+ def query_params(id)
+ { id: }
+ end
+
+ def query_path(query_params)
+ project_calendar_path(project, query_params)
+ end
+ end
+end
diff --git a/modules/calendar/app/views/calendar/calendars/_menu.html.erb b/modules/calendar/app/views/calendar/calendars/_menu.html.erb
deleted file mode 100644
index 478d3f0cc225..000000000000
--- a/modules/calendar/app/views/calendar/calendars/_menu.html.erb
+++ /dev/null
@@ -1,7 +0,0 @@
- <%=
- angular_component_tag 'op-calendar-sidemenu',
- inputs: {
- projectId: (@project ? @project.id.to_s : ''),
- menuItems: [parent_name, name],
- }
- %>
diff --git a/modules/calendar/app/views/calendar/menus/_menu.html.erb b/modules/calendar/app/views/calendar/menus/_menu.html.erb
new file mode 100644
index 000000000000..336ee33a79f5
--- /dev/null
+++ b/modules/calendar/app/views/calendar/menus/_menu.html.erb
@@ -0,0 +1,5 @@
+ <%= turbo_frame_tag "calendar_sidemenu",
+ src: menu_project_calendars_path(@project, **params.permit(:id)),
+ target: '_top',
+ data: { turbo: false },
+ loading: :lazy %>
diff --git a/modules/calendar/app/views/calendar/menus/show.html.erb b/modules/calendar/app/views/calendar/menus/show.html.erb
new file mode 100644
index 000000000000..a7e71e26a214
--- /dev/null
+++ b/modules/calendar/app/views/calendar/menus/show.html.erb
@@ -0,0 +1,5 @@
+<%= turbo_frame_tag "calendar_sidemenu" do %>
+ <%= render OpenProject::Common::SubmenuComponent.new(sidebar_menu_items: @submenu_menu_items,
+ searchable: true,
+ create_btn_options: @create_btn_options) %>
+<% end %>
diff --git a/modules/calendar/config/routes.rb b/modules/calendar/config/routes.rb
index 42322035b027..ec673b259fc0 100644
--- a/modules/calendar/config/routes.rb
+++ b/modules/calendar/config/routes.rb
@@ -4,6 +4,9 @@
controller: "calendar/calendars",
only: %i[index destroy],
as: :calendars do
+ collection do
+ get "menu" => "calendar/menus#show"
+ end
get "/new" => "calendar/calendars#show", on: :collection, as: "new"
get "/ical" => "calendar/ical#show", on: :member, as: "ical"
get "(/*state)" => "calendar/calendars#show", on: :member, as: ""
diff --git a/modules/calendar/lib/open_project/calendar/engine.rb b/modules/calendar/lib/open_project/calendar/engine.rb
index 36d907c9bbab..287d4ce569c0 100644
--- a/modules/calendar/lib/open_project/calendar/engine.rb
+++ b/modules/calendar/lib/open_project/calendar/engine.rb
@@ -28,7 +28,8 @@ class Engine < ::Rails::Engine
settings: {} do
project_module :calendar_view, dependencies: :work_package_tracking do
permission :view_calendar,
- { "calendar/calendars": %i[index show] },
+ { "calendar/calendars": %i[index show],
+ "calendar/menus": %i[show] },
permissible_on: :project,
dependencies: %i[view_work_packages],
contract_actions: { calendar: %i[read] }
@@ -76,7 +77,7 @@ class Engine < ::Rails::Engine
:calendar_menu,
{ controller: "/calendar/calendars", action: "index" },
parent: :calendar_view,
- partial: "calendar/calendars/menu",
+ partial: "calendar/menus/menu",
last: true,
caption: :label_calendar_plural
end
diff --git a/modules/calendar/spec/features/query_handling_spec.rb b/modules/calendar/spec/features/query_handling_spec.rb
index 64a17a140c04..ce2194667eb8 100644
--- a/modules/calendar/spec/features/query_handling_spec.rb
+++ b/modules/calendar/spec/features/query_handling_spec.rb
@@ -78,7 +78,7 @@
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(:query_menu) { Components::Submenu.new }
let(:filters) { calendar_page.filters }
current_user { user }
@@ -128,7 +128,7 @@
it "shows only calendar queries" do
# Go to calendar where a query is already shown
- query_menu.expect_menu_entry saved_query.name
+ query_menu.expect_item saved_query.name
# Change filter
filters.open
@@ -141,12 +141,12 @@
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
+ query_menu.expect_item "I am your Query"
+ query_menu.expect_item 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"
+ query_menu.expect_no_item "I am your Query"
end
it_behaves_like "module specific query view management" do
diff --git a/modules/gantt/app/controllers/gantt/menus_controller.rb b/modules/gantt/app/controllers/gantt/menus_controller.rb
index 0708b6a95ca1..e0e917a6327d 100644
--- a/modules/gantt/app/controllers/gantt/menus_controller.rb
+++ b/modules/gantt/app/controllers/gantt/menus_controller.rb
@@ -30,93 +30,8 @@ class MenusController < ApplicationController
before_action :load_and_authorize_in_optional_project
def show
- @sidebar_menu_items = menu_items
+ @sidebar_menu_items = Gantt::Menu.new(project: @project, params:).menu_items
render layout: nil
end
-
- private
-
- def menu_items
- [
- OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_starred_queries"), children: starred_queries),
- OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_default_queries"), children: default_queries),
- OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_global_queries"), children: global_queries),
- OpenProject::Menu::MenuGroup.new(header: I18n.t("js.label_custom_queries"), children: custom_queries)
- ]
- end
-
- def starred_queries
- base_query
- .where("starred" => "t")
- .pluck(:id, :name)
- .map { |id, name| menu_item({ query_id: id }, name) }
- end
-
- def default_queries
- query_generator = Gantt::DefaultQueryGeneratorService.new(with_project: @project)
- Gantt::DefaultQueryGeneratorService::QUERY_OPTIONS.filter_map do |query_key|
- params = query_generator.call(query_key:)
- next if params.nil?
-
- menu_item(
- params,
- I18n.t("js.queries.#{query_key}")
- )
- end
- end
-
- def global_queries
- base_query
- .where("starred" => "f")
- .where("public" => "t")
- .pluck(:id, :name)
- .map { |id, name| menu_item({ query_id: id }, name) }
- end
-
- def custom_queries
- base_query
- .where("starred" => "f")
- .where("public" => "f")
- .pluck(:id, :name)
- .map { |id, name| menu_item({ query_id: id }, name) }
- end
-
- def base_query
- base_query ||= Query
- .visible(current_user)
- .includes(:project)
- .joins(:views)
- .where("views.type" => "gantt")
-
- if @project.present?
- base_query.where("queries.project_id" => @project.id)
- else
- base_query.where("queries.project_id" => nil)
- end
- end
-
- def menu_item(query_params, name)
- OpenProject::Menu::MenuItem.new(title: name,
- href: gantt_path(query_params),
- selected: selected?(query_params))
- end
-
- def selected?(query_params)
- query_params.each_key do |filter_key|
- if params[filter_key] != query_params[filter_key].to_s
- return false
- end
- end
-
- true
- end
-
- def gantt_path(query_params)
- if @project.present?
- project_gantt_index_path(@project, params.permit(query_params.keys).merge!(query_params))
- else
- gantt_index_path(params.permit(query_params.keys).merge!(query_params))
- end
- end
end
end
diff --git a/modules/gantt/app/menus/gantt/menu.rb b/modules/gantt/app/menus/gantt/menu.rb
new file mode 100644
index 000000000000..f243ec98bd40
--- /dev/null
+++ b/modules/gantt/app/menus/gantt/menu.rb
@@ -0,0 +1,61 @@
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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 Gantt
+ class Menu < Submenu
+ attr_reader :view_type, :project
+
+ def initialize(project: nil, params: nil)
+ @view_type = "gantt"
+ @project = project
+ @params = params
+
+ super(view_type:, project:, params:)
+ end
+
+ def default_queries
+ query_generator = Gantt::DefaultQueryGeneratorService.new(with_project: project)
+ Gantt::DefaultQueryGeneratorService::QUERY_OPTIONS.filter_map do |query_key|
+ params = query_generator.call(query_key:)
+ next if params.nil?
+
+ menu_item(
+ params,
+ I18n.t("js.queries.#{query_key}")
+ )
+ end
+ end
+
+ def query_path(query_params)
+ if project.present?
+ project_gantt_index_path(project, params.permit(query_params.keys).merge!(query_params))
+ else
+ gantt_index_path(params.permit(query_params.keys).merge!(query_params))
+ end
+ end
+ end
+end
diff --git a/modules/gantt/app/views/gantt/menus/show.html.erb b/modules/gantt/app/views/gantt/menus/show.html.erb
index 00db47455e5d..273da01bddb0 100644
--- a/modules/gantt/app/views/gantt/menus/show.html.erb
+++ b/modules/gantt/app/views/gantt/menus/show.html.erb
@@ -1,3 +1,3 @@
<%= turbo_frame_tag "gantt_menu" do %>
- <%= render OpenProject::Common::SubmenuComponent.new(sidebar_menu_items: @sidebar_menu_items) %>
+ <%= render OpenProject::Common::SubmenuComponent.new(sidebar_menu_items: @sidebar_menu_items, searchable: true) %>
<% end %>
diff --git a/spec/features/views/shared_examples.rb b/spec/features/views/shared_examples.rb
index 807343984f57..36a204707702 100644
--- a/spec/features/views/shared_examples.rb
+++ b/spec/features/views/shared_examples.rb
@@ -29,7 +29,7 @@
RSpec.shared_examples "module specific query view management" do
describe "within a module" do
let(:query_title) { Components::WorkPackages::QueryTitle.new }
- let(:query_menu) { Components::WorkPackages::QueryMenu.new }
+ let(:query_menu) { Components::Submenu.new }
let(:settings_menu) { Components::WorkPackages::SettingsMenu.new }
let(:filters) { module_page.filters }
@@ -44,7 +44,7 @@
settings_menu.open_and_save_query "My first query"
query_title.expect_not_changed
query_title.expect_title "My first query"
- query_menu.expect_menu_entry "My first query"
+ query_menu.expect_item "My first query"
# Change the filter again
filters.add_filter_by "% Complete", "is", ["25"], "percentageDone"
@@ -55,8 +55,8 @@
settings_menu.open_and_save_query_as "My second query"
query_title.expect_not_changed
query_title.expect_title "My second query"
- query_menu.expect_menu_entry "My second query"
- query_menu.expect_menu_entry "My first query"
+ query_menu.expect_item "My second query"
+ query_menu.expect_item "My first query"
# Rename a query
settings_menu.open_and_choose "Rename view"
@@ -67,17 +67,16 @@
query_title.expect_not_changed
query_title.expect_title "My second query (renamed)"
- query_menu.expect_menu_entry "My second query (renamed)"
- query_menu.expect_menu_entry "My first query"
+ query_menu.expect_item "My second query (renamed)"
+ query_menu.expect_item "My first query"
# Delete a query
settings_menu.open_and_choose "Delete"
module_page.accept_alert_dialog!
- module_page.expect_and_dismiss_toaster message: I18n.t("js.notice_successful_delete")
query_title.expect_title default_name
- query_menu.expect_menu_entry_not_visible "My query planner (renamed)"
- query_menu.expect_menu_entry "My first query"
+ query_menu.expect_no_item "My query planner (renamed)"
+ query_menu.expect_item "My first query"
end
end
end