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