mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[67007] Render the WP full view from rails (#20109)
* Create a FullView::CopyComponent for WorkPackages which is routed from rails * Remove angular splitCopy route and component as it was overwritten by the angular fullCopy route for quite some time already and nobody complained. So we decided to remove the splitCopy completely * Create FullView::CreateComponent for WorkPackages which is now routed from rails instead of Angular * First draft of implementing the FullView route for WorkPackages from rails * Pass correct tab from the URL to the FullView::ShowComponent * Do a hard reload to "create" route when we are not routed from Angular * Adapt routing spec to new WorkPackage routes and to some fine-tuning with the WP routes * Show correct tab in WP Full view and change URL when clicking a tab entry * Adapt to new rails based routing * Fix some routes and redirects * Make sure, the split screen stil renders correctly * Remove back button from WP full view * Fix routing issues * Start fixing specs * Attempt to override the browser history to be able to use browser back * Use helper function to build new WP url string * Adapt spec that now partially renders backend toasts * Remove ability to move to fullscreen Theoretically, we can re-add it by posting to some form endpoint, but not worth it for the first iteration * Disable cache-control on angular routed pages, so back links work * Fix double click to fullscreen * Adapt navigation and title setting * Let WP breadcrumb to a hard reload instead of Angular transition * Redirect when the WP route is incomplete (this is the attempt to re-implement an angular functionality) * Navigate with Turbo when double clicking a card * Adapt onboarding tour to new hard reload when switching to WP full view * Fix some specs * Fix more tests * Hide Overview tab on FullView * Correct check for incomplete routes * Do a hard refresh when coming from slpit screen to full view * Fix notification navigation * Adapt attachment spec as the tab switch cannot be done anymore while dragging * Fix more tests * Please rubocop and fix more tests * Attempt to fix navigation_spec * Add debian_base for pullpreview --------- Co-authored-by: Oliver Günther <mail@oliverguenther.de>
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
%>
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
<%= helpers.angular_component_tag "opce-wp-full-create",
|
||||
inputs: {
|
||||
type: @type,
|
||||
projectIdentifier: @project.present? ? @project.identifier : nil,
|
||||
routedFromAngular: false,
|
||||
}
|
||||
%>
|
||||
@@ -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
|
||||
@@ -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
|
||||
%>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,6 +31,8 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% html_title(*local_assigns[:page_title]) if local_assigns[:page_title].present? %>
|
||||
<!-- plug-in specific tags -->
|
||||
<%= call_hook :view_work_package_overview_attributes %>
|
||||
<!-- disable Turbo cache for Angular pages -->
|
||||
<meta name="turbo-cache-control" content="no-cache">
|
||||
<% end -%>
|
||||
|
||||
<%= content_for :content_body do %>
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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 %>
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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,
|
||||
|
||||
+13
-10
@@ -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
|
||||
|
||||
@@ -13,6 +13,8 @@ volumes:
|
||||
x-defaults: &defaults
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
DEBIAN_BASE: bookworm
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.pullpreview
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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<HTMLElement>, filterFunction:(match:HTMLElement) => boolean):HTMLElement|null {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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`;
|
||||
},
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ export class IFCViewerPageComponent
|
||||
component: WorkPackageCreateButtonComponent,
|
||||
inputs: {
|
||||
stateName$: of(this.newRoute),
|
||||
allowed: ['work_packages.createWorkPackage', 'work_package.copy'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -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;
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -176,10 +176,8 @@ export class WorkPackageBaseResource extends HalResource {
|
||||
* Return "<type name>: <subject> (#<id>)" 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 {
|
||||
|
||||
+2
-2
@@ -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)"
|
||||
>
|
||||
</a>
|
||||
@if (project) {
|
||||
|
||||
+13
-3
@@ -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
|
||||
|
||||
-3
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-4
@@ -10,7 +10,3 @@
|
||||
&--icon
|
||||
font-size: 1rem
|
||||
line-height: 22px
|
||||
|
||||
&_mobile-limited-width
|
||||
@media only screen and (max-width: $breakpoint-sm)
|
||||
width: 64px
|
||||
|
||||
+5
-9
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-1
@@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
@if (parent) {
|
||||
<a
|
||||
[attr.title]="parent.name"
|
||||
uiSref="work-packages.show"
|
||||
[uiParams]="{ workPackageId: parent.id }"
|
||||
(click)="switchToFullscreenForWp(parent)"
|
||||
class="op-wp-breadcrumb-parent nocut"
|
||||
data-test-selector="op-wp-breadcrumb-parent">
|
||||
<span [textContent]="parent.name"></span>
|
||||
|
||||
+2
-2
@@ -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<string>;
|
||||
|
||||
@Input() routedFromAngular:boolean = true;
|
||||
|
||||
allowed:boolean;
|
||||
|
||||
disabled:boolean;
|
||||
|
||||
+1
@@ -8,6 +8,7 @@
|
||||
[stateName]="stateName$ | async"
|
||||
[dropdownActive]="allowed"
|
||||
[title]="text.title"
|
||||
[routedFromAngular]="routedFromAngular"
|
||||
>
|
||||
<svg plus-icon size="small"/>
|
||||
<span class="button--text"
|
||||
|
||||
@@ -63,6 +63,17 @@ export class WorkPackageCopyController extends WorkPackageCreateComponent {
|
||||
});
|
||||
}
|
||||
|
||||
public cancelAndBack() {
|
||||
this.wpCreate.cancelCreation();
|
||||
|
||||
if (this.routedFromAngular) {
|
||||
this.$state.go(this.cancelState, this.$state.params);
|
||||
} else {
|
||||
const link = this.pathHelper.genericWorkPackagePath(this.currentProjectService.id, this.copiedWorkPackageId);
|
||||
Turbo.visit(link + window.location.search, { action: 'advance' });
|
||||
}
|
||||
}
|
||||
|
||||
protected createdWorkPackage() {
|
||||
this.copiedWorkPackageId = this.stateParams.copiedFromWorkPackageId;
|
||||
return new Promise<WorkPackageChangeset>((resolve, reject) => {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<button class="button dropdown-relative"
|
||||
[attr.accesskey]="7"
|
||||
[title]="text.button_more"
|
||||
data-test-selector="wp-details-toolbar--show-more-button"
|
||||
wpSingleContextMenu
|
||||
[wpSingleContextMenu-workPackage]="workPackage">
|
||||
<svg
|
||||
|
||||
+22
-13
@@ -1,12 +1,19 @@
|
||||
import { StateService } from '@uirouter/core';
|
||||
import { KeepTabService } from 'core-app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service';
|
||||
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator';
|
||||
|
||||
export const uiStateLinkClass = '__ui-state-link';
|
||||
export const checkedClassName = '-checked';
|
||||
|
||||
export class UiStateLinkBuilder {
|
||||
constructor(public readonly $state:StateService,
|
||||
public readonly keepTab:KeepTabService) {
|
||||
constructor(
|
||||
public readonly $state:StateService,
|
||||
public readonly keepTab:KeepTabService,
|
||||
public readonly currentProject:CurrentProjectService,
|
||||
public readonly pathHelper:PathHelperService,
|
||||
) {
|
||||
}
|
||||
|
||||
public linkToDetails(workPackageId:string, title:string, content:string) {
|
||||
@@ -21,21 +28,23 @@ export class UiStateLinkBuilder {
|
||||
const a = document.createElement('a');
|
||||
let tabState:string;
|
||||
let tabIdentifier:string;
|
||||
let href:string;
|
||||
|
||||
if (state === 'show') {
|
||||
tabState = 'work-packages.show.tabs';
|
||||
tabIdentifier = this.keepTab.currentShowTab;
|
||||
const projectIdentifier = this.currentProject.identifier;
|
||||
href = this.pathHelper.genericWorkPackagePath(projectIdentifier, workPackageId, this.keepTab.currentShowTab) + window.location.search;
|
||||
} else {
|
||||
tabState = 'work-packages.partitioned.list.details.tabs';
|
||||
tabIdentifier = this.keepTab.currentDetailsTab;
|
||||
const tab = this.keepTab.currentDetailsTab;
|
||||
href = this.$state.href(
|
||||
'work-packages.partitioned.list.details.tabs',
|
||||
{
|
||||
workPackageId,
|
||||
tab,
|
||||
},
|
||||
)
|
||||
}
|
||||
a.href = this.$state.href(
|
||||
tabState,
|
||||
{
|
||||
workPackageId,
|
||||
tabIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
a.href = href;
|
||||
a.classList.add(uiStateLinkClass);
|
||||
a.dataset.workPackageId = workPackageId;
|
||||
a.dataset.wpState = state;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
Injector,
|
||||
Input,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
@@ -64,7 +65,7 @@ import { HalSource } from 'core-app/features/hal/interfaces';
|
||||
export class WorkPackageCreateComponent extends UntilDestroyedMixin implements OnInit {
|
||||
public successState:string = splitViewRoute(this.$state);
|
||||
|
||||
public cancelState:string = this.$state.current.data.baseRoute;
|
||||
public cancelState:string = this.$state?.current?.data?.baseRoute;
|
||||
|
||||
public newWorkPackage:WorkPackageResource;
|
||||
|
||||
@@ -75,12 +76,14 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O
|
||||
/** Are we in the copying substates ? */
|
||||
public copying = false;
|
||||
|
||||
public stateParams = this.$transition.params('to');
|
||||
@Input() public stateParams:any;
|
||||
|
||||
public text = {
|
||||
button_settings: this.I18n.t('js.button_settings'),
|
||||
};
|
||||
|
||||
@Input() public routedFromAngular:boolean = true;
|
||||
|
||||
@ViewChild(EditFormComponent, { static: false }) protected editForm:EditFormComponent;
|
||||
|
||||
/** Explicitly remember destroy state in this abstract base */
|
||||
@@ -88,7 +91,6 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O
|
||||
|
||||
constructor(
|
||||
public readonly injector:Injector,
|
||||
protected readonly $transition:Transition,
|
||||
protected readonly $state:StateService,
|
||||
protected readonly I18n:I18nService,
|
||||
protected readonly titleService:OpTitleService,
|
||||
@@ -106,6 +108,12 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
// In case the create form is still routed via Angular, the stateParams are empty. We then read the params from the Transition
|
||||
if (this.routedFromAngular) {
|
||||
const transition = this.injector.get<Transition>(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<HalResource>('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() {
|
||||
|
||||
+1
-1
@@ -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 = [];
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</ul>
|
||||
</div>
|
||||
<wp-single-view [workPackage]="newWorkPackage"
|
||||
[showProject]="copying" />
|
||||
<wp-edit-actions-bar (onCancel)="cancelAndBackToList()" />
|
||||
[showProject]="copying" />
|
||||
<wp-edit-actions-bar (onCancel)="cancelAndBack()" />
|
||||
</edit-form>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -9,18 +9,12 @@
|
||||
<div class="work-packages--details-content -create-mode">
|
||||
<div class="work-packages--new-details-header">
|
||||
<wp-type-status [workPackage]="newWorkPackage" />
|
||||
<div class="wp--details--switch-fullscreen-wrapper">
|
||||
<a (click)="switchToFullscreen()"
|
||||
class="work-packages-show-view-button wp--details--switch-fullscreen ">
|
||||
<span class="icon-context icon-to-fullscreen"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<wp-single-view [workPackage]="newWorkPackage"
|
||||
[showProject]="copying" />
|
||||
</div>
|
||||
<div class="work-packages--details-toolbar-container">
|
||||
<wp-edit-actions-bar (onCancel)="cancelAndBackToList()" />
|
||||
<wp-edit-actions-bar (onCancel)="cancelAndBack()"/>
|
||||
</div>
|
||||
<div class="work-packages--details--resizer hidden-for-mobile hide-when-print">
|
||||
<wp-resizer [elementClass]="'work-packages-partitioned-page--content-right'"
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ export class WorkPackageActivityTabComponent extends ActivityPanelBaseController
|
||||
ngOnInit() {
|
||||
const { workPackageId } = this.uiRouterGlobals.params as unknown as { workPackageId:string };
|
||||
this.workPackageId = (this.workPackage.id as string) || workPackageId;
|
||||
|
||||
super.ngOnInit();
|
||||
if (window.location.hash) {
|
||||
this.activitiesTabContentElement.nativeElement.scrollIntoView();
|
||||
|
||||
+5
-1
@@ -27,6 +27,8 @@
|
||||
//++
|
||||
|
||||
import { KeepTabService } from './keep-tab.service';
|
||||
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
|
||||
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
|
||||
|
||||
describe('keepTab service', () => {
|
||||
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',
|
||||
|
||||
+15
-12
@@ -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<string, unknown> = {}):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<string, unknown> = {}):void {
|
||||
|
||||
+5
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-20
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -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() {
|
||||
|
||||
+4
-5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -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'),
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ export interface TabComponent extends Component {
|
||||
|
||||
export interface WpTabDefinition extends TabDefinition {
|
||||
component:Type<TabComponent>;
|
||||
displayable?:(workPackage:WorkPackageResource, $state:StateService) => boolean;
|
||||
displayable?:(workPackage:WorkPackageResource, $state:StateService|null) => boolean;
|
||||
count?:(workPackage:WorkPackageResource, injector:Injector) => Observable<number>;
|
||||
showCountAsBubble?:boolean;
|
||||
}
|
||||
|
||||
+16
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
<op-scrollable-tabs
|
||||
[tabs]="tabs"
|
||||
[currentTabId]="currentTabId"
|
||||
class="op-work-package-tabs"
|
||||
>
|
||||
@if (view === 'split') {
|
||||
|
||||
+4
@@ -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]
|
||||
|
||||
+26
-11
@@ -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 {
|
||||
|
||||
+3
-3
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
-5
@@ -5,11 +5,6 @@
|
||||
/>
|
||||
<div class="toolbar-container -editable">
|
||||
<div class="toolbar">
|
||||
@if (backButtonCallback !== undefined) {
|
||||
<op-back-button class="op-back-button"
|
||||
linkClass="op-back-button_mobile-limited-width"
|
||||
[customBackMethod]="backButtonCallback" />
|
||||
}
|
||||
<h2 class="toolbar-title--container">
|
||||
<editable-toolbar-title [title]="selectedTitle"
|
||||
[inFlight]="toolbarDisabled"
|
||||
|
||||
+2
-5
@@ -60,14 +60,11 @@
|
||||
grid-template-rows: auto auto
|
||||
justify-items: start
|
||||
|
||||
.op-back-button
|
||||
grid-column: 1 / 3
|
||||
grid-row: 1 / 2
|
||||
.toolbar-title--container
|
||||
grid-column: 1 / 3
|
||||
grid-row: 2 / 3
|
||||
grid-row: 1 / 2
|
||||
overflow: hidden
|
||||
.toolbar-items
|
||||
grid-column: 1 / 3
|
||||
grid-row: 3 / 4
|
||||
grid-row: 2 / 3
|
||||
justify-self: end
|
||||
|
||||
-3
@@ -128,9 +128,6 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp
|
||||
/** We need to pass the correct partition state to the view to manage the grid */
|
||||
currentPartition:ViewPartitionState = '-split';
|
||||
|
||||
/** What route (if any) should we go back to using the back button left of the title? */
|
||||
backButtonCallback:() => void|undefined;
|
||||
|
||||
/** Which filter container component to mount */
|
||||
filterContainerDefinition:DynamicComponentDefinition = {
|
||||
component: WorkPackageFilterContainerComponent,
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+62
@@ -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: `
|
||||
<wp-copy-full-view
|
||||
[stateParams]="{ type: type, parent_id: parentId, projectPath: projectIdentifier, copiedFromWorkPackageId: copiedFromWorkPackageId }"
|
||||
[routedFromAngular]="routedFromAngular"
|
||||
></wp-copy-full-view>
|
||||
`,
|
||||
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');
|
||||
}
|
||||
}
|
||||
+61
@@ -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: `
|
||||
<wp-new-full-view
|
||||
[stateParams]="{ type: type, parent_id: parentId, projectPath: projectIdentifier }"
|
||||
[routedFromAngular]="routedFromAngular"
|
||||
></wp-new-full-view>
|
||||
`,
|
||||
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');
|
||||
}
|
||||
}
|
||||
+28
-6
@@ -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: `
|
||||
<op-wp-full-view
|
||||
[workPackageId]="workPackageId"
|
||||
[activeTab]="activeTab"
|
||||
[routedFromAngular]="routedFromAngular"
|
||||
></op-wp-full-view>
|
||||
`,
|
||||
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');
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -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
|
||||
|
||||
@@ -7,16 +7,12 @@
|
||||
<wp-breadcrumb class="wp-show--header-container--breadcrumb"
|
||||
[workPackage]="workPackage"
|
||||
/>
|
||||
<op-back-button class="wp-show--header-container--back-button"
|
||||
linkClass="work-packages-back-button"
|
||||
/>
|
||||
<div class="subject-header wp-show--header-container--subject">
|
||||
<wp-subject />
|
||||
<wp-subject [workPackage]="workPackage" />
|
||||
</div>
|
||||
<ul id="toolbar-items" class="toolbar-items hide-when-print wp-show--header-container--toolbar-items">
|
||||
<li class="toolbar-item hidden-for-tablet">
|
||||
<wp-create-button [allowed]="['work_package.addChild', 'work_package.copy']"
|
||||
[stateName$]="stateName$" />
|
||||
<wp-create-button [routedFromAngular]="routedFromAngular" />
|
||||
</li>
|
||||
@if (displayShareButton$ | async) {
|
||||
<li class="toolbar-item"
|
||||
@@ -70,10 +66,16 @@
|
||||
<div class="work-packages-full-view--split-right work-packages-tab-view--overflow">
|
||||
<div class="work-packages--panel-inner">
|
||||
<span class="sr-only" tabindex="-1" [textContent]="focusAnchorLabel"></span>
|
||||
<op-wp-tabs [workPackage]="workPackage" [view]="'full'" />
|
||||
<op-wp-tabs [workPackage]="workPackage"
|
||||
[currentTabId]="activeTab"
|
||||
[view]="'full'"
|
||||
[routedFromAngular]="routedFromAngular"/>
|
||||
<div class="tabcontent"
|
||||
data-notification-selector='notification-scroll-container'
|
||||
ui-view>
|
||||
data-notification-selector='notification-scroll-container'>
|
||||
<op-wp-tab
|
||||
[workPackageId]="workPackage.id!"
|
||||
[tabIdentifier]="activeTab"
|
||||
></op-wp-tab>
|
||||
</div>
|
||||
</div>
|
||||
<div class="work-packages-full-view--resizer hidden-for-mobile hide-when-print">
|
||||
|
||||
+22
-25
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -55,5 +55,7 @@ export class WorkPackageSplitViewEntryComponent {
|
||||
|
||||
constructor(readonly elementRef:ElementRef) {
|
||||
populateInputsFromDataset(this);
|
||||
|
||||
document.body.classList.add('router--work-packages-partitioned-split-view-details');
|
||||
}
|
||||
}
|
||||
|
||||
+2
-13
@@ -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);
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
data-notification-selector='notification-scroll-container'
|
||||
>
|
||||
<ndc-dynamic [ndcDynamicComponent]="activeTabComponent"
|
||||
[ndcDynamicInputs]="{ workPackage: workPackage }" />
|
||||
[ndcDynamicInputs]="{ workPackage: workPackage, tab: tabIdentifier }" />
|
||||
</div>
|
||||
}
|
||||
</edit-form>
|
||||
|
||||
+24
-6
@@ -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);
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ export class WorkPackageViewPageComponent extends PartitionedQuerySpacePageCompo
|
||||
component: WorkPackageCreateButtonComponent,
|
||||
inputs: {
|
||||
stateName$: of(this.stateName),
|
||||
allowed: ['work_packages.createWorkPackage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -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() {
|
||||
|
||||
+7
-1
@@ -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()) {
|
||||
|
||||
+7
-1
@@ -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) {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
+26
-19
@@ -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;
|
||||
},
|
||||
}));
|
||||
|
||||
+3
-1
@@ -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] });
|
||||
|
||||
+13
-11
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
+13
-1
@@ -28,8 +28,20 @@
|
||||
[attr.aria-selected]="tab.id === currentTabId"
|
||||
(click)="clickTab(tab, $event)"
|
||||
[attr.title]="tabTitle(tab)"
|
||||
[textContent]="tab.name"
|
||||
>
|
||||
<span [textContent]="tab.name"></span>
|
||||
@if (counter(tab) | async; as tabCounter) {
|
||||
@if (tab.showCountAsBubble) {
|
||||
<op-tab-count [count]="tabCounter"
|
||||
[attr.data-test-selector]="'tab-counter-' + tab.name"
|
||||
/>
|
||||
}
|
||||
@if (tabCounter > 0 && !tab.showCountAsBubble) {
|
||||
<span
|
||||
data-test-selector="tab-count"
|
||||
> ({{ tabCounter }})</span>
|
||||
}
|
||||
}
|
||||
</a>
|
||||
}
|
||||
@if (tab.route) {
|
||||
|
||||
+5
-4
@@ -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' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<string, string>,
|
||||
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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user