diff --git a/app/components/work_packages/full_view/copy_component.html.erb b/app/components/work_packages/full_view/copy_component.html.erb new file mode 100644 index 00000000000..869327b933a --- /dev/null +++ b/app/components/work_packages/full_view/copy_component.html.erb @@ -0,0 +1,11 @@ +<%= + helpers.angular_component_tag "opce-wp-full-copy", + inputs: { + type: @type, + copiedFromWorkPackageId: @copied_from_work_package_id, + projectIdentifier: @project.present? ? @project.identifier : nil, + resizerClass: "op-work-package-split-view", + routedFromAngular: false, + } + +%> diff --git a/app/components/work_packages/full_view/copy_component.rb b/app/components/work_packages/full_view/copy_component.rb new file mode 100644 index 00000000000..e2d142a7603 --- /dev/null +++ b/app/components/work_packages/full_view/copy_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WorkPackages + module FullView + class CopyComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:, copied_from_work_package_id:, project:) + super + + @type = type + @copied_from_work_package_id = copied_from_work_package_id + @project = project + end + end + end +end diff --git a/app/components/work_packages/full_view/create_component.html.erb b/app/components/work_packages/full_view/create_component.html.erb new file mode 100644 index 00000000000..ec6842ae08f --- /dev/null +++ b/app/components/work_packages/full_view/create_component.html.erb @@ -0,0 +1,7 @@ +<%= helpers.angular_component_tag "opce-wp-full-create", + inputs: { + type: @type, + projectIdentifier: @project.present? ? @project.identifier : nil, + routedFromAngular: false, + } +%> diff --git a/app/components/work_packages/full_view/create_component.rb b/app/components/work_packages/full_view/create_component.rb new file mode 100644 index 00000000000..7bbec152bef --- /dev/null +++ b/app/components/work_packages/full_view/create_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WorkPackages + module FullView + class CreateComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:, project:) + super + + @type = type + @project = project + end + end + end +end diff --git a/app/components/work_packages/full_view/show_component.html.erb b/app/components/work_packages/full_view/show_component.html.erb new file mode 100644 index 00000000000..4755438946c --- /dev/null +++ b/app/components/work_packages/full_view/show_component.html.erb @@ -0,0 +1,17 @@ +<%= + if @work_package.nil? + render(Primer::Beta::Blankslate.new(spacious: true)) do |component| + component.with_visual_icon(icon: :inbox) + component.with_heading(tag: :h2).with_content( + I18n.t(:error_work_package_id_not_found) + ) + end + else + helpers.angular_component_tag "opce-wp-full-view", + inputs: { + workPackageId: @id, + activeTab: @tab, + routedFromAngular: false + } + end +%> diff --git a/app/components/work_packages/full_view/show_component.rb b/app/components/work_packages/full_view/show_component.rb new file mode 100644 index 00000000000..2547c4a3e68 --- /dev/null +++ b/app/components/work_packages/full_view/show_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module WorkPackages + module FullView + class ShowComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def self.wrapper_key = :"work-package-full-view" + + def initialize(id:, tab: "activity") + super + + @id = id + @tab = tab + @work_package = WorkPackage.visible.find_by(id:) + end + + def wrapper_uniq_by + @id + end + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5a99a5d3550..0ce5dc4cbf4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -258,6 +258,8 @@ class ApplicationController < ActionController::Base # Find project by project_id if given def find_optional_project @project = Project.find(params[:project_id]) if params[:project_id].present? + rescue ActiveRecord::RecordNotFound + render_404 end # Finds and sets @project based on @object.project diff --git a/app/controllers/work_packages/bulk_controller.rb b/app/controllers/work_packages/bulk_controller.rb index acb99ebf93a..56e36faa6df 100644 --- a/app/controllers/work_packages/bulk_controller.rb +++ b/app/controllers/work_packages/bulk_controller.rb @@ -64,7 +64,7 @@ class WorkPackages::BulkController < ApplicationController respond_to do |format| format.html do - redirect_back_or_default(project_work_packages_path(@work_packages.first.project)) + redirect_to (project_work_packages_path(@work_packages.first.project)) end format.json do head :ok diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 01a034f47ff..0f2f9b8a8a9 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -39,14 +39,15 @@ class WorkPackagesController < ApplicationController before_action :authorize_on_work_package, :project, only: %i[show generate_pdf_dialog generate_pdf] - before_action :load_and_authorize_in_optional_project, - :check_allowed_export, + before_action :check_allowed_export, :protect_from_unauthorized_export, only: %i[index export_dialog] + before_action :load_and_authorize_in_optional_project, only: %i[index new show copy export_dialog] before_action :authorize, only: %i[show_conflict_flash_message share_upsell] - authorization_checked! :index, :show, :export_dialog, :generate_pdf_dialog, :generate_pdf + authorization_checked! :index, :show, :new, :copy, :export_dialog, :generate_pdf_dialog, :generate_pdf + + before_action :load_and_validate_query, only: %i[index copy], unless: -> { request.format.html? } - before_action :load_and_validate_query, only: :index, unless: -> { request.format.html? } before_action :load_work_packages, only: :index, if: -> { request.format.atom? } before_action :load_and_validate_query_for_export, only: :export_dialog @@ -71,21 +72,34 @@ class WorkPackagesController < ApplicationController def show respond_to do |format| format.html do + if show_route_incomplete? + redirect_to_complete_route + + return + end + render :show, - locals: { work_package:, menu_name: project_or_global_menu }, - layout: "angular/angular" + locals: { work_package:, menu_name: project_or_global_menu } end - format.any(*supported_single_formats) do - export_single(request.format.symbol) - end + handle_standard_show_formats(format) + end + end - format.atom do - atom_journals + def copy + respond_to do |format| + format.html do + render :copy, + locals: { query: @query, project: @project, menu_name: project_or_global_menu } end + end + end - format.all do - head :not_acceptable + def new + respond_to do |format| + format.html do + render :new, + locals: { query: @query, project: @project, menu_name: project_or_global_menu } end end end @@ -177,6 +191,20 @@ class WorkPackagesController < ApplicationController private + def handle_standard_show_formats(format) + format.any(*supported_single_formats) do + export_single(request.format.symbol) + end + + format.atom do + atom_journals + end + + format.all do + head :not_acceptable + end + end + def save_export_settings # Saving export settings is only allowed for saved queries return false if @query.new_record? @@ -213,7 +241,9 @@ class WorkPackagesController < ApplicationController end def work_package - @work_package ||= WorkPackage.visible(current_user).find_by(id: params[:id]) + return @work_package if defined?(@work_package) + + @work_package = WorkPackage.visible(current_user).find_by(id: params[:id]) end def journals @@ -258,4 +288,16 @@ class WorkPackagesController < ApplicationController def login_back_url_params params.permit(:query_id, :state, :query_props) end + + def redirect_to_complete_route + # redirect /work_packages/:id to a full route with project and tab + redirect_to action: "show", + id: params[:id], + project_id: params[:project_id] || work_package.project.identifier, + tab: params[:tab] || "activity" + end + + def show_route_incomplete? + params[:project_id].blank? || params[:tab].blank? + end end diff --git a/app/models/work_package.rb b/app/models/work_package.rb index 438ed0e2b0c..f0611c9626f 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -277,6 +277,11 @@ class WorkPackage < ApplicationRecord "#{type.name unless type.is_standard} ##{id}: #{subject}" end + def infoline(show_standard_type: true) + type_name = show_standard_type || !type.is_standard ? type.name : "" + "#{type_name}: #{subject} (##{id})" + end + # Return true if the work_package is closed, otherwise false def closed? status.nil? || status.is_closed? diff --git a/app/seeders/demo_data/references.rb b/app/seeders/demo_data/references.rb index 192de2624df..543e10f821c 100644 --- a/app/seeders/demo_data/references.rb +++ b/app/seeders/demo_data/references.rb @@ -133,7 +133,7 @@ module DemoData url_helpers.project_work_package_path( id: work_package.id, project_id: work_package.project.identifier, - state: "activity" + tab: "activity" ) end end diff --git a/app/views/layouts/angular/angular.html.erb b/app/views/layouts/angular/angular.html.erb index 31a27e4b7f4..ba72c7ab00f 100644 --- a/app/views/layouts/angular/angular.html.erb +++ b/app/views/layouts/angular/angular.html.erb @@ -31,6 +31,8 @@ See COPYRIGHT and LICENSE files for more details. <% html_title(*local_assigns[:page_title]) if local_assigns[:page_title].present? %> <%= call_hook :view_work_package_overview_attributes %> + + <% end -%> <%= content_for :content_body do %> diff --git a/app/views/work_packages/copy.html.erb b/app/views/work_packages/copy.html.erb new file mode 100644 index 00000000000..0431d67cbf9 --- /dev/null +++ b/app/views/work_packages/copy.html.erb @@ -0,0 +1,45 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 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. + +++#%> + +<% html_title(t('activerecord.attributes.project.work_packages')) -%> + +<% content_for :sidebar do %> + <%= render partial: "sidebar" %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {query_id: query, format: "atom", page: nil, key: User.current.rss_key}, title: t(:label_work_package_plural)) %> + <%= auto_discovery_link_tag(:atom, {controller: "/journals", action: "index", query_id: query, format: "atom", page: nil, key: User.current.rss_key}, title: t(:label_changes_details)) %> +<% end %> + +<% content_for :content_body do %> + <%= render WorkPackages::FullView::CopyComponent.new(type: params[:type], + copied_from_work_package_id: params[:id], + project: @project) %> +<% end %> diff --git a/app/views/work_packages/new.html.erb b/app/views/work_packages/new.html.erb new file mode 100644 index 00000000000..b9fa694d61e --- /dev/null +++ b/app/views/work_packages/new.html.erb @@ -0,0 +1,44 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 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. + +++#%> + +<% html_title(t('activerecord.attributes.project.work_packages')) -%> + +<% content_for :sidebar do %> + <%= render partial: "sidebar" %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {query_id: query, format: "atom", page: nil, key: User.current.rss_key}, title: t(:label_work_package_plural)) %> + <%= auto_discovery_link_tag(:atom, {controller: "/journals", action: "index", query_id: query, format: "atom", page: nil, key: User.current.rss_key}, title: t(:label_changes_details)) %> +<% end %> + +<% content_for :content_body do %> + <%= render WorkPackages::FullView::CreateComponent.new(type: params[:type], + project: @project) %> +<% end %> diff --git a/app/views/work_packages/show.html.erb b/app/views/work_packages/show.html.erb index 136812d9175..ba824c6b45d 100644 --- a/app/views/work_packages/show.html.erb +++ b/app/views/work_packages/show.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<% html_title h(work_package.to_s) %> +<% html_title h(work_package.infoline) %> <% content_for :sidebar do %> <%= render partial: 'sidebar' %> @@ -38,3 +38,7 @@ See COPYRIGHT and LICENSE files for more details. { format: 'atom', key: User.current.rss_key }, title: "#{work_package.project} - #{work_package.to_s}") %> <% end %> + +<% content_for :content_body do %> + <%= render WorkPackages::FullView::ShowComponent.new(id: work_package.id, tab: params[:tab] || "activity") %> +<% end %> diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 3366186bc60..731e088d339 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -290,7 +290,8 @@ Rails.application.reloader.to_prepare do wpt.permission :add_work_packages, { - work_package_relations: %i[new create] + work_package_relations: %i[new create], + work_packages: %i[new] }, permissible_on: :project, dependencies: :view_work_packages, @@ -313,7 +314,10 @@ Rails.application.reloader.to_prepare do contract_actions: { work_packages: %i[move] } wpt.permission :copy_work_packages, - { "work_packages/moves": %i[new create] }, + { + "work_packages/moves": %i[new create], + work_packages: %i[copy] + }, permissible_on: %i[work_package project], require: :loggedin, dependencies: :view_work_packages, diff --git a/config/routes.rb b/config/routes.rb index 8f6004d5be9..e25b85f913d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -386,7 +386,7 @@ Rails.application.routes.draw do # work as a catchall for everything under /wiki get "wiki" => "wiki#show" - resources :work_packages, only: [] do + resources :work_packages, only: %i[index show] do collection do get "/report/:detail" => "work_packages/reports#report_details" get "/report" => "work_packages/reports#report" @@ -394,14 +394,16 @@ Rails.application.routes.draw do get "/export_dialog" => "work_packages#export_dialog" end + get "/copy" => "work_packages#copy", on: :member, as: "copy" + get "/new" => "work_packages#new", on: :collection, as: "new" + + get "(/:tab)" => "work_packages#show", on: :member, as: "", + constraints: { id: /\d+/, state: /(?!(shares|copy|dialog)).+/ } + # states managed by client-side routing on work_package#index - get "(/*state)" => "work_packages#index", on: :collection, as: "", constraints: { state: /(?!(dialog)).+/ } + get "(/*state)" => "work_packages#index", on: :collection, as: "", constraints: { state: /(?!(dialog|new)).+/ } get "/create_new" => "work_packages#index", on: :collection, as: "new_split" - get "/new" => "work_packages#index", on: :collection, as: "new" - - # state for show view in project context - get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(dialog)).+/ } end namespace :work_packages do @@ -715,7 +717,7 @@ Rails.application.routes.draw do get "/bulk" => "bulk#destroy" end - resources :work_packages, only: [:index] do + resources :work_packages, only: %i[index show new] do concerns :shareable get "hover_card" => "work_packages/hover_card#show", on: :member @@ -797,12 +799,13 @@ Rails.application.routes.draw do get "/split_view/get_relations_counter" => "work_packages/split_view#get_relations_counter", on: :member + get "/copy" => "work_packages#copy", on: :member, as: "copy" + get "(/:tab)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|new|copy)).+/ } + # states managed by client-side (angular) routing on work_package#show get "/" => "work_packages#index", on: :collection, as: "index" get "/create_new" => "work_packages#index", on: :collection, as: "new_split" - get "/new" => "work_packages#index", on: :collection, as: "new", state: "new" - # We do not want to match the work package export routes - get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|split_view)).+/ } + get "/share_upsell" => "work_packages#share_upsell", on: :collection, as: "share_upsell" get "/edit" => "work_packages#show", on: :member, as: "edit" end diff --git a/docker/pullpreview/docker-compose.yml b/docker/pullpreview/docker-compose.yml index 6950a0deb7e..7be578a1adf 100644 --- a/docker/pullpreview/docker-compose.yml +++ b/docker/pullpreview/docker-compose.yml @@ -13,6 +13,8 @@ volumes: x-defaults: &defaults build: context: . + args: + DEBIAN_BASE: bookworm restart: unless-stopped env_file: - .env.pullpreview diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index a70a5ae9348..e053997a690 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -204,6 +204,9 @@ import { OpWpDatePickerInstanceComponent, } from 'core-app/shared/components/datepicker/wp-date-picker-modal/wp-date-picker-instance.component'; import { TimeEntryTimerService } from 'core-app/shared/components/time_entries/services/time-entry-timer.service'; +import { WorkPackageFullCopyEntryComponent } from 'core-app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component'; +import { WorkPackageFullCreateEntryComponent } from 'core-app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component'; +import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component'; import { MyPageComponent } from './features/my-page/my-page.component'; import { DashboardComponent } from './features/overview/dashboard.component'; @@ -392,6 +395,9 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-reminder-settings', ReminderSettingsPageComponent, { injector }); registerCustomElement('opce-notification-center', InAppNotificationCenterComponent, { injector }); registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector }); + registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector }); + registerCustomElement('opce-wp-full-create', WorkPackageFullCreateEntryComponent, { injector }); + registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector }); registerCustomElement('opce-timer-account-menu', TimerAccountMenuComponent, { injector }); registerCustomElement('opce-remote-field-updater', RemoteFieldUpdaterComponent, { injector }); registerCustomElement('opce-wp-date-picker-instance', OpWpDatePickerInstanceComponent, { injector }); diff --git a/frontend/src/app/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts index 72eda9eed25..026ac4529d2 100644 --- a/frontend/src/app/core/path-helper/path-helper.service.ts +++ b/frontend/src/app/core/path-helper/path-helper.service.ts @@ -212,6 +212,10 @@ export class PathHelperService { return `${this.staticBase}/work_packages`; } + public workPackageNewPath():string { + return `${this.staticBase}/work_packages/new`; + } + public projectWorkPackageNewPath(projectId:string) { return `${this.workPackagesPath(projectId)}/new`; } @@ -300,11 +304,23 @@ export class PathHelperService { return `${this.staticBase}/work_packages/${id}`; } + public genericWorkPackagePath(projectIdentifier:string|null, workPackageId:string|number, tab = 'activity') { + if (projectIdentifier) { + return `${this.projectWorkPackagePath(projectIdentifier, workPackageId)}/${tab}`; + } + + return `${this.workPackagePath(workPackageId)}/${tab}`; + } + public workPackageShortPath(id:string|number) { return `${this.staticBase}/wp/${id}`; } - public workPackageCopyPath(workPackageId:string|number) { + public workPackageCopyPath(projectIdentifier:string|null, workPackageId:string|number) { + if (projectIdentifier) { + return `${this.workPackagesPath(projectIdentifier)}/${workPackageId}/copy`; + } + return `${this.workPackagePath(workPackageId)}/copy`; } @@ -316,6 +332,7 @@ export class PathHelperService { return `${this.workPackagesPath(projectIdentifier)}/details/${workPackageId}`; } + // Todo: Remove? public workPackageDetailsCopyPath(projectIdentifier:string, workPackageId:string|number) { return this.workPackageDetailsPath(projectIdentifier, workPackageId, 'copy'); } diff --git a/frontend/src/app/core/setup/globals/onboarding/helpers.ts b/frontend/src/app/core/setup/globals/onboarding/helpers.ts index ee162ca3901..fc125415959 100644 --- a/frontend/src/app/core/setup/globals/onboarding/helpers.ts +++ b/frontend/src/app/core/setup/globals/onboarding/helpers.ts @@ -1,5 +1,5 @@ export const onboardingTourStorageKey = 'openProject-onboardingTour'; -export type OnboardingTourNames = 'homescreen'|'workPackages'|'gantt'|'final'|'boards'|'teamPlanner'; +export type OnboardingTourNames = 'homescreen'|'workPackages'|'workPackagesFullView'|'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 a1b652395a0..b299a9094f6 100644 --- a/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts +++ b/frontend/src/app/core/setup/globals/onboarding/onboarding_tour.ts @@ -18,6 +18,7 @@ import { ganttOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding import { ConfigurationService } from 'core-app/core/config/configuration.service'; import 'core-vendor/enjoyhint'; +import { wpFullViewOnboardingTourSteps } from 'core-app/core/setup/globals/onboarding/tours/work_package_full_view_tour'; declare global { interface Window { @@ -77,6 +78,16 @@ function workPackageTour() { }); } + +function workPackageFullViewTour() { + initializeTour('wpFullViewTourFinished'); + waitForElement('.work-package--single-view', '#content', () => { + const steps:OnboardingStep[] = wpFullViewOnboardingTourSteps(); + + startTour(steps); + }); +} + function ganttTour(configuration:ConfigurationService) { initializeTour('ganttTourFinished'); @@ -147,6 +158,9 @@ export function start(name:OnboardingTourNames, configuration:ConfigurationServi case 'workPackages': workPackageTour(); break; + case 'workPackagesFullView': + workPackageFullViewTour(); + break; case 'gantt': ganttTour(configuration); 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 fe54b4945ed..d709489d7a8 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 @@ -76,8 +76,13 @@ export function detectOnboardingTour():void { void triggerTour('workPackages'); } - // ------------------------------- Tutorial Gantt module ------------------------------- + // ------------------------------- Tutorial WP Full page ------------------------------- if (currentTourPart === 'wpTourFinished') { + void triggerTour('workPackagesFullView'); + } + + // ------------------------------- Tutorial Gantt module ------------------------------- + if (currentTourPart === 'wpFullViewTourFinished') { void triggerTour('gantt'); return; } diff --git a/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts new file mode 100644 index 00000000000..e5a73eea2c7 --- /dev/null +++ b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_full_view_tour.ts @@ -0,0 +1,29 @@ +import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboarding_tour'; + +export function wpFullViewOnboardingTourSteps():OnboardingStep[] { + return [ + { + 'next .work-packages-full-view--split-left': I18n.t('js.onboarding.steps.wp.full_view'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + containerClass: '-dark -hidden-arrow', + onNext() { + jQuery('.main-menu--arrow-left-to-project')[0].click(); + }, + }, + { + 'next #main-menu-gantt': I18n.t('js.onboarding.steps.wp.gantt_menu'), + showSkip: false, + nextButton: { text: I18n.t('js.onboarding.buttons.next') }, + onNext() { + 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/core/setup/globals/onboarding/tours/work_package_tour.ts b/frontend/src/app/core/setup/globals/onboarding/tours/work_package_tour.ts index e366a94916d..2973030e08f 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 @@ -3,28 +3,6 @@ import { OnboardingStep } from 'core-app/core/setup/globals/onboarding/onboardin export function wpOnboardingTourSteps():OnboardingStep[] { return [ - { - 'next .wp-table--row': I18n.t('js.onboarding.steps.wp.list'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - onNext() { - jQuery('.inline-edit--display-field.id a ')[0].click(); - }, - }, - { - 'next .work-packages-full-view--split-left': I18n.t('js.onboarding.steps.wp.full_view'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - containerClass: '-dark -hidden-arrow', - }, - { - 'next .work-packages-back-button': I18n.t('js.onboarding.steps.wp.back_button'), - showSkip: false, - nextButton: { text: I18n.t('js.onboarding.buttons.next') }, - onNext() { - jQuery('.work-packages-back-button')[0].click(); - }, - }, { 'next .add-work-package': I18n.t('js.onboarding.steps.wp.create_button'), showSkip: false, @@ -37,24 +15,16 @@ export function wpOnboardingTourSteps():OnboardingStep[] { waitForElement('#work-packages-filter-toggle-button .badge', '#content', () => { resolve(undefined); }); - }), - onNext() { - jQuery('.main-menu--arrow-left-to-project')[0].click(); - }, + }) }, { - 'next #main-menu-gantt': I18n.t('js.onboarding.steps.wp.gantt_menu'), + 'next .wp-table--row': I18n.t('js.onboarding.steps.wp.list'), showSkip: false, nextButton: { text: I18n.t('js.onboarding.buttons.next') }, onNext() { - jQuery('#main-menu-gantt')[0].click(); + const firstId = document.querySelectorAll('.inline-edit--display-field.id a ')[0].innerHTML; + window.location.href = `${window.location.origin}/projects/demo-project/work_packages/${firstId}/activity`; }, - }, - { - containerClass: '-dark -hidden-arrow', - onBeforeStart() { - window.location.href = `${window.location.origin}/projects/demo-project/gantt`; - }, - }, + } ]; } diff --git a/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts b/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts index 61930df3c99..e2166f50c39 100644 --- a/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts +++ b/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts @@ -110,7 +110,6 @@ export class IFCViewerPageComponent component: WorkPackageCreateButtonComponent, inputs: { stateName$: of(this.newRoute), - allowed: ['work_packages.createWorkPackage', 'work_package.copy'], }, }, { diff --git a/frontend/src/app/features/boards/board/board-list/board-list.component.ts b/frontend/src/app/features/boards/board/board-list/board-list.component.ts index afe97dd31ab..bef95fe310f 100644 --- a/frontend/src/app/features/boards/board/board-list/board-list.component.ts +++ b/frontend/src/app/features/boards/board/board-list/board-list.component.ts @@ -15,9 +15,7 @@ import { LoadingIndicatorService, withLoadingIndicator, } from 'core-app/core/loading-indicator/loading-indicator.service'; -import { - WorkPackageInlineCreateService, -} from 'core-app/features/work-packages/components/wp-inline-create/wp-inline-create.service'; +import { WorkPackageInlineCreateService } from 'core-app/features/work-packages/components/wp-inline-create/wp-inline-create.service'; import { BoardInlineCreateService } from 'core-app/features/boards/board/board-list/board-inline-create.service'; import { AbstractWidgetComponent } from 'core-app/shared/components/grids/widgets/abstract-widget.component'; import { I18nService } from 'core-app/core/i18n/i18n.service'; @@ -25,59 +23,49 @@ import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space'; import { Board } from 'core-app/features/boards/board/board'; import { AuthorisationService } from 'core-app/core/model-auth/model-auth.service'; -import { - Highlighting, -} from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions'; -import { - WorkPackageCardViewComponent, -} from 'core-app/features/work-packages/components/wp-card-view/wp-card-view.component'; -import { - WorkPackageStatesInitializationService, -} from 'core-app/features/work-packages/components/wp-list/wp-states-initialization.service'; +import { Highlighting } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions'; +import { WorkPackageCardViewComponent } from 'core-app/features/work-packages/components/wp-card-view/wp-card-view.component'; +import { WorkPackageStatesInitializationService } from 'core-app/features/work-packages/components/wp-list/wp-states-initialization.service'; import { BoardService } from 'core-app/features/boards/board/board.service'; -import { - HalResourceEditingService, -} from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service'; +import { HalResourceEditingService } from 'core-app/shared/components/fields/edit/services/hal-resource-editing.service'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; -import { - BoardActionsRegistryService, -} from 'core-app/features/boards/board/board-actions/board-actions-registry.service'; +import { BoardActionsRegistryService } from 'core-app/features/boards/board/board-actions/board-actions-registry.service'; import { BoardActionService } from 'core-app/features/boards/board/board-actions/board-action.service'; import { ComponentType } from '@angular/cdk/portal'; import { CausedUpdatesService } from 'core-app/features/boards/board/caused-updates/caused-updates.service'; import { BoardListMenuComponent } from 'core-app/features/boards/board/board-list/board-list-menu.component'; import { debugLog } from 'core-app/shared/helpers/debug_output'; -import { - WorkPackageCardDragAndDropService, -} from 'core-app/features/work-packages/components/wp-card-view/services/wp-card-drag-and-drop.service'; +import { WorkPackageCardDragAndDropService } from 'core-app/features/work-packages/components/wp-card-view/services/wp-card-drag-and-drop.service'; import { BoardFiltersService } from 'core-app/features/boards/board/board-filter/board-filters.service'; -import { StateService, TransitionService } from '@uirouter/core'; import { - WorkPackageViewFocusService, -} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service'; + StateService, + TransitionService, +} from '@uirouter/core'; +import { WorkPackageViewFocusService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service'; +import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service'; +import { BoardListCrossSelectionService } from 'core-app/features/boards/board/board-list/board-list-cross-selection.service'; import { - WorkPackageViewSelectionService, -} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service'; -import { - BoardListCrossSelectionService, -} from 'core-app/features/boards/board/board-list/board-list-cross-selection.service'; -import { debounceTime, filter, map } from 'rxjs/operators'; + debounceTime, + filter, + map, +} from 'rxjs/operators'; import { ChangeItem } from 'core-app/shared/components/fields/changeset/changeset'; import { WorkPackageChangeset } from 'core-app/features/work-packages/components/wp-edit/work-package-changeset'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { ApiV3Filter } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; -import { - KeepTabService, -} from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service'; +import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { QueryResource } from 'core-app/features/hal/resources/query-resource'; -import { HalEvent, HalEventsService } from 'core-app/features/hal/services/hal-events.service'; +import { + HalEvent, + HalEventsService, +} from 'core-app/features/hal/services/hal-events.service'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { firstValueFrom } from 'rxjs'; -import { - WorkPackageIsolatedQuerySpaceDirective, -} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { WorkPackageIsolatedQuerySpaceDirective } from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; export interface DisabledButtonPlaceholder { text:string; @@ -160,7 +148,8 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni public buttonPlaceholder:DisabledButtonPlaceholder|undefined; - constructor(readonly apiv3Service:ApiV3Service, + constructor( + readonly apiv3Service:ApiV3Service, readonly I18n:I18nService, readonly state:StateService, readonly cdRef:ChangeDetectorRef, @@ -184,7 +173,9 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni readonly boardActionRegistry:BoardActionsRegistryService, readonly causedUpdates:CausedUpdatesService, readonly keepTab:KeepTabService, - readonly $state:StateService) { + readonly currentProject:CurrentProjectService, + readonly pathHelper:PathHelperService, + ) { super(I18n, injector); } @@ -498,10 +489,9 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni openFullViewOnDoubleClick(event:{ workPackageId:string, double:boolean }) { if (event.double) { - this.state.go( - 'work-packages.show', - { workPackageId: event.workPackageId }, - ); + const projectIdentifier = this.currentProject.identifier; + const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, event.workPackageId) + window.location.search; + Turbo.visit(link, { action: 'advance' }); } } @@ -511,7 +501,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } 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 4ca0647a9d1..3868f298ab7 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 @@ -84,9 +84,6 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { /** Whether the board is editable */ editable:boolean; - /** Go back to boards using back-button */ - backButtonCallback:() => void; - /** Current query title to render */ selectedTitle?:string; diff --git a/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts b/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts index 63b8dc130ef..1035ee11671 100644 --- a/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts +++ b/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts @@ -70,9 +70,6 @@ export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePage unsaved_title: this.I18n.t('js.calendar.unsaved_title'), }; - /** Go back using back-button */ - backButtonCallback:() => void; - breadcrumbItems() { return [ { href: this.pathHelperService.homePath(), text: this.titleService.appTitle }, diff --git a/frontend/src/app/features/hal/resources/work-package-resource.ts b/frontend/src/app/features/hal/resources/work-package-resource.ts index 41eaaaabbab..9e05688eaeb 100644 --- a/frontend/src/app/features/hal/resources/work-package-resource.ts +++ b/frontend/src/app/features/hal/resources/work-package-resource.ts @@ -176,10 +176,8 @@ export class WorkPackageBaseResource extends HalResource { * Return ": (#)" if type and id are known. */ public subjectWithType(truncateSubject = 40):string { - const type = this.type ? `${this.type.name}: ` : ''; - const subject = this.subjectWithId(truncateSubject); - - return `${type}${subject}`; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return `${this.type.name}: ${this.subjectWithId(truncateSubject)}`; } /** @@ -187,9 +185,12 @@ export class WorkPackageBaseResource extends HalResource { */ public subjectWithId(truncateSubject = 40):string { const id = isNewResource(this) ? '' : ` (#${this.id || ''})`; - const subject = truncateSubject <= 0 ? this.subject : _.truncate(this.subject, { length: truncateSubject }); - return `${subject}${id}`; + return `${this.truncatedSubject(truncateSubject)}${id}`; + } + + public truncatedSubject(length = 40):string { + return length <= 0 ? this.subject : _.truncate(this.subject, { length: length }); } public get isLeaf():boolean { diff --git a/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.html b/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.html index bfcb2b6dcc4..01e403fe667 100644 --- a/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.html +++ b/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.html @@ -14,9 +14,9 @@ class="op-ian-item--work-package-id-link spot-link" [class.spot-link_inactive]="isMobile()" [attr.title]="workPackage.subject" - uiSref="work-packages.show" - [uiParams]="{workPackageId: workPackage.id, projects: null, projectPath: null}" [textContent]="'#' + workPackage.id" + [attr.href]="fullScreenLink()" + (click)="onLinkClick($event)" > @if (project) { diff --git a/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.ts b/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.ts index 0d368a7595d..cca7189c6a8 100644 --- a/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.ts +++ b/frontend/src/app/features/in-app-notifications/entry/in-app-notification-entry.component.ts @@ -110,10 +110,20 @@ export class InAppNotificationEntryComponent implements OnInit { } showFullView():void { - const href = this.notification._links.resource?.href; - const id = href && HalResource.matchFromLink(href, 'work_packages'); + if (!this.workPackageId) { + return; + } - this.storeService.openFullView(id); + const link = this.pathHelper.workPackagePath(this.workPackageId) + window.location.search; + Turbo.visit(link, { action: 'advance' }); + } + + fullScreenLink():string { + return this.workPackageId ? this.pathHelper.workPackagePath(this.workPackageId) : this.pathHelper.workPackagesPath(null); + } + + onLinkClick(e:Event):void { + e.stopPropagation(); } projectClicked(event:MouseEvent):void { // eslint-disable-line class-methods-use-this diff --git a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts index 2551c433579..d1760f9120c 100644 --- a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts @@ -52,9 +52,6 @@ export class TeamPlannerPageComponent extends PartitionedQuerySpacePageComponent unsaved_title: this.I18n.t('js.team_planner.unsaved_title'), }; - /** Go back using back-button */ - backButtonCallback:() => void; - /** Current query title to render */ selectedTitle = this.text.unsaved_title; diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts index ba3c405a15f..d33d7121eb1 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts @@ -855,7 +855,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } diff --git a/frontend/src/app/features/work-packages/components/back-routing/back-button.component.sass b/frontend/src/app/features/work-packages/components/back-routing/back-button.component.sass index 1ff3ecbb8bc..4b81c9a5de4 100644 --- a/frontend/src/app/features/work-packages/components/back-routing/back-button.component.sass +++ b/frontend/src/app/features/work-packages/components/back-routing/back-button.component.sass @@ -10,7 +10,3 @@ &--icon font-size: 1rem line-height: 22px - - &_mobile-limited-width - @media only screen and (max-width: $breakpoint-sm) - width: 64px diff --git a/frontend/src/app/features/work-packages/components/back-routing/back-button.component.ts b/frontend/src/app/features/work-packages/components/back-routing/back-button.component.ts index 026eafaf792..8cda6114a8c 100644 --- a/frontend/src/app/features/work-packages/components/back-routing/back-button.component.ts +++ b/frontend/src/app/features/work-packages/components/back-routing/back-button.component.ts @@ -40,21 +40,17 @@ import { I18nService } from 'core-app/core/i18n/i18n.service'; export class BackButtonComponent { @Input() public linkClass:string; - @Input() public customBackMethod:() => unknown; - public text = { goBack: this.I18n.t('js.button_back'), }; - constructor(readonly backRoutingService:BackRoutingService, - readonly I18n:I18nService) { + constructor( + readonly backRoutingService:BackRoutingService, + readonly I18n:I18nService, + ) { } public goBack():void { - if (this.customBackMethod) { - this.customBackMethod(); - } else { - this.backRoutingService.goBack(); - } + this.backRoutingService.goBack(); } } diff --git a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts index e8c11bca152..b287fabf2ed 100644 --- a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.component.ts @@ -27,12 +27,16 @@ //++ import { - Component, Input, EventEmitter, Output, + Component, + EventEmitter, + Input, + Output, } from '@angular/core'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { WorkPackageRelationsHierarchyService } from 'core-app/features/work-packages/components/wp-relations/wp-relations-hierarchy/wp-relations-hierarchy.service'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; @Component({ templateUrl: './wp-breadcrumb-parent.html', @@ -59,6 +63,7 @@ export class WorkPackageBreadcrumbParentComponent { protected readonly I18n:I18nService, protected readonly wpRelationsHierarchy:WorkPackageRelationsHierarchyService, protected readonly notificationService:WorkPackageNotificationService, + protected readonly pathHelper:PathHelperService, ) { } @@ -103,4 +108,9 @@ export class WorkPackageBreadcrumbParentComponent { this.onSwitch.emit(this.editing); } } + + public switchToFullscreenForWp(wp:WorkPackageResource):void { + const link = this.pathHelper.genericWorkPackagePath(wp.project?.identifier, wp.id!) + window.location.search; + Turbo.visit(link, { action: 'advance' }); + } } diff --git a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.html b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.html index 7d5b038daaf..8a9cdbaa175 100644 --- a/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.html +++ b/frontend/src/app/features/work-packages/components/wp-breadcrumb/wp-breadcrumb-parent.html @@ -2,8 +2,7 @@ @if (parent) { diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts index 99f47a24988..5ea48055cdf 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts @@ -43,10 +43,10 @@ import { CurrentUserService } from 'core-app/core/current-user/current-user.serv standalone: false, }) export class WorkPackageCreateButtonComponent extends UntilDestroyedMixin implements OnInit, OnDestroy { - @Input('allowed') allowedWhen:string[]; - @Input('stateName$') stateName$:Observable; + @Input() routedFromAngular:boolean = true; + allowed:boolean; disabled:boolean; diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html index da0b5279f33..bd58022e259 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html @@ -8,6 +8,7 @@ [stateName]="stateName$ | async" [dropdownActive]="allowed" [title]="text.title" + [routedFromAngular]="routedFromAngular" > ((resolve, reject) => { diff --git a/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.html b/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.html index b79fca923d7..3706b4ff49d 100644 --- a/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.html +++ b/frontend/src/app/features/work-packages/components/wp-details/wp-details-toolbar.html @@ -20,6 +20,7 @@ (Transition); + this.stateParams = transition.params('to'); + } + this.closeEditFormWhenNewWorkPackageSaved(); this.showForm(); @@ -117,23 +125,22 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O super.ngOnDestroy(); } - public switchToFullscreen() { - const type = idFromLink(this.change.value('type')?.href); - void this.$state.go('work-packages.new', { ...this.$state.params, type }); - } - public onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }) { const { savedResource, isInitial } = params; this.editForm?.cancel(false); - if (this.successState) { + if(this.routedFromAngular && this.successState) { this.$state.go(this.successState, { workPackageId: savedResource.id }) .then(() => { this.wpViewFocus.updateFocus(savedResource.id!); this.notificationService.showSave(savedResource, isInitial); }); + } else { + window.OpenProject.pageState = 'submitted'; + Turbo.visit(this.pathHelper.projectWorkPackagePath(savedResource.project.identifier, savedResource.id!) + window.location.search); } + } protected showForm() { @@ -188,9 +195,15 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O this.titleService.setFirstPart(this.I18n.t('js.work_packages.create.title')); } - public cancelAndBackToList() { + public cancelAndBack() { this.wpCreate.cancelCreation(); - this.$state.go(this.cancelState, this.$state.params); + + if (this.routedFromAngular) { + this.$state.go(this.cancelState, this.$state.params); + } else { + const link = this.stateParams.projectPath ? this.pathHelper.workPackagesPath(this.stateParams.projectPath) : this.pathHelper.workPackagesPath(null); + window.location.href = (link + window.location.search); + } } protected createdWorkPackage() { diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts index cae01743536..2b53b53fb47 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts @@ -37,7 +37,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; standalone: false, }) export class WorkPackageNewFullViewComponent extends WorkPackageCreateComponent { - public successState = this.$state.current.data.successState as string; + public successState = (this.$state?.current?.data?.successState as string) || ''; breadcrumbItems() { const items = []; diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html index 49fc2ac7224..ba553097295 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html @@ -27,8 +27,8 @@ - + [showProject]="copying" /> + } diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html index 1bc711f169e..443bfef74c6 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html @@ -9,18 +9,12 @@ - - - - - - + { let callback:(transition:any) => void; @@ -34,6 +36,8 @@ describe('keepTab service', () => { let $state:any; let $transitions:any; let uiRouterGlobals:any; + let pathHelper:any; + let currentProject:any; let keepTab:KeepTabService; let defaults:any; @@ -53,7 +57,7 @@ describe('keepTab service', () => { params: { tabIdentifier: 'activity' }, }; - keepTab = new KeepTabService($state, uiRouterGlobals, $transitions); + keepTab = new KeepTabService($state, uiRouterGlobals, $transitions, pathHelper, currentProject); defaults = { showTab: 'work-packages.show.tabs', diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts index 7a4e6b2e952..6246174d058 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts @@ -27,11 +27,16 @@ //++ import { - StateService, Transition, TransitionService, UIRouterGlobals, + StateService, + Transition, + TransitionService, + UIRouterGlobals, } from '@uirouter/core'; import { ReplaySubject } from 'rxjs'; import { Injectable } from '@angular/core'; import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; @Injectable({ providedIn: 'root' }) export class KeepTabService { @@ -39,9 +44,13 @@ export class KeepTabService { protected subject = new ReplaySubject<{ [tab:string]:string; }>(1); - constructor(protected $state:StateService, + constructor( + protected $state:StateService, protected uiRouterGlobals:UIRouterGlobals, - protected $transitions:TransitionService) { + protected $transitions:TransitionService, + protected pathHelper:PathHelperService, + protected currentProject:CurrentProjectService, + ) { this.updateTabs(); $transitions.onSuccess({}, (transition:Transition) => { this.updateTabs(transition.params('to').tabIdentifier); @@ -63,15 +72,9 @@ export class KeepTabService { return this.currentDetailsTab; } - public goCurrentShowState(params:Record = {}):void { - this.$state.go( - 'work-packages.show.tabs', - { - ...this.uiRouterGlobals.params, - ...params, - tabIdentifier: this.currentShowTab, - }, - ); + public goCurrentShowState(workPackageId:string):void { + const projectIdentifier = this.currentProject.identifier; + window.location.href = this.pathHelper.genericWorkPackagePath(projectIdentifier, workPackageId, this.currentShowTab) + window.location.search; } public goCurrentDetailsState(params:Record = {}):void { diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts index 7a5e26908d3..96742911769 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/watchers-tab/watchers-tab.component.ts @@ -95,6 +95,7 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme public ngOnInit() { this.$element = jQuery(this.elementRef.nativeElement); + const { workPackageId } = this.uiRouterGlobals.params as unknown as { workPackageId:string }; this.workPackageId = (this.workPackage.id as string) || workPackageId; @@ -180,7 +181,9 @@ export class WorkPackageWatchersTabComponent extends UntilDestroyedMixin impleme } public updateCounter() { - const url = this.pathHelper.workPackageUpdateCounterPath(this.workPackageId, 'watchers'); - void this.turboRequests.request(url); + if (this.workPackageId !== undefined) { + const url = this.pathHelper.workPackageUpdateCounterPath(this.workPackageId, 'watchers'); + void this.turboRequests.request(url); + } } } diff --git a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts index 5862a4b0f8e..28b8c71b626 100644 --- a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts @@ -26,7 +26,10 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { Component, Input, OnInit } from '@angular/core'; +import { + Component, + Input, +} from '@angular/core'; import { UIRouterGlobals } from '@uirouter/core'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { randomString } from 'core-app/shared/helpers/random-string'; @@ -38,29 +41,15 @@ import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; templateUrl: './wp-subject.html', standalone: false, }) -export class WorkPackageSubjectComponent extends UntilDestroyedMixin implements OnInit { +export class WorkPackageSubjectComponent extends UntilDestroyedMixin { @Input('workPackage') workPackage:WorkPackageResource; public readonly uniqueElementIdentifier = `work-packages--subject-type-row-${randomString(16)}`; - constructor(protected uiRouterGlobals:UIRouterGlobals, - protected apiV3Service:ApiV3Service) { + constructor( + protected uiRouterGlobals:UIRouterGlobals, + protected apiV3Service:ApiV3Service, + ) { super(); } - - ngOnInit() { - if (!this.workPackage) { - this - .apiV3Service - .work_packages - .id(this.uiRouterGlobals.params.workPackageId) - .requireAndStream() - .pipe( - this.untilDestroyed(), - ) - .subscribe((wp:WorkPackageResource) => { - this.workPackage = wp; - }); - } - } } diff --git a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts index 0287ba60741..b3d13cd589f 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-base.component.ts @@ -20,6 +20,7 @@ import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; import { WorkPackageStatesInitializationService } from '../../wp-list/wp-states-initialization.service'; import { firstValueFrom } from 'rxjs'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; @Directive() export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewBase implements AfterViewInit { @@ -53,6 +54,8 @@ export abstract class WorkPackageEmbeddedBaseComponent extends WorkPackagesViewB @InjectField() currentProject:CurrentProjectService; + @InjectField() pathHelper:PathHelperService; + @InjectField() cdRef:ChangeDetectorRef; ngOnInit() { diff --git a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts index 3e87aee2716..f7717558be2 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts @@ -181,10 +181,9 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) { if (event.double) { - this.$state.go( - 'work-packages.show', - { workPackageId: event.workPackageId }, - ); + const projectIdentifier = this.currentProject.identifier; + const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, event.workPackageId) + window.location.search; + Turbo.visit(link, { action: 'advance' }); } } @@ -197,7 +196,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } } diff --git a/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/details-table-action.ts b/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/details-table-action.ts index 47b3c0a09fa..e9ae14b0260 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/details-table-action.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/table-actions/actions/details-table-action.ts @@ -4,13 +4,19 @@ import { opIconElement } from 'core-app/shared/helpers/op-icon-builder'; import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service'; import { UiStateLinkBuilder } from 'core-app/features/work-packages/components/wp-fast-table/builders/ui-state-link-builder'; import { StateService } from '@uirouter/core'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; export const detailsLinkClassName = 'wp-table--details-link'; export class OpDetailsTableAction extends OpTableAction { public readonly identifier = 'open-details-action'; - private uiStatebuilder = new UiStateLinkBuilder(this.injector.get(StateService), this.injector.get(KeepTabService)); + private uiStatebuilder = new UiStateLinkBuilder( + this.injector.get(StateService), + this.injector.get(KeepTabService), + this.injector.get(CurrentProjectService), + this.injector.get(PathHelperService)); private text = { button: this.I18n.t('js.button_open_details'), diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab.ts b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab.ts index 85478fa123e..ecde439655a 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab.ts @@ -10,7 +10,7 @@ export interface TabComponent extends Component { export interface WpTabDefinition extends TabDefinition { component:Type; - displayable?:(workPackage:WorkPackageResource, $state:StateService) => boolean; + displayable?:(workPackage:WorkPackageResource, $state:StateService|null) => boolean; count?:(workPackage:WorkPackageResource, injector:Injector) => Observable; showCountAsBubble?:boolean; } diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts index 3cc865a698a..2dd873ce1b2 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts @@ -29,6 +29,7 @@ import { UIRouterGlobals } from '@uirouter/core'; import { Component, + Input, OnInit, } from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; @@ -45,6 +46,9 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag standalone: false, }) export class WpTabWrapperComponent implements OnInit { + @Input() public workPackageId:string; + @Input() public tabIdentifier:string; + workPackage:WorkPackageResource; ndcDynamicInputs$:Observable<{ @@ -52,17 +56,18 @@ export class WpTabWrapperComponent implements OnInit { tab:WpTabDefinition | undefined; }>; - get workPackageId():string { - const { workPackageId } = this.uiRouterGlobals.params as unknown as { workPackageId:string }; - return workPackageId; - } - - constructor(readonly I18n:I18nService, + constructor( + readonly I18n:I18nService, readonly uiRouterGlobals:UIRouterGlobals, readonly apiV3Service:ApiV3Service, - readonly wpTabsService:WorkPackageTabsService) {} + readonly wpTabsService:WorkPackageTabsService + ) {} ngOnInit() { + if (this.workPackageId === undefined) { + this.workPackageId = this.uiRouterGlobals.params.workPackageId; + } + this.ndcDynamicInputs$ = this .apiV3Service .work_packages @@ -77,8 +82,10 @@ export class WpTabWrapperComponent implements OnInit { } findTab(workPackage:WorkPackageResource):WpTabDefinition | undefined { - const { tabIdentifier } = this.uiRouterGlobals.params as unknown as { tabIdentifier:string }; + if (this.tabIdentifier === undefined) { + this.tabIdentifier = this.uiRouterGlobals.params.tabIdentifier; + } - return this.wpTabsService.getTab(tabIdentifier, workPackage); + return this.wpTabsService.getTab(this.tabIdentifier, workPackage); } } diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html index 0742174762f..bb22a4aa544 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html @@ -1,5 +1,6 @@ @if (view === 'split') { diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.spec.ts b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.spec.ts index b83d184b4a2..2bef9e25905 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.spec.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.spec.ts @@ -8,6 +8,8 @@ import { ScrollableTabsComponent } from 'core-app/shared/components/tabs/scrolla import { WorkPackageTabsService } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service'; import { WpTabsComponent } from './wp-tabs.component'; import { TabComponent } from '../wp-tab-wrapper/tab'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; describe('WpTabsComponent', () => { class TestComponent implements TabComponent { @@ -39,6 +41,8 @@ describe('WpTabsComponent', () => { { provide: StateService, useValue: { includes: () => false } }, { provide: UIRouterGlobals, useValue: {} }, { provide: KeepTabService, useValue: {} }, + { provide: CurrentProjectService, useValue: {} }, + { provide: PathHelperService, useValue: {} }, WorkPackageTabsService, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts index c7a4f1bea8a..30186f6d9b7 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts @@ -9,6 +9,9 @@ import { WorkPackageTabsService, } from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { WpTabDefinition } from 'core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/tab'; @Component({ selector: 'op-wp-tabs', @@ -22,9 +25,11 @@ export class WpTabsComponent implements OnInit { @Input() view:'full'|'split'; - public tabs:TabDefinition[]; + @Input() routedFromAngular:boolean = true; - public uiSrefBase:string; + @Input() public currentTabId:string|null = null; + + public tabs:TabDefinition[]; public canViewWatchers = false; @@ -42,28 +47,38 @@ export class WpTabsComponent implements OnInit { readonly $state:StateService, readonly uiRouterGlobals:UIRouterGlobals, readonly keepTab:KeepTabService, + readonly pathHelper:PathHelperService, + readonly currentProject:CurrentProjectService, ) { } ngOnInit():void { - this.uiSrefBase = this.view === 'split' ? '' : 'work-packages.show'; this.canViewWatchers = !!(this.workPackage && this.workPackage.watchers); this.tabs = this.getDisplayableTabs(); } - private getDisplayableTabs() { + private getDisplayableTabs():WpTabDefinition[]{ return this .wpTabsService - .getDisplayableTabs(this.workPackage) - .map((tab) => ({ - ...tab, - route: `${this.uiSrefBase}.tabs`, - routeParams: { workPackageId: this.workPackage.id, tabIdentifier: tab.id }, - })); + .getDisplayableTabs(this.workPackage, this.routedFromAngular) + .map((tab) => { + if (this.routedFromAngular) { + return ({ + ...tab, + route: '.tabs', + routeParams: { workPackageId: this.workPackage.id, tabIdentifier: tab.id }, + }); + } + + return ({ + ...tab, + path: this.pathHelper.genericWorkPackagePath(this.currentProject.identifier, this.workPackage.id!, tab.id), + }); + }); } public switchToFullscreen():void { - this.keepTab.goCurrentShowState(); + this.keepTab.goCurrentShowState(this.workPackage.id!); } public close():void { diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts b/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts index adc2a379d91..09d9dd8d4d0 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts @@ -92,11 +92,11 @@ export class WorkPackageTabsService { } } - getDisplayableTabs(workPackage:WorkPackageResource):WpTabDefinition[] { + getDisplayableTabs(workPackage:WorkPackageResource, routedFromAngular = true):WpTabDefinition[] { return this .tabs .filter( - (tab) => !tab.displayable || tab.displayable(workPackage, this.$state), + (tab) => !tab.displayable || tab.displayable(workPackage, routedFromAngular ? this.$state : null), ) .map( (tab) => ({ @@ -118,7 +118,7 @@ export class WorkPackageTabsService { component: WorkPackageOverviewTabComponent, name: this.I18n.t('js.work_packages.tabs.overview'), id: 'overview', - displayable: (_, $state) => $state.includes('**.details.*'), + displayable: (_, $state) => $state ? $state.includes('**.details.*') : false, }, { id: 'activity', diff --git a/frontend/src/app/features/work-packages/openproject-work-packages.module.ts b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts index b5141b9b3ea..021d9d994fd 100644 --- a/frontend/src/app/features/work-packages/openproject-work-packages.module.ts +++ b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts @@ -297,9 +297,6 @@ import { GroupDescriptor, WorkPackageSingleViewComponent, } from 'core-app/features/work-packages/components/wp-single-view/wp-single-view.component'; -import { - WorkPackageCopySplitViewComponent, -} from 'core-app/features/work-packages/components/wp-copy/wp-copy-split-view.component'; import { WorkPackageFormAttributeGroupComponent, } from 'core-app/features/work-packages/components/wp-form-group/wp-attribute-group.component'; @@ -407,6 +404,9 @@ import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openpr import { WorkPackageReminderContextMenuDirective, } from 'core-app/features/work-packages/components/wp-buttons/wp-reminder-button/wp-reminder-context-menu.directive'; +import { WorkPackageFullCopyEntryComponent } from 'core-app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component'; +import { WorkPackageFullCreateEntryComponent } from 'core-app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component'; +import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component'; @NgModule({ imports: [ @@ -491,7 +491,6 @@ import { // WP Copy WorkPackageCopyFullViewComponent, - WorkPackageCopySplitViewComponent, // Embedded table WorkPackageEmbeddedTableComponent, @@ -602,6 +601,9 @@ import { // Full view WorkPackagesFullViewComponent, + WorkPackageFullViewEntryComponent, + WorkPackageFullCopyEntryComponent, + WorkPackageFullCreateEntryComponent, // Modals WpTableConfigurationModalComponent, diff --git a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html index 46cc9f3d442..f11671ec1bb 100644 --- a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html +++ b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html @@ -5,11 +5,6 @@ /> - @if (backButtonCallback !== undefined) { - - } void|undefined; - /** Which filter container component to mount */ filterContainerDefinition:DynamicComponentDefinition = { component: WorkPackageFilterContainerComponent, diff --git a/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts b/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts index de776930032..0cccd336bc2 100644 --- a/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts +++ b/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts @@ -35,6 +35,6 @@ import { StateService } from '@uirouter/angular'; */ export function splitViewRoute(state:StateService, target:'details'|'new' = 'details'):string { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const baseRoute:string = state.current.data.baseRoute || ''; + const baseRoute:string = state?.current?.data?.baseRoute || ''; return `${baseRoute}.${target}`; } diff --git a/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts b/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts index 9b05b51e476..1889b643950 100644 --- a/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts +++ b/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts @@ -30,7 +30,6 @@ import { WorkPackageNewSplitViewComponent } from 'core-app/features/work-package import { Ng2StateDeclaration } from '@uirouter/angular'; import { ComponentType } from '@angular/cdk/overlay'; import { WpTabWrapperComponent } from 'core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component'; -import { WorkPackageCopySplitViewComponent } from 'core-app/features/work-packages/components/wp-copy/wp-copy-split-view.component'; /** * Return a set of routes for a split view mounted under the given base route, @@ -125,23 +124,5 @@ export function makeSplitViewRoutes(baseRoute:string, 'content-right@^.^': { component: newComponent }, }, }, - // Split copy route - { - name: `${routeName}.copy`, - url: '/details/{copiedFromWorkPackageId:[0-9]+}/copy', - views: { - 'content-right@^.^': { component: WorkPackageCopySplitViewComponent }, - }, - reloadOnSearch: false, - data: { - baseRoute, - parent: baseRoute, - allowMovingInEditMode: true, - bodyClasses: 'router--work-packages-partitioned-split-view', - menuItem: menuItemClass, - partition: '-split', - mobileAlternative: showMobileAlternative ? 'work-packages.show' : undefined, - }, - }, ]; } diff --git a/frontend/src/app/features/work-packages/routing/work-packages-routes.ts b/frontend/src/app/features/work-packages/routing/work-packages-routes.ts index 0b94fc7f0b5..d6c8788e01a 100644 --- a/frontend/src/app/features/work-packages/routing/work-packages-routes.ts +++ b/frontend/src/app/features/work-packages/routing/work-packages-routes.ts @@ -27,7 +27,6 @@ //++ import { WpTabWrapperComponent } from 'core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component'; -import { WorkPackageNewFullViewComponent } from 'core-app/features/work-packages/components/wp-new/wp-new-full-view.component'; import { WorkPackagesFullViewComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view.component'; import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view.component'; import { Ng2StateDeclaration } from '@uirouter/angular'; @@ -68,70 +67,6 @@ export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [ name: { type: 'string', dynamic: true }, }, }, - { - name: 'work-packages.new', - url: '/new?type&parent_id', - component: WorkPackageNewFullViewComponent, - reloadOnSearch: false, - params: { - defaults: { - value: null, - }, - }, - data: { - baseRoute: 'work-packages', - allowMovingInEditMode: true, - bodyClasses: 'router--work-packages-full-create', - menuItem: menuItemClass, - successState: 'work-packages.show', - sideMenuOptions, - }, - }, - { - name: 'work-packages.copy', - url: '/{copiedFromWorkPackageId:[0-9]+}/copy', - component: WorkPackageCopyFullViewComponent, - reloadOnSearch: false, - data: { - baseRoute: 'work-packages', - allowMovingInEditMode: true, - bodyClasses: 'router--work-packages-full-create', - menuItem: menuItemClass, - sideMenuOptions, - }, - }, - { - name: 'work-packages.show', - url: '/{workPackageId:[0-9]+}', - // Redirect to 'activity' by default. - redirectTo: (trans) => { - const params = trans.params('to'); - const keepTab = trans.injector().get(KeepTabService) as KeepTabService; - const tabIdentifier = keepTab.currentShowTab; - return { - state: 'work-packages.show.tabs', - params: { ...params, tabIdentifier: tabIdentifier || 'activity' }, - }; - }, - component: WorkPackagesFullViewComponent, - data: { - baseRoute: 'work-packages', - bodyClasses: ['router--work-packages-full-view', 'router--work-packages-base'], - newRoute: 'work-packages.new', - menuItem: menuItemClass, - sideMenuOptions, - }, - }, - { - name: 'work-packages.show.tabs', - url: '/:tabIdentifier', - component: WpTabWrapperComponent, - data: { - parent: 'work-packages.show', - menuItem: menuItemClass, - sideMenuOptions, - }, - }, { name: 'work-packages.partitioned', component: WorkPackageViewPageComponent, diff --git a/frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts new file mode 100644 index 00000000000..4e6f76a9af1 --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts @@ -0,0 +1,62 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; + +/** + * An entry component to be rendered by Rails which opens an isolated query space + * for the work package split view + */ +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + standalone: false, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkPackageFullCopyEntryComponent { + @Input() type:string; + @Input() copiedFromWorkPackageId:string; + @Input() parentId?:string; + @Input() projectIdentifier?:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-full-create'); + } +} diff --git a/frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts new file mode 100644 index 00000000000..d5859bf0aab --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts @@ -0,0 +1,61 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; + +/** + * An entry component to be rendered by Rails which opens an isolated query space + * for the work package split view + */ +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + standalone: false, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkPackageFullCreateEntryComponent { + @Input() type:string; + @Input() parentId?:string; + @Input() projectIdentifier?:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-full-create'); + } +} diff --git a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts similarity index 56% rename from frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts rename to frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts index c4bde78e8e9..7c0b841a1fb 100644 --- a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts @@ -26,14 +26,36 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { WorkPackageCopyController } from 'core-app/features/work-packages/components/wp-copy/wp-copy.controller'; +import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; +/** + * An entry component to be rendered by Rails which opens an isolated query space + * for the work package full view + */ @Component({ - selector: 'wp-copy-split-view', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: '../wp-new/wp-new-split-view.html', + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], standalone: false, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class WorkPackageCopySplitViewComponent extends WorkPackageCopyController { +export class WorkPackageFullViewEntryComponent { + @Input() workPackageId:string; + @Input() activeTab:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-full-view', 'router--work-packages-base'); + } } diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts index 215a2d4ee51..4ff91cdc1fa 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts @@ -31,10 +31,10 @@ import { Component, HostListener, Injector, + Input, OnInit, } from '@angular/core'; import { StateService } from '@uirouter/core'; -import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; import { RecentItemsService } from 'core-app/core/recent-items.service'; import { ProjectResource } from 'core-app/features/hal/resources/project-resource'; @@ -48,7 +48,7 @@ import { Observable, of } from 'rxjs'; @Component({ templateUrl: './wp-full-view.html', - selector: 'wp-full-view-entry', + selector: 'op-wp-full-view', // Required class to support inner scrolling on page host: { class: 'work-packages-page--ui-view' }, providers: [ @@ -78,17 +78,14 @@ export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase imp }, }; - stateName$ = of('work-packages.new'); - constructor( public injector:Injector, public wpTableSelection:WorkPackageViewSelectionService, public recentItemsService:RecentItemsService, readonly $state:StateService, readonly currentUserService:CurrentUserService, - private readonly configurationService:ConfigurationService, ) { - super(injector, $state.params.workPackageId); + super(injector); } // enable other parts of the application to trigger an immediate update diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html index 86e8a3159bf..0de8d204398 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html @@ -7,16 +7,12 @@ - - + - + @if (displayShareButton$ | async) { - + + data-notification-selector='notification-scroll-container'> + diff --git a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts index e0b7c3b0ab3..61ab771a213 100644 --- a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts @@ -30,10 +30,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - Injector, - OnInit, ElementRef, + inject, + Injector, NgZone, + OnInit, } from '@angular/core'; import { take } from 'rxjs/operators'; import { CausedUpdatesService } from 'core-app/features/boards/board/caused-updates/caused-updates.service'; @@ -56,6 +57,7 @@ import { StateService } from '@uirouter/core'; import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service'; import { WorkPackageViewBaselineService } from '../wp-view-base/view-services/wp-view-baseline.service'; import { combineLatest } from 'rxjs'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; @Component({ selector: 'wp-list-view', @@ -71,6 +73,21 @@ import { combineLatest } from 'rxjs'; standalone: false, }) export class WorkPackageListViewComponent extends UntilDestroyedMixin implements OnInit { + readonly I18n = inject(I18nService); + readonly injector = inject(Injector); + readonly $state = inject(StateService); + readonly keepTab = inject(KeepTabService); + readonly querySpace = inject(IsolatedQuerySpace); + readonly wpViewFilters = inject(WorkPackageViewFiltersService); + readonly deviceService = inject(DeviceService); + readonly CurrentProject = inject(CurrentProjectService); + readonly wpDisplayRepresentation = inject(WorkPackageViewDisplayRepresentationService); + readonly cdRef = inject(ChangeDetectorRef); + readonly elementRef = inject(ElementRef); + readonly ngZone = inject(NgZone); + readonly wpTableBaseline = inject(WorkPackageViewBaselineService); + readonly pathHelper = inject(PathHelperService); + text = { jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.pagination'), text_jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.label_pagination'), @@ -96,24 +113,6 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements dragAndDropEnabled: true, }; - constructor( - readonly I18n:I18nService, - readonly injector:Injector, - readonly $state:StateService, - readonly keepTab:KeepTabService, - readonly querySpace:IsolatedQuerySpace, - readonly wpViewFilters:WorkPackageViewFiltersService, - readonly deviceService:DeviceService, - readonly CurrentProject:CurrentProjectService, - readonly wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService, - readonly cdRef:ChangeDetectorRef, - readonly elementRef:ElementRef, - private ngZone:NgZone, - readonly wpTableBaseline:WorkPackageViewBaselineService, - ) { - super(); - } - ngOnInit() { // Mark tableInformationLoaded when initially loading done this.setupInformationLoadedListener(); @@ -192,7 +191,7 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.openInFullView(event.workPackageId); } } @@ -209,9 +208,7 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements } private openInFullView(workPackageId:string) { - this.$state.go( - 'work-packages.show', - { workPackageId }, - ); + const projectIdentifier = this.CurrentProject.identifier; + window.location.href = this.pathHelper.genericWorkPackagePath(projectIdentifier, workPackageId) + window.location.search; } } diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts index 8f312a1721b..5613c3716b8 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts @@ -55,5 +55,7 @@ export class WorkPackageSplitViewEntryComponent { constructor(readonly elementRef:ElementRef) { populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-partitioned-split-view-details'); } } diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts index 4cfc14ef034..738e66511a9 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts @@ -70,9 +70,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp /** Reference to the base route e.g., work-packages.partitioned.list or bim.partitioned.split */ private baseRoute:string = this.$state.current?.data?.baseRoute as string; - @Input() workPackageId:string; @Input() showTabs = true; - @Input() activeTab?:string; @Input() resizerClass = 'work-packages-partitioned-page--content-right'; @@ -89,7 +87,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp readonly backRouting:BackRoutingService, readonly wpTabs:WorkPackageTabsService, ) { - super(injector, $state.params.workPackageId); + super(injector); } // enable other parts of the application to trigger an immediate update @@ -104,16 +102,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp this.observeWorkPackage(); const wpId = (this.$state.params.workPackageId || this.workPackageId) as string; - const focusedWP = this.wpTableFocus.focusedWorkPackage; - - if (!focusedWP) { - // Focus on the work package if we're the first route - const isFirstRoute = this.firstRoute.name === `${this.baseRoute}.details.overview`; - const isSameID = this.firstRoute.params && wpId === this.firstRoute.params.workPackageI; - this.wpTableFocus.updateFocus(wpId, (isFirstRoute && isSameID)); - } else { - this.wpTableFocus.updateFocus(wpId, false); - } + this.wpTableFocus.updateFocus(wpId, false); if (this.wpTableSelection.isEmpty) { this.wpTableSelection.setRowState(wpId, true); diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html index 27c5c0e46f4..f662ff05abb 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.html @@ -48,7 +48,7 @@ data-notification-selector='notification-scroll-container' > + [ndcDynamicInputs]="{ workPackage: workPackage, tab: tabIdentifier }" /> } diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts index 89851497d45..5184962137e 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts @@ -26,7 +26,12 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { ChangeDetectorRef, Injector } from '@angular/core'; +import { + ChangeDetectorRef, + Directive, + Injector, + Input, +} from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { @@ -62,10 +67,20 @@ import { ProjectsResourceService } from 'core-app/core/state/projects/projects.s import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { HttpErrorResponse } from '@angular/common/http'; +import { StateService } from '@uirouter/angular'; + +@Directive() +export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { + @Input() routedFromAngular:boolean = true; + + @Input() workPackageId:string; + + @Input() activeTab:string = 'activity'; -export class WorkPackageSingleViewBase extends UntilDestroyedMixin { @InjectField() states:States; + @InjectField() $state:StateService; + @InjectField() i18n:I18nService; @InjectField() keepTab:KeepTabService; @@ -115,9 +130,12 @@ export class WorkPackageSingleViewBase extends UntilDestroyedMixin { constructor( public injector:Injector, - protected workPackageId:string, ) { super(); + + if (this.routedFromAngular && this.workPackageId === undefined) { + this.workPackageId = this.$state.params.workPackageId as string; + } } /** @@ -139,6 +157,9 @@ export class WorkPackageSingleViewBase extends UntilDestroyedMixin { this.workPackage = wp; } + // Push the current title + this.titleService.setFirstPart(this.workPackage.subjectWithType(-1)); + this.cdRef.detectChanges(); }, (error) => { this.handleLoadingError(error); @@ -177,9 +198,6 @@ export class WorkPackageSingleViewBase extends UntilDestroyedMixin { // Set authorisation data this.authorisationService.initModelAuth('work_package', this.workPackage.$links); - // Push the current title - this.titleService.setFirstPart(this.workPackage.subjectWithType(-1)); - // Preselect this work package for future list operations this.showStaticPagePath = this.PathHelper.workPackagePath(this.workPackageId); diff --git a/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts b/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts index 389d8a0b927..241732f03ac 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts @@ -68,7 +68,6 @@ export class WorkPackageViewPageComponent extends PartitionedQuerySpacePageCompo component: WorkPackageCreateButtonComponent, inputs: { stateName$: of(this.stateName), - allowed: ['work_packages.createWorkPackage'], }, }, { diff --git a/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts b/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts index 5e4aadf7f18..dba82f99d5f 100644 --- a/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts +++ b/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts @@ -82,7 +82,7 @@ export class WorkPackageAuthorization { if (stateName.indexOf('work-packages.partitioned.list.details') === 0) { return this.PathHelper.workPackageDetailsCopyPath(this.project.identifier, this.workPackage.id as string); } - return this.PathHelper.workPackageCopyPath(this.workPackage.id as string); + return this.PathHelper.workPackageCopyPath(this.project.identifier, this.workPackage.id as string); } private shortLink() { diff --git a/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts index a73cc993fcc..28ede5a7928 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/linked-work-package-display-field.module.ts @@ -31,6 +31,8 @@ import { KeepTabService } from 'core-app/features/work-packages/components/wp-si import { UiStateLinkBuilder } from 'core-app/features/work-packages/components/wp-fast-table/builders/ui-state-link-builder'; import { WorkPackageDisplayField } from 'core-app/shared/components/fields/display/field-types/work-package-display-field.module'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; export class LinkedWorkPackageDisplayField extends WorkPackageDisplayField { public text = { @@ -42,7 +44,11 @@ export class LinkedWorkPackageDisplayField extends WorkPackageDisplayField { @InjectField() keepTab!:KeepTabService; - private uiStateBuilder:UiStateLinkBuilder = new UiStateLinkBuilder(this.$state, this.keepTab); + @InjectField() currentProject!:CurrentProjectService; + + @InjectField() pathHelper!:PathHelperService; + + private uiStateBuilder:UiStateLinkBuilder = new UiStateLinkBuilder(this.$state, this.keepTab, this.currentProject, this.pathHelper); public render(element:HTMLElement, displayText:string):void { if (this.isEmpty()) { diff --git a/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts b/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts index 4c6009d35c6..7e16c426e49 100644 --- a/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts +++ b/frontend/src/app/shared/components/fields/display/field-types/wp-id-display-field.module.ts @@ -31,13 +31,19 @@ import { StateService } from '@uirouter/core'; import { UiStateLinkBuilder } from 'core-app/features/work-packages/components/wp-fast-table/builders/ui-state-link-builder'; import { IdDisplayField } from 'core-app/shared/components/fields/display/field-types/id-display-field.module'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; export class WorkPackageIdDisplayField extends IdDisplayField { @InjectField() $state!:StateService; @InjectField() keepTab!:KeepTabService; - private uiStateBuilder:UiStateLinkBuilder = new UiStateLinkBuilder(this.$state, this.keepTab); + @InjectField() currentProject!:CurrentProjectService; + + @InjectField() pathHelper!:PathHelperService; + + private uiStateBuilder:UiStateLinkBuilder = new UiStateLinkBuilder(this.$state, this.keepTab, this.currentProject, this.pathHelper); public render(element:HTMLElement, displayText:string):void { if (!this.value) { diff --git a/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts b/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts index 90855cea16e..1b83db1e8e7 100644 --- a/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts +++ b/frontend/src/app/shared/components/modals/wp-destroy-modal/wp-destroy.modal.ts @@ -29,22 +29,24 @@ import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service'; import { States } from 'core-app/core/states/states.service'; import { - ChangeDetectorRef, Component, ElementRef, Inject, OnInit, + ChangeDetectorRef, + Component, + ElementRef, + Inject, + OnInit, } from '@angular/core'; import { OpModalComponent } from 'core-app/shared/components/modal/modal.component'; import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service'; import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; -import { - WorkPackageViewFocusService, -} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service'; +import { WorkPackageViewFocusService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-focus.service'; import { StateService } from '@uirouter/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { BackRoutingService } from 'core-app/features/work-packages/components/back-routing/back-routing.service'; -import { - WorkPackageNotificationService, -} from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; +import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; import { WorkPackageService } from 'core-app/features/work-packages/services/work-package.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { BackRoutingService } from 'core-app/features/work-packages/components/back-routing/back-routing.service'; @Component({ templateUrl: './wp-destroy.modal.html', @@ -82,7 +84,8 @@ export class WpDestroyModalComponent extends OpModalComponent implements OnInit deletesChildren: '', }; - constructor(readonly elementRef:ElementRef, + constructor( + readonly elementRef:ElementRef, readonly workPackageService:WorkPackageService, @Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, readonly I18n:I18nService, @@ -92,7 +95,10 @@ export class WpDestroyModalComponent extends OpModalComponent implements OnInit readonly wpTableFocus:WorkPackageViewFocusService, readonly wpListService:WorkPackagesListService, readonly notificationService:WorkPackageNotificationService, - readonly backRoutingService:BackRoutingService) { + readonly currentProject:CurrentProjectService, + readonly pathHelper:PathHelperService, + readonly backRoutingService:BackRoutingService, + ) { super(locals, cdRef, elementRef); } @@ -154,10 +160,11 @@ export class WpDestroyModalComponent extends OpModalComponent implements OnInit this.busy = false; this.closeMe($event); this.wpTableFocus.clear('Clearing after destroying work packages'); - - // Go back to a previous list state if we're in a split or full view - if (this.$state.current.data.baseRoute) { + if (this.$state.current.data?.baseRoute) { this.backRoutingService.goBack(true); + } else { + const projectIdentifier = this.currentProject.identifier; + window.location.href = this.pathHelper.workPackagesPath(projectIdentifier) + window.location.search; } }) .catch(() => { diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts index 318e198cc41..41816c51bad 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts @@ -29,17 +29,19 @@ import { OpContextMenuItem } from 'core-app/shared/components/op-context-menu/op-context-menu.types'; import { StateService } from '@uirouter/core'; import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service'; -import { Directive, ElementRef, Input } from '@angular/core'; +import { + Directive, + ElementRef, + Input, +} from '@angular/core'; import { isClickedWithModifier } from 'core-app/shared/helpers/link-handling/link-handling'; -import { - OpContextMenuTrigger, -} from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive'; -import { BrowserDetector } from 'core-app/core/browser/browser-detector.service'; +import { OpContextMenuTrigger } from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive'; import { WorkPackageCreateService } from 'core-app/features/work-packages/components/wp-new/wp-create.service'; -import { - Highlighting, -} from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions'; +import { Highlighting } from 'core-app/features/work-packages/components/wp-fast-table/builders/highlighting/highlighting.functions'; import { TypeResource } from 'core-app/features/hal/resources/type-resource'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { extendSearchParams } from 'core-stimulus/helpers/url-helpers'; @Directive({ selector: '[opTypesCreateDropdown]', @@ -52,14 +54,17 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger { @Input('dropdownActive') active:boolean; + @Input() routedFromAngular:boolean = true; + public isOpen = false; constructor( readonly elementRef:ElementRef, readonly opContextMenu:OPContextMenuService, - readonly browserDetector:BrowserDetector, readonly wpCreate:WorkPackageCreateService, readonly $state:StateService, + readonly pathHelper:PathHelperService, + readonly currentProject:CurrentProjectService, ) { super(elementRef, opContextMenu); } @@ -70,11 +75,6 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger { if (!this.active) { return; } - - // Force full-view create if in mobile view - if (this.browserDetector.isMobile) { - this.stateName = 'work-packages.new'; - } } protected open(evt:JQuery.TriggeredEvent) { @@ -113,12 +113,19 @@ export class OpTypesContextMenuDirective extends OpContextMenuTrigger { ariaLabel: type.name, class: Highlighting.inlineClass('type', type.id!), onClick: ($event:JQuery.TriggeredEvent) => { - this.isOpen = false; - if (isClickedWithModifier($event)) { - return false; - } + if (this.routedFromAngular) { + this.isOpen = false; + if (isClickedWithModifier($event)) { + return false; + } - this.$state.go(this.stateName, { type: type.id }); + this.$state.go(this.stateName, { type: type.id }); + } else { + window.location.href = extendSearchParams( + this.pathHelper.projectWorkPackageNewPath(this.currentProject.id!), + { type: type.id! }, + ); + } return true; }, })); diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts index d48e510d268..b529ee7e694 100644 --- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts +++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts @@ -91,7 +91,9 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger void this.timeEntryService.stop(); break; case 'copy': - this.$state.go('work-packages.copy', { copiedFromWorkPackageId: this.workPackage.id }); + if (this.workPackage.id) { + window.location.href = `${this.PathHelper.workPackageCopyPath(this.workPackage.project.identifier, this.workPackage.id)}`; + } break; case 'delete': this.opModalService.show(WpDestroyModalComponent, this.injector, { workPackages: [this.workPackage] }); diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts index 7bc4ad54316..7501feb4b68 100644 --- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts @@ -29,6 +29,7 @@ import { WpDestroyModalComponent } from 'core-app/shared/components/modals/wp-de import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; export class WorkPackageViewContextMenu extends OpContextMenuHandler { @InjectField() protected states!:States; @@ -43,6 +44,8 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { @InjectField() protected WorkPackageContextMenuHelper!:WorkPackageContextMenuHelperService; + @InjectField() protected currentProject:CurrentProjectService; + @InjectField() protected pathHelper:PathHelperService; @InjectField() protected turboRequests:TurboRequestsService; @@ -103,7 +106,7 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { this.editSelectedWorkPackages(link!); break; - case 'copy': + case 'duplicate': this.copySelectedWorkPackages(link!); break; @@ -162,11 +165,9 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { return; } - const params = { - copiedFromWorkPackageId: selected[0].id, - }; - - this.$state.go(`${this.baseRoute}.copy`, params); + if (selected[0].id) { + window.location.href = this.pathHelper.workPackageCopyPath(selected[0].project.id, selected[0].id); + } } private logTimeForSelectedWorkPackage() { @@ -206,21 +207,22 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { })); if (selected.length === 1 && !isNewResource(this.workPackage)) { + const projectIdentifier = this.currentProject.identifier; + const link = this.pathHelper.genericWorkPackagePath(projectIdentifier, this.workPackageId) + window.location.search; + items.unshift({ disabled: false, icon: 'icon-view-fullscreen', class: 'openFullScreenView', - href: this.$state.href('work-packages.show', { workPackageId: this.workPackageId }), + href: link, linkText: I18n.t('js.button_open_fullscreen'), onClick: ($event:JQuery.TriggeredEvent) => { if (isClickedWithModifier($event)) { return false; } - this.$state.go( - 'work-packages.show', - { workPackageId: this.workPackageId }, - ); + Turbo.visit(link, { action: 'advance' }); + return true; }, }); diff --git a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html index 9014d0c970f..0d0d9af2847 100644 --- a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html +++ b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.html @@ -28,8 +28,20 @@ [attr.aria-selected]="tab.id === currentTabId" (click)="clickTab(tab, $event)" [attr.title]="tabTitle(tab)" - [textContent]="tab.name" > + + @if (counter(tab) | async; as tabCounter) { + @if (tab.showCountAsBubble) { + + } + @if (tabCounter > 0 && !tab.showCountAsBubble) { + ({{ tabCounter }}) + } + } } @if (tab.route) { diff --git a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts index 72da4ff6226..7141da78899 100644 --- a/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts +++ b/frontend/src/app/shared/components/tabs/scrollable-tabs/scrollable-tabs.component.ts @@ -118,10 +118,11 @@ export class ScrollableTabsComponent extends UntilDestroyedMixin implements Afte this.currentTabId = tab.id; this.tabSelected.emit(tab); - // If the tab does not provide its own link, - // avoid propagation - if (!tab.path) { - event.preventDefault(); + event.preventDefault(); + + // Override history to avoid that browser back leads you to a different tab instead of the page you originated from + if (tab.path) { + Turbo.visit(tab.path, { action: document.referrer != '' ? 'replace' : 'advance' }) } } diff --git a/frontend/src/global_styles/content/work_packages/single_view/_single_view.sass b/frontend/src/global_styles/content/work_packages/single_view/_single_view.sass index 6e571b7d6c8..525116d3063 100644 --- a/frontend/src/global_styles/content/work_packages/single_view/_single_view.sass +++ b/frontend/src/global_styles/content/work_packages/single_view/_single_view.sass @@ -39,7 +39,7 @@ display: grid grid-template-columns: auto 1fr auto grid-template-rows: minmax(30px, auto) 1fr - grid-template-areas: "breadcrumb breadcrumb breadcrumb" "backButton subject toolbar" + grid-template-areas: "breadcrumb breadcrumb breadcrumb" "subject subject toolbar" align-items: center grid-column-gap: 5px @@ -47,9 +47,6 @@ grid-area: breadcrumb align-self: baseline - &--back-button - grid-area: backButton - &--subject grid-area: subject diff --git a/frontend/src/global_styles/layout/work_packages/_mobile.sass b/frontend/src/global_styles/layout/work_packages/_mobile.sass index 3e6ddbbd1be..7c927662743 100644 --- a/frontend/src/global_styles/layout/work_packages/_mobile.sass +++ b/frontend/src/global_styles/layout/work_packages/_mobile.sass @@ -114,10 +114,6 @@ grid-template-areas: "backButton toolbar" "breadcrumb breadcrumb" "subject subject" grid-row-gap: 0.5rem - // On mobile, we will not show the back button any more. The space will be taken by the menu toggler. - .op-back-button - display: none - .work-packages-full-view--split-container border-top: none diff --git a/frontend/src/stimulus/helpers/url-helpers.ts b/frontend/src/stimulus/helpers/url-helpers.ts new file mode 100644 index 00000000000..85ba1084088 --- /dev/null +++ b/frontend/src/stimulus/helpers/url-helpers.ts @@ -0,0 +1,51 @@ +/* + * -- copyright + * OpenProject is an open source project management software. + * Copyright (C) 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. + * ++ + */ + +/** + * Extend a given URL (string or URL object) with the provided search parameters. + * + * @param base The base URL to extend + * @param params A record of key-value pairs to add as search parameters + * @param addCurrentSearch Whether to include the current window's search parameters (default: true) + */ +export function extendSearchParams( + base:string, + params:Record, + addCurrentSearch = true, +) { + const url = new URL(base, window.location.origin); + url.search = addCurrentSearch ? window.location.search : ''; + + Object.entries(params).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); + + return url.toString(); +} diff --git a/lib/redmine/menu_manager/top_menu/quick_add_menu.rb b/lib/redmine/menu_manager/top_menu/quick_add_menu.rb index 7c5c6852d07..7ab3fe971ad 100644 --- a/lib/redmine/menu_manager/top_menu/quick_add_menu.rb +++ b/lib/redmine/menu_manager/top_menu/quick_add_menu.rb @@ -129,7 +129,7 @@ module Redmine::MenuManager::TopMenu::QuickAddMenu classes: "__hl_inline_type_#{type_id}" } else { caption: type_name, - href: new_work_packages_path(type: type_id), + href: new_work_package_path(type: type_id), classes: "__hl_inline_type_#{type_id}" } end end diff --git a/modules/bim/spec/features/bim_navigation_spec.rb b/modules/bim/spec/features/bim_navigation_spec.rb index b382cd7dbf9..1f26eba8979 100644 --- a/modules/bim/spec/features/bim_navigation_spec.rb +++ b/modules/bim/spec/features/bim_navigation_spec.rb @@ -122,14 +122,13 @@ RSpec.describe "BIM navigation spec", :js, with_config: { edition: "bim" } do details_view.expect_closed end - it "after deleting an WP in full view it returns to the model and list view (see #33317)" do + it "after deleting an WP in split view it returns to the model and list view (see #33317)" do # Go to full single view card_view.open_split_view_by_info_icon(work_package) - details_view.switch_to_fullscreen - full_view.expect_tab "Activity" + details_view.expect_tab "Overview" # Delete via the context menu - find("#action-show-more-dropdown-menu .button").click + page.find_test_selector("wp-details-toolbar--show-more-button").click find(".menu-item", text: "Delete").click destroy_modal.expect_listed(work_package) diff --git a/modules/boards/spec/features/board_navigation_spec.rb b/modules/boards/spec/features/board_navigation_spec.rb index 3e63c1e06a1..30ba4d05bd0 100644 --- a/modules/boards/spec/features/board_navigation_spec.rb +++ b/modules/boards/spec/features/board_navigation_spec.rb @@ -76,9 +76,11 @@ RSpec.describe "Work Package boards spec", expect(page).to have_current_path project_work_package_path(project, wp.id, "activity") # Click back goes back to the board - find(".work-packages-back-button").click + page.go_back expect(page).to have_current_path project_work_package_board_path(project, board_view) + wait_for_network_idle + # Open the details page with the info icon card = board_page.card_for(wp) split_view = card.open_details_view @@ -199,6 +201,7 @@ RSpec.describe "Work Package boards spec", # Go to full view of WP split_view.switch_to_fullscreen + wait_for_network_idle find_by_id("action-show-more-dropdown-menu").click click_link(I18n.t("js.button_delete")) @@ -208,8 +211,6 @@ RSpec.describe "Work Package boards spec", wait_for_network_idle - board_page.expect_query "List 1", editable: true - board_page.expect_not_any_card - board_page.expect_path + expect(page).to have_current_path "/projects/#{project.identifier}/work_packages" end end diff --git a/modules/boards/spec/features/support/board_page.rb b/modules/boards/spec/features/support/board_page.rb index ff70c03fca8..6df9283083c 100644 --- a/modules/boards/spec/features/support/board_page.rb +++ b/modules/boards/spec/features/support/board_page.rb @@ -365,9 +365,9 @@ module Pages def expect_query(name, editable: true) if editable - expect(page).to have_field("editable-toolbar-title", with: name) + expect(page).to have_field("editable-toolbar-title", with: name, wait: 10) else - expect(page).to have_css(".editable-toolbar-title--fixed", text: name) + expect(page).to have_css(".editable-toolbar-title--fixed", text: name, wait: 10) end end diff --git a/spec/controllers/work_packages_controller_spec.rb b/spec/controllers/work_packages_controller_spec.rb index 4756a82f8e1..ca4fde8d9e1 100644 --- a/spec/controllers/work_packages_controller_spec.rb +++ b/spec/controllers/work_packages_controller_spec.rb @@ -65,6 +65,9 @@ RSpec.describe WorkPackagesController do "with having the necessary permissions" do before do expect(WorkPackage).to receive_message_chain("visible.find_by").and_return(stub_work_package) + mock_permissions_for(current_user) do |mock| + mock.allow_in_project :view_work_packages, project: stub_work_package.project + end end instance_eval(&) @@ -261,10 +264,10 @@ RSpec.describe WorkPackagesController do let(:call_action) { get("show", params: { id: "1337" }) } requires_permission_in_project do - it "renders the show builder template" do + it "redirects to the full url" do call_action - expect(response).to render_template("work_packages/show") + expect(response).to redirect_to("/projects/test_project/work_packages/1337/activity") end end end diff --git a/spec/features/activities/work_package/activities_spec.rb b/spec/features/activities/work_package/activities_spec.rb index f4e471d8b1e..6ce7b89ab18 100644 --- a/spec/features/activities/work_package/activities_spec.rb +++ b/spec/features/activities/work_package/activities_spec.rb @@ -1453,6 +1453,10 @@ RSpec.describe "Work package activity", :js, :with_cuprite do context "when the current user does not have the activity tab open the whole time" do it "raises a conflict warning when the work package is updated by another user while the current user is editing" do + pending "This has become obselete with the removal of uiRouter (#67007), because switching tabs now result in a + hard reload and no conflict appears like described in these examples. + Before removing this, please check the polling controller for possible cleanups. + Ticket: https://community.openproject.org/wp/68630" using_session(:admin) do login_as(admin) diff --git a/spec/features/menu_items/quick_add_menu_spec.rb b/spec/features/menu_items/quick_add_menu_spec.rb index ece3c3c21d4..7d7a41c4c1c 100644 --- a/spec/features/menu_items/quick_add_menu_spec.rb +++ b/spec/features/menu_items/quick_add_menu_spec.rb @@ -148,7 +148,7 @@ RSpec.describe "Quick-add menu", :js do quick_add.expect_work_package_type other_project_type.name quick_add.click_link other_project_type.name - expect(page).to have_current_path new_work_packages_path(type: other_project_type.id) + expect(page).to have_current_path new_work_package_path(type: other_project_type.id) end end diff --git a/spec/features/notifications/navigation_spec.rb b/spec/features/notifications/navigation_spec.rb index aa2c23578d3..f209a968cf1 100644 --- a/spec/features/notifications/navigation_spec.rb +++ b/spec/features/notifications/navigation_spec.rb @@ -56,6 +56,7 @@ RSpec.describe "Notification center navigation", :js do # Close the split screen split_screen.close + wait_for_network_idle expect(page).to have_current_path "/notifications" end end diff --git a/spec/features/notifications/turbo_angular_navigation_spec.rb b/spec/features/notifications/turbo_angular_navigation_spec.rb index d2de0bfb98a..8e7f0c797b6 100644 --- a/spec/features/notifications/turbo_angular_navigation_spec.rb +++ b/spec/features/notifications/turbo_angular_navigation_spec.rb @@ -69,27 +69,25 @@ RSpec.describe "Turbo and Angular navigation integration", :js do # Step 3: Click on the work package #id from the notifications list # This should open the work package in full view center.click_id notification + wait_for_network_idle full_screen.expect_subject # Step 4: Click back once # Expected: Should go back to the notification center with split screen open - # Bug: The same work package page is displayed again page.go_back + wait_for_network_idle - # This assertion will fail with the current bug - it should show the split screen - # but instead shows the full work package view again expect(page).to have_current_path(/notifications/) split_screen.expect_open # Step 5: Click back a second time # Expected: Should go back to the notification center, but the work package sidebar is hidden - # Bug: The work package sidebar should be visible but is hidden page.go_back + wait_for_network_idle - # Sidebar should remain open expect(page).to have_current_path(/notifications/) center.expect_open - split_screen.expect_open + split_screen.expect_closed end end end diff --git a/spec/features/search/recently_viewed_work_packages_spec.rb b/spec/features/search/recently_viewed_work_packages_spec.rb index ffde8f86552..7079c501cbf 100644 --- a/spec/features/search/recently_viewed_work_packages_spec.rb +++ b/spec/features/search/recently_viewed_work_packages_spec.rb @@ -80,7 +80,7 @@ RSpec.describe "Recently viewed work packages", expect(page) .to have_css(".subject", text: work_package.subject) expect(page) - .to have_current_path project_work_package_path(work_package.project, work_package, state: "activity") + .to have_current_path project_work_package_path(work_package.project, work_package, "activity") end it "is not shown after typing something in the global search bar" do diff --git a/spec/features/search/search_spec.rb b/spec/features/search/search_spec.rb index 26a6f3104bd..fb7ff906519 100644 --- a/spec/features/search/search_spec.rb +++ b/spec/features/search/search_spec.rb @@ -136,7 +136,7 @@ RSpec.describe "Search", :js, :selenium, with_settings: { per_page_options: "5" .to have_css(".subject", text: target_work_package.subject) expect(page) - .to have_current_path project_work_package_path(target_work_package.project, target_work_package, state: "activity") + .to have_current_path project_work_package_path(target_work_package.project, target_work_package, "activity") search_target = work_packages.last @@ -153,7 +153,7 @@ RSpec.describe "Search", :js, :selenium, with_settings: { per_page_options: "5" .to have_css(".subject", text: search_target.subject) expect(page) - .to have_current_path project_work_package_path(search_target.project, search_target, state: "activity") + .to have_current_path project_work_package_path(search_target.project, search_target, "activity") # Typing a hash sign before an ID shall only suggest that work package and (no hits within the subject) global_search.search("##{search_target.id}", submit: false) diff --git a/spec/features/work_packages/attachments/attachment_upload_spec.rb b/spec/features/work_packages/attachments/attachment_upload_spec.rb index e88d24b815b..9bb1984c13e 100644 --- a/spec/features/work_packages/attachments/attachment_upload_spec.rb +++ b/spec/features/work_packages/attachments/attachment_upload_spec.rb @@ -62,7 +62,6 @@ RSpec.describe "Upload attachment to work package", :js, :selenium do before do wp_page.visit! wp_page.ensure_page_loaded - wp_page.switch_to_tab(tab: "Activity") wp_page.wait_for_activity_tab end @@ -226,23 +225,31 @@ RSpec.describe "Upload attachment to work package", :js, :selenium do describe "attachment dropzone" do shared_examples "attachment dropzone common" do it "can drag something to the files tab and have it open" do - wp_page.switch_to_tab(tab: "Activity") - wp_page.wait_for_activity_tab + wp_page.switch_to_tab(tab: "Files") + wait_for_network_idle - wp_page.expect_tab "Activity" + wp_page = Pages::FullWorkPackage.new(work_package, project) + wp_page.ensure_page_loaded + wp_page.expect_tab "Files" + + attachments = Components::Attachments.new attachments.drag_and_drop_file test_selector("op-attachments--drop-box"), image_fixture.path, :center, page.find('[data-qa-tab-id="files"]'), delay_dragleave: true - expect(page).to have_test_selector("op-files-tab--file-list-item-title", text: "image.png", wait: 10) editor.wait_until_upload_progress_toaster_cleared + expect(page).to have_test_selector("op-files-tab--file-list-item-title", text: "image.png", wait: 10) wp_page.expect_tab "Files" end it "can drag something from the files tab and create a comment with it" do - wp_page.switch_to_tab(tab: "files") + wp_page.switch_to_tab(tab: "Activity") + wait_for_network_idle + wp_page = Pages::FullWorkPackage.new(work_package, project) + wp_page.ensure_page_loaded + wp_page.wait_for_activity_tab attachments.drag_and_drop_file ".work-package-comment", image_fixture.path, diff --git a/spec/features/work_packages/cancel_editing_spec.rb b/spec/features/work_packages/cancel_editing_spec.rb index 8c260dbc604..ac74b5baba0 100644 --- a/spec/features/work_packages/cancel_editing_spec.rb +++ b/spec/features/work_packages/cancel_editing_spec.rb @@ -39,7 +39,7 @@ RSpec.describe "Cancel editing work package", :js, :selenium do let(:wp_table) { Pages::WorkPackagesTable.new } let(:paths) do [ - new_work_packages_path, + new_work_package_path, new_split_work_packages_path, new_project_work_packages_path(project), new_split_project_work_packages_path(project) @@ -135,56 +135,32 @@ RSpec.describe "Cancel editing work package", :js, :selenium do end end - it "allows to move from split to full screen in edit mode" do - # Start creating on split view - expect_active_edit(new_split_work_packages_path) - - find_by_id("wp-new-inline-edit--field-subject").set "foobar" - - # Expect editing works when moving to full screen - find(".work-packages-show-view-button").click - - expect(wp_page).not_to have_alert_dialog - expect(page).to have_css("#wp-new-inline-edit--field-subject") - expect_subject("foobar") - - # Moving back also works - page.execute_script("window.history.back()") - - expect(wp_page).not_to have_alert_dialog - expect(page).to have_css("#wp-new-inline-edit--field-subject") - expect_subject("foobar") - - # Cancel edition - find_by_id("work-packages--edit-actions-cancel").click - expect(wp_page).not_to have_alert_dialog - - # Visiting another page does not create alert - find(".op-logo--link").click - expect(wp_page).not_to have_alert_dialog - end - it "correctly cancels setting the back route (Regression #30714)" do - wp_page = Pages::FullWorkPackage.new work_package - wp_page.visit! + wp_table.visit! + wp_table.expect_work_package_listed(work_package, work_package2) + + # Edit subject in split page + wp_page = wp_table.open_split_view(work_package) wp_page.ensure_page_loaded # Edit description in full view description = wp_page.edit_field :description description.activate! + wait_for_network_idle + description.click_and_type_slowly "foobar" # Try to move back to list, expect warning - wp_page.go_back + page.execute_script("window.history.back()") wp_page.dismiss_alert_dialog! # Now cancel the field description.cancel_by_click # Now we should be able to get back to list - wp_page.go_back + page.execute_script("window.history.back()") - wp_table.expect_work_package_listed(work_package, work_package2) + expect(wp_page.has_alert_dialog?).to be false end context "when user does not want to be warned" do diff --git a/spec/features/work_packages/copy_spec.rb b/spec/features/work_packages/copy_spec.rb index cedffbb5f76..9f0886fc8a5 100644 --- a/spec/features/work_packages/copy_spec.rb +++ b/spec/features/work_packages/copy_spec.rb @@ -47,6 +47,7 @@ RSpec.describe "Work package copy", :js, :selenium do create(:project_role, permissions: %i[view_work_packages add_work_packages + copy_work_packages manage_work_package_relations edit_work_packages assign_versions]) @@ -151,48 +152,4 @@ RSpec.describe "Work package copy", :js, :selenium do to_copy_work_package_page.expect_and_dismiss_toaster message: I18n.t("js.notice_successful_create") end end - - it "on split screen page" do - original_work_package_page = Pages::SplitWorkPackage.new(original_work_package, project) - activity_tab = Components::WorkPackages::Activities.new(original_work_package) - to_copy_work_package_page = original_work_package_page.visit_copy! - - to_copy_work_package_page.expect_current_path - to_copy_work_package_page.expect_fully_loaded - - to_copy_work_package_page.update_attributes Description: "Copied WP Description" - - to_copy_work_package_page.save! - find(".op-toast--content", text: I18n.t("js.notice_successful_create"), wait: 20) - - copied_work_package = WorkPackage.order(created_at: "desc").first - - expect(copied_work_package).not_to eql original_work_package - - work_package_page = Pages::SplitWorkPackage.new(copied_work_package, project) - - work_package_page.ensure_page_loaded - - work_package_page.expect_attributes Subject: original_work_package.subject, - Description: "Copied WP Description", - Version: original_work_package.version, - Priority: original_work_package.priority, - Assignee: original_work_package.assigned_to, - Responsible: original_work_package.responsible - - work_package_page.switch_to_tab(tab: :activity) - activity_tab.expect_journal_details_header(text: user.name) - - work_package_page.switch_to_tab(tab: :overview) - - work_package_page.expect_current_path - - work_package_page.visit_tab!("relations") - expect_angular_frontend_initialized - work_package_page.expect_subject - loading_indicator_saveguard - - relations_tab.expect_relation_group(:relates) - relations_tab.expect_relation_by_text(original_work_package.subject) - end end diff --git a/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb b/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb index 5f940ab0518..05783597341 100644 --- a/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb +++ b/spec/features/work_packages/details/query_groups/relation_query_group_spec.rb @@ -84,9 +84,9 @@ RSpec.describe "Work package with relation query group", :js, :selenium do context "children table" do it "creates and removes across all tables" do - embedded_table.expect_work_package_count 1 relations_tab.click relations.expect_child(related_work_package) + embedded_table.expect_work_package_count 1 # Create new work package within embedded table embedded_table.table_container.find("button", text: I18n.t("js.relation_buttons.add_new_child")).click @@ -193,9 +193,9 @@ RSpec.describe "Work package with relation query group", :js, :selenium do end before do - embedded_table.expect_work_package_listed related_work_package relations_tab.click relations.expect_relation(related_work_package) + embedded_table.expect_work_package_listed related_work_package end it "creates and removes across all tables" do @@ -242,6 +242,11 @@ RSpec.describe "Work package with relation query group", :js, :selenium do # Check that deletion of relations still work after a page reload full_wp.visit! relations_tab = find(".op-tab-row--link", text: "RELATIONS") + relations_tab.click + + wait_for_network_idle + full_wp.ensure_page_loaded + relations = Components::WorkPackages::Relations.new(work_package) embedded_table = Pages::EmbeddedWorkPackagesTable.new(first("wp-single-view .work-packages-embedded-view--container")) @@ -250,7 +255,6 @@ RSpec.describe "Work package with relation query group", :js, :selenium do within(embedded_table.table_container) do embedded_table.ensure_work_package_not_listed!(independent_work_package) end - relations_tab.click within(relations.relations_group) do relations.expect_no_relation(independent_work_package) end diff --git a/spec/features/work_packages/navigation_spec.rb b/spec/features/work_packages/navigation_spec.rb index fa0d6e185b2..43aa4a9c1ac 100644 --- a/spec/features/work_packages/navigation_spec.rb +++ b/spec/features/work_packages/navigation_spec.rb @@ -36,9 +36,7 @@ RSpec.describe "Work package navigation", :js, :selenium do let(:work_package) { build(:work_package, project:) } let(:global_html_title) { Components::HtmlTitle.new } let(:project_html_title) { Components::HtmlTitle.new project } - let(:wp_title_segment) do - "#{work_package.type.name}: #{work_package.subject} (##{work_package.id})" - end + let(:wp_title_segment) { work_package.infoline } let!(:query) do query = build(:query, user:, project:) @@ -79,7 +77,8 @@ RSpec.describe "Work package navigation", :js, :selenium do full_work_package.expect_subject full_work_package.expect_current_path - global_html_title.expect_first_segment "#{wp_title_segment} | Work Packages" + + global_html_title.expect_first_segment "#{wp_title_segment} | Some project" # deep link work package details pane @@ -124,17 +123,17 @@ RSpec.describe "Work package navigation", :js, :selenium do full_work_package.expect_subject expect(page).to have_current_path project_work_package_path(project, work_package, "activity") - project_html_title.expect_first_segment "#{wp_title_segment} | Work Packages" + project_html_title.expect_first_segment wp_title_segment # Switch tabs full_work_package.switch_to_tab tab: :relations expect(page).to have_current_path project_work_package_path(project, work_package, "relations") - project_html_title.expect_first_segment "#{wp_title_segment} | Work Packages" + project_html_title.expect_first_segment wp_title_segment # Back to split screen using the button full_work_package.go_back global_work_packages.expect_work_package_listed(work_package) - expect(page).to have_current_path project_work_packages_path(project) + "/details/#{work_package.id}/relations" + expect(page).to have_current_path project_work_packages_path(project) + "/details/#{work_package.id}/overview" # Link to full screen from index global_work_packages.open_full_screen_by_link(work_package) @@ -149,12 +148,10 @@ RSpec.describe "Work package navigation", :js, :selenium do it "loading an unknown work package ID" do visit "/work_packages/999999999" - expect_flash type: :error, message: I18n.t(:notice_file_not_found) visit "/projects/#{project.identifier}/work_packages/999999999" - global_work_packages = Pages::WorkPackagesTable.new - global_work_packages.expect_toast type: :error, message: I18n.t("api_v3.errors.not_found.work_package") + expect_flash type: :error, message: I18n.t(:notice_file_not_found) end # Regression #29994 @@ -188,6 +185,8 @@ RSpec.describe "Work package navigation", :js, :selenium do visit my_page_path + wait_for_network_idle + page.find(".wp-table--cell-td.id a", text: work_package.id).click full_page = Pages::FullWorkPackage.new work_package, work_package.project @@ -208,7 +207,7 @@ RSpec.describe "Work package navigation", :js, :selenium do it "filters out the work package" do wp_table = Pages::WorkPackagesTable.new project - wp_table.visit! + wp_table.visit_query query wp_table.expect_work_package_listed work_package full_view = wp_table.open_full_screen_by_link work_package @@ -218,10 +217,13 @@ RSpec.describe "Work package navigation", :js, :selenium do subject.update "bar" full_view.expect_and_dismiss_toaster message: "Successful update." + work_package.reload + expect(work_package.subject).to eq("bar") # Go back to list full_view.go_back + expect(page).to have_field("editable-toolbar-title", with: query.name, wait: 10) wp_table.ensure_work_package_not_listed! work_package end end diff --git a/spec/features/work_packages/new/new_work_package_spec.rb b/spec/features/work_packages/new/new_work_package_spec.rb index e97fd8176f5..a13d42ce4a3 100644 --- a/spec/features/work_packages/new/new_work_package_spec.rb +++ b/spec/features/work_packages/new/new_work_package_spec.rb @@ -421,20 +421,18 @@ RSpec.describe "new work package", :js do let(:user) { create(:user, member_with_roles: { project => role }) } let(:wp_page) { Pages::Page.new } - let(:paths) do - [ - new_work_packages_path, - new_split_work_packages_path, - new_project_work_packages_path(project), - new_split_project_work_packages_path(project) - ] - end - it "shows a 403 error on creation paths" do - paths.each do |path| - visit path - wp_page.expect_toast(type: :error, message: I18n.t("api_v3.errors.code_403")) - end + visit new_work_package_path + wp_page.expect_flash(type: :error, message: I18n.t(:notice_not_authorized)) + + visit new_project_work_packages_path(project) + wp_page.expect_flash(type: :error, message: I18n.t(:notice_not_authorized)) + + visit new_split_work_packages_path + wp_page.expect_toast(type: :error, message: I18n.t("api_v3.errors.code_403")) + + visit new_split_project_work_packages_path(project) + wp_page.expect_toast(type: :error, message: I18n.t("api_v3.errors.code_403")) end end @@ -470,7 +468,7 @@ RSpec.describe "new work package", :js do let(:paths) do [ - new_work_packages_path, + new_work_package_path, new_split_work_packages_path, new_project_work_packages_path(project), new_split_project_work_packages_path(project) @@ -523,21 +521,5 @@ RSpec.describe "new work package", :js do expect(split_create_page).to have_test_selector("op-wp-breadcrumb", text: "Parent:\n#{parent.subject}") end - - it "can navigate to the fullscreen page (Regression #49565)" do - work_packages_page.visit_index - - context_menu.open_for(parent) - context_menu.choose("Create new child") - - subject_field = split_create_page.edit_field(:subject) - subject_field.set_value "My subtask" - - find(".work-packages-show-view-button").click - - expect(split_create_page).not_to have_alert_dialog - subject_field = wp_page_create.edit_field(:subject) - subject_field.expect_value "My subtask" - end end end diff --git a/spec/features/work_packages/switching_to_project_from_work_package_spec.rb b/spec/features/work_packages/switching_to_project_from_work_package_spec.rb index 36d7817e3cd..238709ee5ba 100644 --- a/spec/features/work_packages/switching_to_project_from_work_package_spec.rb +++ b/spec/features/work_packages/switching_to_project_from_work_package_spec.rb @@ -6,29 +6,49 @@ RSpec.describe "Switching to project from work package", :js do let(:user) { create(:admin) } let(:project) { create(:project) } + let(:another_project) { create(:project, parent: project) } let(:work_package) { create(:work_package, project:) } + let(:work_package_from_another_project) { create(:work_package, project: another_project) } let(:wp_table) { Pages::WorkPackagesTable.new } + let(:wp_project_table) { Pages::WorkPackagesTable.new(project) } let(:wp_page) { Pages::FullWorkPackage.new(work_package, project) } before do login_as(user) work_package + work_package_from_another_project end - it "allows to switch to the project the work package belongs to" do + it "does switch to the project context when being on the global WP table" do wp_table.visit! wp_table.expect_work_package_listed work_package + wp_table.expect_work_package_listed work_package_from_another_project # Open WP in global selection wp_table.open_full_screen_by_link work_package + # We are already in the project context and thus there is no project info box + expect(page).to have_no_css(".attributes-group.-project-context") + wp_page.expect_current_path + end + + it "allows to switch to the project the work package belongs to" do + wp_project_table.visit! + + # expect all WP to be visible + wp_project_table.expect_work_package_listed work_package + wp_project_table.expect_work_package_listed work_package_from_another_project + + wp_project_table.open_full_screen_by_link work_package_from_another_project + # Follow link to project expect(page).to have_css(".attributes-group.-project-context") link = find(".attributes-group.-project-context .project-context--switch-link") - expect(link[:href]).to include(project_path(project.id)) + expect(link[:href]).to include(project_path(another_project.id)) link.click - expect(page).to have_current_path project_path(project.id) + wait_for_network_idle + expect(page).to have_current_path project_path(another_project.id) end end diff --git a/spec/features/work_packages/zen_mode_spec.rb b/spec/features/work_packages/zen_mode_spec.rb index 01d52c44668..e6c3507487b 100644 --- a/spec/features/work_packages/zen_mode_spec.rb +++ b/spec/features/work_packages/zen_mode_spec.rb @@ -40,8 +40,6 @@ RSpec.describe "Zen mode", :js do wp_page.expect_no_zen_mode wp_page.page.find_by_id("work-packages-zen-mode-toggle-button").click wp_page.expect_zen_mode - wp_page.go_back - wp_page.expect_zen_mode wp_page.page.find_by_id("work-packages-zen-mode-toggle-button").click wp_page.expect_no_zen_mode end diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb index 08be4d08565..6c3b0d43746 100644 --- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb @@ -1169,7 +1169,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter do describe "move" do it_behaves_like "has a titled action link" do let(:link) { "move" } - let(:href) { work_package_path(work_package, "move/new") } + let(:href) { "/work_packages/#{work_package.id}/move/new" } let(:permission) { :move_work_packages } let(:title) { "Move work package '#{work_package.subject}'" } end diff --git a/spec/routing/work_packages_spec.rb b/spec/routing/work_packages_spec.rb index d794729ce03..62452d074da 100644 --- a/spec/routing/work_packages_spec.rb +++ b/spec/routing/work_packages_spec.rb @@ -42,33 +42,32 @@ RSpec.describe WorkPackagesController do action: "index") end - it "connects GET /work_packages/new to work_packages#index" do + it "connects GET /work_packages/new to work_packages#new" do expect(get("/work_packages/new")) .to route_to(controller: "work_packages", - action: "index", - state: "new") + action: "new") end - it "connects GET /projects/:project_id/work_packages/new to work_packages#index" do + it "connects GET /projects/:project_id/work_packages/new to work_packages#new" do expect(get("/projects/1/work_packages/new")) .to route_to(controller: "work_packages", - action: "index", - project_id: "1", - state: "new") + action: "new", + project_id: "1") end it "connects GET /work_packages/:id/overview to work_packages#show" do expect(get("/work_packages/1/overview")) .to route_to(controller: "work_packages", - action: "show", id: "1", state: "overview") + action: "show", id: "1", tab: "overview") end - it "connects GET /projects/:project_id/work_packages/:id/overview to work_packages#index" do + it "connects GET /projects/:project_id/work_packages/:id/overview to work_packages#show" do expect(get("/projects/1/work_packages/2/overview")) .to route_to(controller: "work_packages", - action: "index", + action: "show", project_id: "1", - state: "2/overview") + id: "2", + tab: "overview") end it "connects GET /work_packages/details/:state to work_packages#index" do diff --git a/spec/support/components/work_packages/context_menu.rb b/spec/support/components/work_packages/context_menu.rb index 4d815f062e4..0b5d1316d4d 100644 --- a/spec/support/components/work_packages/context_menu.rb +++ b/spec/support/components/work_packages/context_menu.rb @@ -78,7 +78,6 @@ module Components within_modal(I18n.t("js.modals.destroy_work_package.title", label: "work package")) do click_button "Delete" end - expect_and_dismiss_toaster end def expect_no_options(*options) diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb index a52db0d5348..44235e4e342 100644 --- a/spec/support/components/work_packages/relations.rb +++ b/spec/support/components/work_packages/relations.rb @@ -78,12 +78,12 @@ module Components def find_row(relatable) actual_relatable = find_relatable(relatable) - page.find_test_selector("op-relation-row-visible-#{actual_relatable.id}", wait: 5) + page.find_test_selector("op-relation-row-visible-#{actual_relatable.id}", wait: 10) end def find_ghost_row(relatable) actual_relatable = find_relatable(relatable) - page.find_test_selector("op-relation-row-ghost-#{actual_relatable.id}", wait: 5) + page.find_test_selector("op-relation-row-ghost-#{actual_relatable.id}", wait: 10) end def find_some_row(text:) diff --git a/spec/support/onboarding_helper.rb b/spec/support/onboarding_helper.rb index 164972e22f2..71fcd5e220a 100644 --- a/spec/support/onboarding_helper.rb +++ b/spec/support/onboarding_helper.rb @@ -33,17 +33,15 @@ require "spec_helper" module OnboardingHelper def step_through_onboarding_wp_tour(project, wp) expect(page).to have_no_css(".op-loading-indicator") + expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.create_button")), normalize_ws: true, wait: 10 + + next_button.click expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.list")), normalize_ws: true next_button.click expect(page).to have_current_path project_work_package_path(project, wp.id, "activity") - expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.full_view")), normalize_ws: true - - next_button.click - expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.back_button")), normalize_ws: true - - next_button.click - expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.create_button")), normalize_ws: true + expect(page).to have_no_css(".op-loading-indicator") + expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.full_view")), normalize_ws: true, wait: 10 next_button.click expect(page).to have_text sanitize_string(I18n.t("js.onboarding.steps.wp.gantt_menu")), normalize_ws: true diff --git a/spec/support/pages/work_packages/abstract_work_package.rb b/spec/support/pages/work_packages/abstract_work_package.rb index 739fbcbd459..cd2b78cb71c 100644 --- a/spec/support/pages/work_packages/abstract_work_package.rb +++ b/spec/support/pages/work_packages/abstract_work_package.rb @@ -319,8 +319,13 @@ module Pages find(".inline-edit--container.subject input") end - def go_back - find(".work-packages-back-button").click + def go_back(wait: true) + page.go_back + + if wait + wait_for_network_idle + ensure_page_loaded + end end def mark_notifications_as_read diff --git a/spec/support/pages/work_packages/full_work_package.rb b/spec/support/pages/work_packages/full_work_package.rb index f006316b5cc..52d04bfbbaa 100644 --- a/spec/support/pages/work_packages/full_work_package.rb +++ b/spec/support/pages/work_packages/full_work_package.rb @@ -114,7 +114,7 @@ module Pages if project project_work_package_path(project, work_package.id, tab) else - work_package_path(work_package.id, tab) + project_work_package_path(work_package.project.identifier, work_package.id, tab) end end diff --git a/spec/support/pages/work_packages/split_work_package.rb b/spec/support/pages/work_packages/split_work_package.rb index 5e084ca7eb0..ba2f12e8df0 100644 --- a/spec/support/pages/work_packages/split_work_package.rb +++ b/spec/support/pages/work_packages/split_work_package.rb @@ -59,6 +59,10 @@ module Pages find(".work-packages--details-close-icon").click end + def go_back + find(".work-packages-back-button").click + end + def container find(@selector) end diff --git a/spec/support/pages/work_packages/work_package_card.rb b/spec/support/pages/work_packages/work_package_card.rb index f5c64c00e17..0bcb1bd9f28 100644 --- a/spec/support/pages/work_packages/work_package_card.rb +++ b/spec/support/pages/work_packages/work_package_card.rb @@ -40,7 +40,7 @@ module Pages end def card_element - page.find(card_selector) + page.find(card_selector, wait: 10) end def card_selector