diff --git a/app/components/open_project/common/submenu_component.html.erb b/app/components/open_project/common/submenu_component.html.erb index 35e4f8e2a954..5b38527ce187 100644 --- a/app/components/open_project/common/submenu_component.html.erb +++ b/app/components/open_project/common/submenu_component.html.erb @@ -31,7 +31,7 @@ <% top_level_sidebar_menu_items.first.children.each do |menu_item| %>
  • <% selected = menu_item.selected ? 'selected' : '' %> - + <%= menu_item.title %>
  • @@ -60,7 +60,7 @@ <% menu_item.children.each do |child_item| %>
  • <% selected = child_item.selected ? 'selected' : '' %> - + <%= child_item.title %>
  • @@ -75,9 +75,14 @@ <%= render Primer::Beta::Button.new(scheme: :primary, tag: :a, href: @create_btn_options[:href], + test_selector: "#{@create_btn_options[:module_key]}--create-button", classes: "op-sidebar--footer-action") do |button| button.with_leading_visual_icon(icon: "plus") - @create_btn_options[:text] + if @create_btn_options[:btn_text].present? + @create_btn_options[:btn_text] + else + I18n.t("label_#{@create_btn_options[:module_key]}") + end end %> <% end %> diff --git a/frontend/src/app/core/main-menu/submenu.service.ts b/frontend/src/app/core/main-menu/submenu.service.ts new file mode 100644 index 000000000000..941a9cd5cc61 --- /dev/null +++ b/frontend/src/app/core/main-menu/submenu.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { TurboElement } from 'core-typings/turbo'; +import { StateService } from '@uirouter/core'; + +@Injectable({ providedIn: 'root' }) +export class SubmenuService { + constructor(protected $state:StateService) {} + + reloadSubmenu():void { + // 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); + const currentSrc = menu.getAttribute('src'); + + if (currentSrc && menu) { + const frameUrl = new URL(currentSrc); + + // Override the frame src to enforce a reload + menu.setAttribute('src', frameUrl.href); + } + } + } +} 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 a4ad79800833..b57f75b80686 100644 --- a/frontend/src/app/core/setup/global-dynamic-components.const.ts +++ b/frontend/src/app/core/setup/global-dynamic-components.const.ts @@ -20,7 +20,6 @@ import { CustomDateActionAdminComponent, customDateActionAdminSelector, } from 'core-app/features/work-packages/components/wp-custom-actions/date-action/custom-date-action-admin.component'; -import { BoardsMenuComponent, boardsMenuSelector } from 'core-app/features/boards/boards-sidebar/boards-menu.component'; import { GlobalSearchWorkPackagesEntryComponent, globalSearchWorkPackagesSelectorEntry, @@ -143,10 +142,6 @@ import { opInAppNotificationBellSelector, } from 'core-app/features/in-app-notifications/bell/in-app-notification-bell.component'; import { IanMenuComponent, ianMenuSelector } from 'core-app/features/in-app-notifications/center/menu/menu.component'; -import { - opTeamPlannerSidemenuSelector, - TeamPlannerSidemenuComponent, -} from 'core-app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component'; import { OpModalOverlayComponent, opModalOverlaySelector, @@ -187,7 +182,6 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [ { selector: globalSearchWorkPackagesSelector, cls: GlobalSearchWorkPackagesComponent }, { selector: homescreenNewFeaturesBlockSelector, cls: HomescreenNewFeaturesBlockComponent }, { selector: customDateActionAdminSelector, cls: CustomDateActionAdminComponent }, - { selector: boardsMenuSelector, cls: BoardsMenuComponent }, { selector: globalSearchWorkPackagesSelectorEntry, cls: GlobalSearchWorkPackagesEntryComponent }, { selector: toastsContainerSelector, cls: ToastsContainerComponent }, { selector: sidemenuSelector, cls: OpSidemenuComponent }, @@ -213,7 +207,6 @@ export const globalDynamicComponents:OptionalBootstrapDefinition[] = [ { selector: headerProjectSelectSelector, cls: OpHeaderProjectSelectComponent }, { selector: wpOverviewGraphSelector, cls: WorkPackageOverviewGraphComponent }, { selector: opViewSelectSelector, cls: ViewSelectComponent }, - { selector: opTeamPlannerSidemenuSelector, cls: TeamPlannerSidemenuComponent }, { selector: triggerActionsEntryComponentSelector, cls: TriggerActionsEntryComponent, embeddable: true }, { selector: editableQueryPropsSelector, cls: EditableQueryPropsComponent }, { selector: backupSelector, cls: BackupComponent }, diff --git a/frontend/src/app/core/setup/globals/onboarding/helpers.ts b/frontend/src/app/core/setup/globals/onboarding/helpers.ts index 7eea4eb9d5b9..ee162ca39017 100644 --- a/frontend/src/app/core/setup/globals/onboarding/helpers.ts +++ b/frontend/src/app/core/setup/globals/onboarding/helpers.ts @@ -1,9 +1,5 @@ export const onboardingTourStorageKey = 'openProject-onboardingTour'; -export type OnboardingTourNames = 'prepareBacklogs'|'backlogs'|'taskboard'|'homescreen'|'workPackages'|'main'; - -export enum ProjectName { - demo = 'demo', -} +export type OnboardingTourNames = 'homescreen'|'workPackages'|'gantt'|'final'|'boards'|'teamPlanner'; function matchingFilter(list:NodeListOf, filterFunction:(match:HTMLElement) => boolean):HTMLElement|null { for (let i = 0; i < list.length; i++) { diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts index 0862cd8208c0..575f0ce59510 100644 --- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts @@ -2,13 +2,18 @@ import { wpOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/to import { OnboardingTourNames, onboardingTourStorageKey, - ProjectName, waitForElement, } from 'core-app/core/setup/globals/onboarding/helpers'; -import { boardTourSteps } from 'core-app/core/setup/globals/onboarding/tours/boards_tour'; +import { + boardTourSteps, + navigateToBoardStep, +} from 'core-app/core/setup/globals/onboarding/tours/boards_tour'; import { menuTourSteps } from 'core-app/core/setup/globals/onboarding/tours/menu_tour'; import { homescreenOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/tours/homescreen_tour'; -import { teamPlannerTourSteps } from 'core-app/core/setup/globals/onboarding/tours/team_planners_tour'; +import { + navigateToTeamPlannerStep, + teamPlannerTourSteps, +} from 'core-app/core/setup/globals/onboarding/tours/team_planners_tour'; import { ganttOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/tours/gantt_tour'; require('core-vendor/enjoyhint'); @@ -36,9 +41,11 @@ export type OnboardingStep = { }; function initializeTour(storageValue:string) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment window.onboardingTourInstance = new window.EnjoyHint({ onStart() { jQuery('#content-wrapper, #menu-sidebar').addClass('-hidden-overflow'); + sessionStorage.setItem(onboardingTourStorageKey, storageValue); }, onEnd() { sessionStorage.setItem(onboardingTourStorageKey, storageValue); @@ -56,8 +63,8 @@ function startTour(steps:OnboardingStep[]) { window.onboardingTourInstance.run(); } -function moduleVisible(name:string):boolean { - return document.getElementsByClassName(`${name}-menu-item`).length > 0; +export function moduleVisible(name:string):boolean { + return document.querySelector(`#menu-sidebar .${name}-menu-item`) !== null; } function workPackageTour() { @@ -68,8 +75,9 @@ function workPackageTour() { startTour(steps); }); } -function mainTour(project:ProjectName = ProjectName.demo) { - initializeTour('mainTourFinished'); + +function ganttTour() { + initializeTour('ganttTourFinished'); const boardsDemoDataAvailable = jQuery('meta[name=boards_demo_data_available]').attr('content') === 'true'; const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true'; @@ -77,30 +85,59 @@ function mainTour(project:ProjectName = ProjectName.demo) { waitForElement('.work-package--results-tbody', '#content', () => { let steps:OnboardingStep[] = ganttOnboardingTourSteps(); - // Check for EE edition if (eeTokenAvailable) { // ... and available seed data of boards. // Then add boards to the tour, otherwise skip it. if (boardsDemoDataAvailable && moduleVisible('boards')) { - steps = steps.concat(boardTourSteps('enterprise', project)); - } - - // ... same for team planners - if (teamPlannerDemoDataAvailable && moduleVisible('team-planner-view')) { - steps = steps.concat(teamPlannerTourSteps()); + steps = steps.concat(navigateToBoardStep('enterprise')); + } else if (teamPlannerDemoDataAvailable && moduleVisible('team-planner-view')) { + steps = steps.concat(navigateToTeamPlannerStep()); + } else { + steps = steps.concat(menuTourSteps()); } } else if (boardsDemoDataAvailable && moduleVisible('boards')) { - steps = steps.concat(boardTourSteps('basic', project)); + steps = steps.concat(navigateToBoardStep('basic')); + } else { + steps = steps.concat(menuTourSteps()); + } + + startTour(steps); + }); +} + +function boardTour() { + initializeTour('boardsTourFinished'); + + const teamPlannerDemoDataAvailable = jQuery('meta[name=demo_view_of_type_team_planner_seeded]').attr('content') === 'true'; + const eeTokenAvailable = !jQuery('body').hasClass('ee-banners-visible'); + + waitForElement('wp-single-card', '#content', () => { + let steps:OnboardingStep[] = eeTokenAvailable ? boardTourSteps('enterprise') : boardTourSteps('basic'); + + // Available seed data of team planner. + // Then add Team planner to the tour, otherwise skip it. + if (teamPlannerDemoDataAvailable && moduleVisible('team-planner-view')) { + steps = steps.concat(navigateToTeamPlannerStep()); + } else { + steps = steps.concat(menuTourSteps()); } + startTour(steps); + }); +} + +function teamPlannerTour() { + initializeTour('teamPlannerTourFinished'); + waitForElement('full-calendar', '#content', () => { + let steps:OnboardingStep[] = teamPlannerTourSteps(); steps = steps.concat(menuTourSteps()); startTour(steps); }); } -export function start(name:OnboardingTourNames, project?:ProjectName):void { +export function start(name:OnboardingTourNames):void { switch (name) { case 'homescreen': initializeTour('startProjectTour'); @@ -109,8 +146,14 @@ export function start(name:OnboardingTourNames, project?:ProjectName):void { case 'workPackages': workPackageTour(); break; - case 'main': - mainTour(project); + case 'gantt': + ganttTour(); + break; + case 'boards': + boardTour(); + break; + case 'teamPlanner': + teamPlannerTour(); break; default: break; diff --git a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts index 0dd1be044a1b..c7c4be84a716 100644 --- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts +++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour_trigger.ts @@ -3,15 +3,14 @@ import { OnboardingTourNames, onboardingTourStorageKey, - ProjectName, waitForElement, } from 'core-app/core/setup/globals/onboarding/helpers'; import { debugLog } from 'core-app/shared/helpers/debug_output'; -async function triggerTour(name:OnboardingTourNames, project?:ProjectName):Promise { +async function triggerTour(name:OnboardingTourNames):Promise { debugLog(`Loading and triggering onboarding tour ${name}`); await import(/* webpackChunkName: "onboarding-tour" */ './onboarding_tour').then((tour) => { - tour.start(name, project); + tour.start(name); }); } @@ -67,12 +66,40 @@ export function detectOnboardingTour():void { // ------------------------------- Tutorial WP page ------------------------------- if (url.searchParams.get('start_onboarding_tour')) { - void triggerTour('workPackages', ProjectName.demo); + void triggerTour('workPackages'); } - // ------------------------------- Tutorial Main part (starting from the Gantt module) ------------------------------- + // ------------------------------- Tutorial Gantt module ------------------------------- if (currentTourPart === 'wpTourFinished') { - void triggerTour('main', ProjectName.demo); + void triggerTour('gantt'); + return; + } + + // ------------------------------- Tutorial Boards module ------------------------------- + if (currentTourPart === 'ganttTourFinished') { + if (url.pathname.includes('boards')) { + void triggerTour('boards'); + return; + } + if (url.pathname.includes('team_planner')) { + void triggerTour('teamPlanner'); + return; + } + void triggerTour('final'); + } + + // ------------------------------- Tutorial TeamPlanner module ------------------------------- + if (currentTourPart === 'boardsTourFinished') { + if (url.pathname.includes('team_planner')) { + void triggerTour('teamPlanner'); + return; + } + void triggerTour('final'); + } + + // ------------------------------- Fina tutorial ------------------------------- + if (currentTourPart === 'teamPlannerTourFinished') { + void triggerTour('final'); } } } diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts index 69d9a39f4b6d..0e0d9e973f8c 100644 --- a/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/tours/boards_tour.ts @@ -1,34 +1,12 @@ import { - ProjectName, waitForElement, } from 'core-app/core/setup/globals/onboarding/helpers'; import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboarding_tour'; -export function boardTourSteps(edition:'basic'|'enterprise', project:ProjectName):OnboardingStep[] { - let boardName:string; - if (edition === 'basic') { - boardName = project === ProjectName.demo ? 'Basic board' : 'Task board'; - } else { - boardName = 'Kanban'; - } - +export function boardTourSteps(edition:'basic'|'enterprise'):OnboardingStep[] { const listExplanation = edition === 'basic' ? 'basic' : 'kanban'; return [ - { - 'next #boards-wrapper>.boards-menu-item': I18n.t('js.onboarding.steps.boards.overview'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - onNext() { - jQuery('#boards-wrapper>.boards-menu-item ~ .toggler')[0].click(); - waitForElement( - '.op-sidemenu--item-action', - '#main-menu', - (match) => match.click(), - (match) => !!match.textContent?.includes(boardName), - ); - }, - }, { 'next [data-tour-selector="op-board-list"]': I18n.t(`js.onboarding.steps.boards.lists_${listExplanation}`), showSkip: false, @@ -57,3 +35,27 @@ export function boardTourSteps(edition:'basic'|'enterprise', project:ProjectName }, ]; } + +export function navigateToBoardStep(edition:'basic'|'enterprise'):OnboardingStep { + let boardName:string; + if (edition === 'basic') { + boardName = 'Basic board'; + } else { + boardName = 'Kanban'; + } + + return { + 'next #boards-wrapper>.boards-menu-item': I18n.t('js.onboarding.steps.boards.overview'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + jQuery('#boards-wrapper>.boards-menu-item ~ .toggler')[0].click(); + waitForElement( + '.op-sidemenu--item-action', + '#main-menu', + (match) => match.click(), + (match) => !!match.textContent?.includes(boardName), + ); + }, + }; +} diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts index 474021b106b0..e529f78f1e00 100644 --- a/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/tours/team_planners_tour.ts @@ -3,21 +3,6 @@ import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboardin export function teamPlannerTourSteps():OnboardingStep[] { return [ - { - 'next .team-planner-view-menu-item': I18n.t('js.onboarding.steps.team_planner.overview'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - onNext() { - jQuery('.team-planner-view-menu-item ~ .toggler')[0].click(); - - waitForElement( - '.op-sidemenu--item-action', - '#main-menu', - (match) => match.click(), - (match) => !!match.textContent?.includes('Team planner'), - ); - }, - }, { 'next [data-tour-selector="op-team-planner--calendar-pane"]': I18n.t('js.onboarding.steps.team_planner.calendar'), showSkip: false, @@ -53,3 +38,21 @@ export function teamPlannerTourSteps():OnboardingStep[] { }, ]; } + +export function navigateToTeamPlannerStep():OnboardingStep { + return { + 'next .team-planner-view-menu-item': I18n.t('js.onboarding.steps.team_planner.overview'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + jQuery('.team-planner-view-menu-item ~ .toggler')[0].click(); + + waitForElement( + '.op-sidemenu--item-action', + '#main-menu', + (match) => match.click(), + (match) => !!match.textContent?.includes('Team planner'), + ); + }, + }; +} diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts index 9480d1d41ee7..c92c1aeb7cd1 100644 --- a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts @@ -50,5 +50,11 @@ export function wpOnboardingTourSteps():OnboardingStep[] { jQuery('#main-menu-gantt')[0].click(); }, }, + { + containerClass: '-dark -hidden-arrow', + onBeforeStart() { + window.location.href = `${window.location.origin}/projects/demo-project/gantt`; + }, + }, ]; } diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts index cc50a353861e..f27b4581cbb4 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts @@ -40,6 +40,7 @@ import { Observable, of, } from 'rxjs'; +import { SubmenuService } from 'core-app/core/main-menu/submenu.service'; export function boardCardViewHandlerFactory(injector:Injector) { return new CardViewHandlerRegistry(injector); @@ -152,6 +153,7 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { readonly boardFilters:BoardFiltersService, readonly Boards:BoardService, readonly titleService:OpTitleService, + readonly submenuService:SubmenuService, ) { super(); } @@ -217,6 +219,7 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { }), finalize(() => { this.toolbarDisabled = false; + this.reloadSidemenu(); this.cdRef.detectChanges(); }), ).subscribe(() => { @@ -244,4 +247,8 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { protected setPartition(state:Ng2StateDeclaration) { this.currentPartition = (state.data && state.data.partition) ? state.data.partition : '-split'; } + + private reloadSidemenu():void { + this.submenuService.reloadSubmenu(); + } } diff --git a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.html b/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.html deleted file mode 100644 index 9f431caa7df3..000000000000 --- a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.html +++ /dev/null @@ -1,22 +0,0 @@ -
    - - -
    - - diff --git a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts b/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts deleted file mode 100644 index d879f650b6b7..000000000000 --- a/frontend/src/app/features/boards/boards-sidebar/boards-menu.component.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - Component, - HostBinding, - OnInit, -} from '@angular/core'; -import { Observable } from 'rxjs'; -import { BoardService } from 'core-app/features/boards/board/board.service'; -import { Board } from 'core-app/features/boards/board/board'; -import { map } from 'rxjs/operators'; -import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; -import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; -import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { MainMenuNavigationService } from 'core-app/core/main-menu/main-menu-navigation.service'; -import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { IOpSidemenuItem } from 'core-app/shared/components/sidemenu/sidemenu.component'; -import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; - -export const boardsMenuSelector = 'boards-menu'; - -@Component({ - selector: boardsMenuSelector, - templateUrl: './boards-menu.component.html', -}) - -export class BoardsMenuComponent extends UntilDestroyedMixin implements OnInit { - @HostBinding('class.op-sidebar') className = true; - - boardOptions$:Observable = this - .apiV3Service - .boards - .observeAll() - .pipe( - map((boards:Board[]) => { - const menuItems:IOpSidemenuItem[] = boards.map((board) => ({ - title: board.name, - uiSref: 'boards.partitioned.show', - uiParams: { - board_id: board.id, - query_props: undefined, - projects: 'projects', - projectPath: this.currentProject.identifier, - }, - uiOptions: { reload: true }, - })); - - return menuItems.sort((a, b) => a.title.localeCompare(b.title)); - }), - ); - - canCreateBoards$ = this - .currentUserService - .hasCapabilities$( - 'boards/create', - this.currentProject.id || null, - ) - .pipe(this.untilDestroyed()); - - text = { - board: this.I18n.t('js.label_board'), - create_new_board: this.I18n.t('js.boards.create_new'), - }; - - constructor( - readonly boardService:BoardService, - readonly apiV3Service:ApiV3Service, - readonly currentProject:CurrentProjectService, - readonly mainMenuService:MainMenuNavigationService, - readonly currentUserService:CurrentUserService, - readonly I18n:I18nService, - readonly pathHelper:PathHelperService, - ) { - super(); - } - - ngOnInit():void { - // When activating the boards submenu, - // either initially or through click on the toggle, load the results - this.mainMenuService - .onActivate('boards') - .subscribe(() => { - this.focusBackArrow(); - void this.boardService.loadAllBoards(); - }); - } - - redirectToNewBoardForm():void { - window.location.href = this.pathHelper.newBoardsPath(this.currentProject.identifier); - } - - private focusBackArrow():void { - const buttonArrowLeft = jQuery('*[data-name="boards"] .main-menu--arrow-left-to-project'); - buttonArrowLeft.focus(); - } -} diff --git a/frontend/src/app/features/boards/openproject-boards.module.ts b/frontend/src/app/features/boards/openproject-boards.module.ts index 8151a3202c6e..e10f851a8d68 100644 --- a/frontend/src/app/features/boards/openproject-boards.module.ts +++ b/frontend/src/app/features/boards/openproject-boards.module.ts @@ -36,7 +36,6 @@ import { BoardsRootComponent } from 'core-app/features/boards/boards-root/boards import { BoardInlineAddAutocompleterComponent } from 'core-app/features/boards/board/inline-add/board-inline-add-autocompleter.component'; import { BoardsToolbarMenuDirective } from 'core-app/features/boards/board/toolbar-menu/boards-toolbar-menu.directive'; import { BoardConfigurationModalComponent } from 'core-app/features/boards/board/configuration-modal/board-configuration.modal'; -import { BoardsMenuComponent } from 'core-app/features/boards/boards-sidebar/boards-menu.component'; import { AddListModalComponent } from 'core-app/features/boards/board/add-list-modal/add-list-modal.component'; import { BoardHighlightingTabComponent } from 'core-app/features/boards/board/configuration-modal/tabs/highlighting-tab.component'; import { AddCardDropdownMenuDirective } from 'core-app/features/boards/board/add-card-dropdown/add-card-dropdown-menu.directive'; @@ -76,7 +75,6 @@ import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autoc BoardListComponent, BoardsRootComponent, BoardInlineAddAutocompleterComponent, - BoardsMenuComponent, BoardHighlightingTabComponent, BoardConfigurationModalComponent, BoardsToolbarMenuDirective, diff --git a/frontend/src/app/features/boards/openproject-boards.routes.ts b/frontend/src/app/features/boards/openproject-boards.routes.ts index 70fd46aae91e..a31fad5d05bd 100644 --- a/frontend/src/app/features/boards/openproject-boards.routes.ts +++ b/frontend/src/app/features/boards/openproject-boards.routes.ts @@ -35,6 +35,12 @@ import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/r export const menuItemClass = 'boards-menu-item'; +export const sidemenuId = 'boards_sidemenu'; +export const sideMenuOptions = { + sidemenuId, + hardReloadOnBaseRoute: true, +}; + export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ { name: 'boards', @@ -45,6 +51,7 @@ export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ data: { bodyClasses: 'router--boards-view-base', menuItem: menuItemClass, + sideMenuOptions, }, params: { // Use custom encoder/decoder that ensures validity of URL string @@ -63,6 +70,7 @@ export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ parent: 'boards', bodyClasses: 'router--boards-full-view', menuItem: menuItemClass, + sideMenuOptions, }, reloadOnSearch: false, component: BoardPartitionedPageComponent, @@ -73,6 +81,7 @@ export const BOARDS_ROUTES:Ng2StateDeclaration[] = [ url: '', data: { baseRoute: 'boards.partitioned.show', + sideMenuOptions, }, views: { 'content-left': { component: BoardListContainerComponent }, diff --git a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.html b/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.html deleted file mode 100644 index 6d49c6203a3e..000000000000 --- a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - diff --git a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.ts b/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.ts deleted file mode 100644 index a434c52cb8cc..000000000000 --- a/frontend/src/app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - ElementRef, - HostBinding, - Input, -} from '@angular/core'; -import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; -import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; -import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; -import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { BannersService } from 'core-app/core/enterprise/banners.service'; -import { map } from 'rxjs/operators'; - -export const opTeamPlannerSidemenuSelector = 'op-team-planner-sidemenu'; - -@Component({ - selector: opTeamPlannerSidemenuSelector, - templateUrl: './team-planner-sidemenu.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TeamPlannerSidemenuComponent extends UntilDestroyedMixin { - @HostBinding('class.op-sidebar') className = true; - - @Input() menuItems:string[] = []; - - @Input() projectId:string|undefined; - - canAddTeamPlanner$ = this - .currentUserService - .hasCapabilities$( - 'team_planners/create', - this.currentProjectService.id || null, - ) - .pipe( - map((val) => val && !this.bannersService.eeShowBanners), - ); - - createButton = { - text: this.I18n.t('js.team_planner.create_label'), - title: this.I18n.t('js.team_planner.create_title'), - uiSref: 'team_planner.page.show', - uiParams: { - query_id: null, - query_props: '', - }, - }; - - constructor( - readonly elementRef:ElementRef, - readonly currentUserService:CurrentUserService, - readonly currentProjectService:CurrentProjectService, - readonly bannersService:BannersService, - readonly I18n:I18nService, - ) { - super(); - - populateInputsFromDataset(this); - } -} diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts index 3d8f8316ee23..3d3d2c1e744f 100644 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts +++ b/frontend/src/app/features/team-planner/team-planner/team-planner.module.ts @@ -14,7 +14,6 @@ import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-pl import { OpSharedModule } from 'core-app/shared/shared.module'; import { AddExistingPaneComponent } from './add-work-packages/add-existing-pane.component'; import { OpenprojectContentLoaderModule } from 'core-app/shared/components/op-content-loader/openproject-content-loader.module'; -import { TeamPlannerSidemenuComponent } from 'core-app/features/team-planner/team-planner/sidemenu/team-planner-sidemenu.component'; import { TeamPlannerViewSelectMenuDirective } from 'core-app/features/team-planner/team-planner/view-select/view-select-menu.directive'; @NgModule({ @@ -23,7 +22,6 @@ import { TeamPlannerViewSelectMenuDirective } from 'core-app/features/team-plann TeamPlannerPageComponent, AddAssigneeComponent, AddExistingPaneComponent, - TeamPlannerSidemenuComponent, TeamPlannerViewSelectMenuDirective, ], imports: [ diff --git a/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts b/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts index 2674e488e6f7..8efb9246cb73 100644 --- a/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts +++ b/frontend/src/app/features/team-planner/team-planner/team-planner.routes.ts @@ -33,6 +33,13 @@ import { WorkPackagesBaseComponent } from 'core-app/features/work-packages/routi import { TeamPlannerPageComponent } from 'core-app/features/team-planner/team-planner/page/team-planner-page.component'; import { TeamPlannerComponent } from 'core-app/features/team-planner/team-planner/planner/team-planner.component'; +export const sidemenuId = 'team_planner_sidemenu'; +export const sideMenuOptions = { + sidemenuId, + hardReloadOnBaseRoute: true, + defaultQuery: 'new', +}; + export const TEAM_PLANNER_ROUTES:Ng2StateDeclaration[] = [ { name: 'team_planner', @@ -56,12 +63,14 @@ export const TEAM_PLANNER_ROUTES:Ng2StateDeclaration[] = [ redirectTo: 'team_planner.page.show', data: { bodyClasses: 'router--team-planner', + sideMenuOptions, }, }, { name: 'team_planner.page.show', data: { baseRoute: 'team_planner.page.show', + sideMenuOptions, }, views: { 'content-left': { component: TeamPlannerComponent }, 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 7c682c59487a..6f6d19353782 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 @@ -54,7 +54,7 @@ import { QueryFormResource } from 'core-app/features/hal/resources/query-form-re import { WorkPackageStatesInitializationService } from './wp-states-initialization.service'; import { WorkPackagesListInvalidQueryService } from './wp-list-invalid-query.service'; import { WorkPackagesQueryViewService } from 'core-app/features/work-packages/components/wp-list/wp-query-view.service'; -import { TurboElement } from 'core-typings/turbo'; +import { SubmenuService } from 'core-app/core/main-menu/submenu.service'; export interface QueryDefinition { queryParams:{ query_id?:string|null, query_props?:string|null }; @@ -104,6 +104,7 @@ export class WorkPackagesListService { protected wpStatesInitialization:WorkPackageStatesInitializationService, protected wpListInvalidQueryService:WorkPackagesListInvalidQueryService, protected wpQueryView:WorkPackagesQueryViewService, + protected submenuService:SubmenuService, ) { } /** @@ -261,7 +262,7 @@ export class WorkPackagesListService { // Reload the query, and then reload the menu this.reloadQuery(createdQuery).subscribe(() => { this.states.changes.queries.next(createdQuery.id); - this.reloadSidemenu(createdQuery.id); + this.reloadSidemenu(); }); return createdQuery; @@ -312,7 +313,7 @@ export class WorkPackagesListService { if (queryAccessibleByUser) { void this.$state.go('.', { query_id: query.id, query_props: null }, { reload: true }); this.states.changes.queries.next(query.id); - this.reloadSidemenu(query.id); + this.reloadSidemenu(); } else { this.navigateToDefaultQuery(query); } @@ -343,7 +344,7 @@ export class WorkPackagesListService { this.toastService.addSuccess(this.I18n.t('js.notice_successful_update')); this.states.changes.queries.next(query.id!); - this.reloadSidemenu(query.id); + this.reloadSidemenu(); }); return promise; @@ -457,28 +458,11 @@ export class WorkPackagesListService { void this.loadDefaultQuery(projectId); this.states.changes.queries.next(query.id); - this.reloadSidemenu(null); + this.reloadSidemenu(); } } - private reloadSidemenu(selectedQueryId:string|null):void { - // 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); - const currentSrc = menu.getAttribute('src'); - - if (currentSrc && menu) { - const frameUrl = new URL(currentSrc); - - // Override the frame src to enforce a reload - if (selectedQueryId) { - frameUrl.search = `?query_id=${selectedQueryId}`; - } - - menu.setAttribute('src', frameUrl.href); - } - } + private reloadSidemenu():void { + this.submenuService.reloadSubmenu(); } } diff --git a/lookbook/previews/open_project/common/submenu_component_preview.rb b/lookbook/previews/open_project/common/submenu_component_preview.rb index b8d4bce36f26..b64d39f2c7b9 100644 --- a/lookbook/previews/open_project/common/submenu_component_preview.rb +++ b/lookbook/previews/open_project/common/submenu_component_preview.rb @@ -21,7 +21,7 @@ def searchable 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" } }) + create_btn_options: { href: "/#", module_key: "user" } }) end private diff --git a/modules/boards/app/controllers/boards/menus_controller.rb b/modules/boards/app/controllers/boards/menus_controller.rb new file mode 100644 index 000000000000..341eb1ed2518 --- /dev/null +++ b/modules/boards/app/controllers/boards/menus_controller.rb @@ -0,0 +1,44 @@ +# -- 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 ::Boards + class MenusController < ApplicationController + before_action :find_project_by_project_id, + :authorize + + def show + @submenu_menu_items = ::Boards::Menu.new(project: @project, params:).menu_items + @create_btn_options = if User.current.allowed_in_project?(:manage_board_views, @project) + { href: new_project_work_package_board_path(@project), + module_key: "boards", + btn_text: I18n.t("boards.label_board") } + end + + render layout: nil + end + end +end diff --git a/modules/boards/app/menus/boards/menu.rb b/modules/boards/app/menus/boards/menu.rb new file mode 100644 index 000000000000..0784b8145c76 --- /dev/null +++ b/modules/boards/app/menus/boards/menu.rb @@ -0,0 +1,71 @@ +# -- 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 Boards + class Menu < Submenu + attr_reader :view_type, :project + + def initialize(project: nil, params: nil) + @project = project + @params = params + + super(view_type: nil, project:, params:) + end + + def global_queries + Boards::Grid.includes(:project) + .references(:project) + .where(project: @project) + .pluck(:id, :name) + .map { |id, name| menu_item(query_params(id), name) } + end + + def starred_queries + [] + end + + def default_queries + [] + end + + def custom_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_work_package_board_path(project, query_params) + end + end +end diff --git a/modules/boards/app/views/boards/boards/_menu_board.html b/modules/boards/app/views/boards/boards/_menu_board.html deleted file mode 100644 index 48ddc1b6a3a9..000000000000 --- a/modules/boards/app/views/boards/boards/_menu_board.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/boards/app/views/boards/menus/_menu.html.erb b/modules/boards/app/views/boards/menus/_menu.html.erb new file mode 100644 index 000000000000..1c703fff83be --- /dev/null +++ b/modules/boards/app/views/boards/menus/_menu.html.erb @@ -0,0 +1,5 @@ + <%= turbo_frame_tag "boards_sidemenu", + src: menu_project_work_package_boards_path(@project, **params.permit(:id)), + target: "_top", + data: { turbo: false }, + loading: :lazy %> diff --git a/modules/boards/app/views/boards/menus/show.html.erb b/modules/boards/app/views/boards/menus/show.html.erb new file mode 100644 index 000000000000..d4ba132edd12 --- /dev/null +++ b/modules/boards/app/views/boards/menus/show.html.erb @@ -0,0 +1,5 @@ +<%= turbo_frame_tag "boards_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/boards/config/routes.rb b/modules/boards/config/routes.rb index 84550482ad2b..545afd771023 100644 --- a/modules/boards/config/routes.rb +++ b/modules/boards/config/routes.rb @@ -9,6 +9,9 @@ controller: "boards/boards", only: %i[index show new create], as: :work_package_boards do + collection do + get "menu" => "boards/menus#show" + end get "(/*state)" => "boards/boards#show", on: :member, as: "", constraints: { id: /\d+/ } end end diff --git a/modules/boards/lib/open_project/boards/engine.rb b/modules/boards/lib/open_project/boards/engine.rb index 5c3126ae6c81..8d55c8ba1789 100644 --- a/modules/boards/lib/open_project/boards/engine.rb +++ b/modules/boards/lib/open_project/boards/engine.rb @@ -28,7 +28,8 @@ class Engine < ::Rails::Engine settings: {} do project_module :board_view, dependencies: :work_package_tracking, order: 80 do permission :show_board_views, - { "boards/boards": %i[index show] }, + { "boards/boards": %i[index show], + "boards/menus": %i[show] }, permissible_on: :project, dependencies: :view_work_packages, contract_actions: { boards: %i[read] } @@ -50,7 +51,7 @@ class Engine < ::Rails::Engine :board_menu, { controller: "/boards/boards", action: :index }, parent: :boards, - partial: "boards/boards/menu_board", + partial: "boards/menus/menu", last: true, caption: :"boards.label_boards" diff --git a/modules/boards/spec/features/board_navigation_spec.rb b/modules/boards/spec/features/board_navigation_spec.rb index 9fa5a359a0f4..4a82058f93be 100644 --- a/modules/boards/spec/features/board_navigation_spec.rb +++ b/modules/boards/spec/features/board_navigation_spec.rb @@ -106,9 +106,9 @@ page.find_test_selector("main-menu-toggler--boards", wait: 10).click - subitem = page.find_test_selector("op-sidemenu--item-action--Myboard", wait: 10) + subitem = page.find_test_selector("op-sidemenu--item-action", text: "My board", wait: 10) # Ends with boards due to lazy route - expect(subitem[:href]).to end_with project_work_package_boards_path(project) + expect(subitem[:href]).to end_with project_work_package_board_path(project, board_view.id) subitem.click diff --git a/modules/boards/spec/features/support/board_index_page.rb b/modules/boards/spec/features/support/board_index_page.rb index d5f1632fb425..ca3c9e094d47 100644 --- a/modules/boards/spec/features/support/board_index_page.rb +++ b/modules/boards/spec/features/support/board_index_page.rb @@ -63,7 +63,7 @@ def create_board(action: "Basic", title: "#{action} Board", expect_empty: false, click_link "Board" end else - find('[data-test-selector="sidebar--create-board-button"]').click + find('[data-test-selector="boards--create-button"]').click end new_board_page = NewBoard.new diff --git a/modules/calendar/app/controllers/calendar/menus_controller.rb b/modules/calendar/app/controllers/calendar/menus_controller.rb index b32d394b53d6..41da3240b9b9 100644 --- a/modules/calendar/app/controllers/calendar/menus_controller.rb +++ b/modules/calendar/app/controllers/calendar/menus_controller.rb @@ -33,7 +33,7 @@ class MenusController < ApplicationController 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") } + { href: new_project_calendars_path(@project), module_key: "calendar" } end render layout: nil diff --git a/modules/team_planner/app/controllers/team_planner/menus_controller.rb b/modules/team_planner/app/controllers/team_planner/menus_controller.rb new file mode 100644 index 000000000000..e782768bac5a --- /dev/null +++ b/modules/team_planner/app/controllers/team_planner/menus_controller.rb @@ -0,0 +1,43 @@ +# -- 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 ::TeamPlanner + class MenusController < ApplicationController + before_action :find_project_by_project_id, + :authorize + + def show + @submenu_menu_items = ::TeamPlanner::Menu.new(project: @project, params:).menu_items + if User.current.allowed_in_project?(:manage_team_planner, @project) && + EnterpriseToken.allows_to?(:team_planner_view) + @create_btn_options = { href: new_project_team_planners_path(@project), module_key: "team_planner" } + end + + render layout: nil + end + end +end diff --git a/modules/team_planner/app/menus/team_planner/menu.rb b/modules/team_planner/app/menus/team_planner/menu.rb new file mode 100644 index 000000000000..7041404b2872 --- /dev/null +++ b/modules/team_planner/app/menus/team_planner/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 TeamPlanner + class Menu < Submenu + attr_reader :view_type, :project + + def initialize(project: nil, params: nil) + @view_type = "team_planner" + @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_team_planner_path(project, query_params) + end + end +end diff --git a/modules/team_planner/app/views/team_planner/menus/_menu.html.erb b/modules/team_planner/app/views/team_planner/menus/_menu.html.erb new file mode 100644 index 000000000000..7ceb63595fb1 --- /dev/null +++ b/modules/team_planner/app/views/team_planner/menus/_menu.html.erb @@ -0,0 +1,5 @@ + <%= turbo_frame_tag "team_planner_sidemenu", + src: menu_project_team_planners_path(@project, **params.permit(:id)), + target: '_top', + data: { turbo: false }, + loading: :lazy %> diff --git a/modules/team_planner/app/views/team_planner/menus/show.html.erb b/modules/team_planner/app/views/team_planner/menus/show.html.erb new file mode 100644 index 000000000000..7c6fb6985aeb --- /dev/null +++ b/modules/team_planner/app/views/team_planner/menus/show.html.erb @@ -0,0 +1,5 @@ +<%= turbo_frame_tag "team_planner_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/team_planner/app/views/team_planner/team_planner/_menu.html.erb b/modules/team_planner/app/views/team_planner/team_planner/_menu.html.erb deleted file mode 100644 index fed603f4a415..000000000000 --- a/modules/team_planner/app/views/team_planner/team_planner/_menu.html.erb +++ /dev/null @@ -1,7 +0,0 @@ - <%= - angular_component_tag 'op-team-planner-sidemenu', - inputs: { - projectId: (@project ? @project.id.to_s : ''), - menuItems: [parent_name, name], - } - %> diff --git a/modules/team_planner/config/routes.rb b/modules/team_planner/config/routes.rb index 2f694e154101..501607c86cc2 100644 --- a/modules/team_planner/config/routes.rb +++ b/modules/team_planner/config/routes.rb @@ -15,6 +15,7 @@ only: %i[index destroy], as: :team_planners do collection do + get "menu" => "team_planner/menus#show" get "/upsale", to: "team_planner/team_planner#upsale", as: :upsale get "/new", to: "team_planner/team_planner#show", as: :new end diff --git a/modules/team_planner/lib/open_project/team_planner/engine.rb b/modules/team_planner/lib/open_project/team_planner/engine.rb index 2a853694ae21..422c42bafe8c 100644 --- a/modules/team_planner/lib/open_project/team_planner/engine.rb +++ b/modules/team_planner/lib/open_project/team_planner/engine.rb @@ -38,7 +38,8 @@ class Engine < ::Rails::Engine settings: {} do project_module :team_planner_view, dependencies: :work_package_tracking, enterprise_feature: true do permission :view_team_planner, - { "team_planner/team_planner": %i[index show upsale overview] }, + { "team_planner/team_planner": %i[index show upsale overview], + "team_planner/menus": %i[show] }, permissible_on: :project, dependencies: %i[view_work_packages], contract_actions: { team_planner: %i[read] } @@ -80,7 +81,7 @@ class Engine < ::Rails::Engine :team_planner_menu, { controller: "/team_planner/team_planner", action: :index }, parent: :team_planner_view, - partial: "team_planner/team_planner/menu", + partial: "team_planner/menus/menu", last: true, caption: :"team_planner.label_team_planner_plural" diff --git a/modules/team_planner/spec/features/query_handling_spec.rb b/modules/team_planner/spec/features/query_handling_spec.rb index 7388ee6f9f8f..005c10402b88 100644 --- a/modules/team_planner/spec/features/query_handling_spec.rb +++ b/modules/team_planner/spec/features/query_handling_spec.rb @@ -75,7 +75,7 @@ let(:team_planner) { Pages::TeamPlanner.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) { team_planner.filters } current_user { user } @@ -114,8 +114,8 @@ it "shows only team planner queries" do # Go to team planner where no query is shown, only the create option - query_menu.expect_no_menu_entry - expect(page).to have_test_selector("team-planner--create-button") + query_menu.expect_no_items + expect(page).to have_test_selector("team_planner--create-button") # Change filter filters.open @@ -128,11 +128,11 @@ team_planner.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_item "I am your Query" # .. 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/team_planner/spec/features/team_planner_index_spec.rb b/modules/team_planner/spec/features/team_planner_index_spec.rb index 067fbb09ec3e..f06f50a88c4b 100644 --- a/modules/team_planner/spec/features/team_planner_index_spec.rb +++ b/modules/team_planner/spec/features/team_planner_index_spec.rb @@ -63,7 +63,7 @@ end it "can create an action through the sidebar" do - find_test_selector("team-planner--create-button").click + find_test_selector("team_planner--create-button").click team_planner.expect_no_toaster team_planner.expect_title diff --git a/modules/team_planner/spec/features/team_planner_menu_spec.rb b/modules/team_planner/spec/features/team_planner_menu_spec.rb index 0732a7378215..e7b61a15cabc 100644 --- a/modules/team_planner/spec/features/team_planner_menu_spec.rb +++ b/modules/team_planner/spec/features/team_planner_menu_spec.rb @@ -77,7 +77,7 @@ click_link "Team planners" end - expect(page).not_to have_test_selector("team-planner--create-button") + expect(page).not_to have_test_selector("team_planner--create-button") end end @@ -89,7 +89,7 @@ click_link "Team planners" end - expect(page).not_to have_test_selector("team-planner--create-button") + expect(page).not_to have_test_selector("team_planner--create-button") end end end @@ -105,7 +105,7 @@ click_link "Team planners" end - expect(page).not_to have_test_selector("team-planner--create-button") + expect(page).not_to have_test_selector("team_planner--create-button") end end @@ -117,7 +117,7 @@ click_link "Team planners" end - expect(page).to have_test_selector("team-planner--create-button") + expect(page).to have_test_selector("team_planner--create-button") end end end diff --git a/spec/support/components/common/submenu.rb b/spec/support/components/common/submenu.rb index 77c59af075d4..f876bdf700fb 100644 --- a/spec/support/components/common/submenu.rb +++ b/spec/support/components/common/submenu.rb @@ -37,12 +37,13 @@ def expect_item(name, selected: false, visible: true) selected_specifier = selected ? ".selected" : ":not(.selected)" expect(page).to have_css(".op-sidemenu--item-action#{selected_specifier}", text: name, visible:) + # expect(page).to have_css("[data-test-selector='op-sidemenu--item-action']#{selected_specifier}", text: name, visible:) end end def expect_no_item(name) within "#main-menu" do - expect(page).to have_no_css(".op-sidemenu--item-action", text: name) + expect(page).not_to have_test_selector("op-sidemenu--item-action", text: name) end end @@ -52,6 +53,12 @@ def click_item(name) end end + def expect_no_items + within "#main-menu" do + expect(page).not_to have_test_selector("op-sidemenu--item-action") + end + end + def search_for_item(name) within "#main-menu" do page.find_test_selector("op-sidebar--search-input").set(name) diff --git a/spec/support/components/work_packages/query_menu.rb b/spec/support/components/work_packages/query_menu.rb index 10f541c862ba..a370d6bb68b4 100644 --- a/spec/support/components/work_packages/query_menu.rb +++ b/spec/support/components/work_packages/query_menu.rb @@ -69,10 +69,6 @@ def expect_menu_entry(name) def expect_menu_entry_not_visible(name) expect(page).to have_no_selector(autocompleter_item_selector, text: name) end - - def expect_no_menu_entry - expect(page).to have_no_selector(autocompleter_item_selector) - end end end end