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| %>
@@ -60,7 +60,7 @@
<% menu_item.children.each do |child_item| %>
@@ -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