diff --git a/app/components/_index.sass b/app/components/_index.sass index a14f1dce5db6..b54524317d56 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -10,7 +10,7 @@ @import "work_packages/details/tab_component" @import "work_packages/progress/modal_body_component" @import "work_packages/hover_card_component" -@import "work_packages/split_view_component" +@import "work_packages/split_view/show_component" @import "open_project/common/attribute_component" @import "open_project/common/submenu_component" @import "filter/filters_component" diff --git a/app/components/filter/filter_component.html.erb b/app/components/filter/filter_component.html.erb index 007059539753..6198c523959a 100644 --- a/app/components/filter/filter_component.html.erb +++ b/app/components/filter/filter_component.html.erb @@ -15,7 +15,6 @@ <% each_filter do |filter, filter_active, additional_options| %> <% filter_boolean = filter.is_a?(Queries::Filters::Shared::BooleanFilter) %> <% autocomplete_filter = additional_options.key?(:autocomplete_options) %> -
  • <% end %> -
  • -
  • -
    <%= select_tag 'add_filter_select', options_from_collection_for_select( @@ -109,9 +105,11 @@ } %>
  • + <% if show_apply? %>
  • <%= submit_tag t('button_apply'), class: 'button -small -primary', name: nil %>
  • + <% end %> <% end %> diff --git a/app/components/filter/filter_component.rb b/app/components/filter/filter_component.rb index 6b625627b2d3..522fd34ffef0 100644 --- a/app/components/filter/filter_component.rb +++ b/app/components/filter/filter_component.rb @@ -38,6 +38,10 @@ def show_filters_section? always_visible || params[:filters].present? end + def show_apply? + true + end + # Returns filters, active and inactive. # In case a filter is active, the active one will be preferred over the inactive one. def each_filter diff --git a/app/components/projects/projects_filters_component.html.erb b/app/components/projects/projects_filters_component.html.erb deleted file mode 100644 index 96cba9e261dd..000000000000 --- a/app/components/projects/projects_filters_component.html.erb +++ /dev/null @@ -1,109 +0,0 @@ -<%= form_tag({}, - method: :get, - class: "op-filters-form op-filters-form_top-margin #{show_filters_section? ? "-expanded" : ""}", - data: { - 'filter--filters-form-target': 'filterForm', - action: 'submit->filter--filters-form#sendForm:prevent' - }) do %> - <% operators_without_values = %w[* !* t w] %> -
    - - <%= t(:label_filter_plural) %> - -
    -<% end %> diff --git a/app/components/projects/projects_filters_component.rb b/app/components/projects/projects_filters_component.rb index 0dbde255ca60..79106628b591 100644 --- a/app/components/projects/projects_filters_component.rb +++ b/app/components/projects/projects_filters_component.rb @@ -41,6 +41,10 @@ def turbo_requests? true end + def show_apply? + false + end + private def allowed_filter?(filter) diff --git a/app/components/work_packages/baseline/modal_dialog_component.html.erb b/app/components/work_packages/baseline/modal_dialog_component.html.erb new file mode 100644 index 000000000000..d0bae082340a --- /dev/null +++ b/app/components/work_packages/baseline/modal_dialog_component.html.erb @@ -0,0 +1,23 @@ +<%= render(Primer::Alpha::Dialog.new( + title: I18n.t("js.baseline.baseline_comparison"), + subtitle: subtitle_text, + size: :medium_portrait, + id: MODAL_ID +)) do |dialog| %> + <% dialog.with_header(variant: :large) %> + <% dialog.with_body do %> + <%= helpers.angular_component_tag "opce-baseline", + inputs: { + showHeaderText: false, + } %> + <% end %> + <% dialog.with_footer do %> + <%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %> + <%= render(Primer::ButtonComponent.new( + data: { + "close-dialog-id": MODAL_ID, + }, + scheme: :primary, + type: :submit)) { I18n.t(:button_apply) } %> + <% end %> +<% end %> diff --git a/app/components/work_packages/baseline/modal_dialog_component.rb b/app/components/work_packages/baseline/modal_dialog_component.rb new file mode 100644 index 000000000000..cda496cc4f20 --- /dev/null +++ b/app/components/work_packages/baseline/modal_dialog_component.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +module WorkPackages + module Baseline + class ModalDialogComponent < ApplicationComponent + MODAL_ID = "op-work-packages-baseline-dialog" + EXPORT_FORM_ID = "op-work-packages-baseline-dialog-form" + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + attr_reader :query, :project, :query_params + + def initialize(query:, project:, title:) + super + + @query = query + @project = project + @query_params = ::API::V3::Queries::QueryParamsRepresenter.new(query).to_url_query(merge_params: { columns: [], title: }) + end + + def subtitle_text + # TODO: show different text and EE icon if no EE licence + I18n.t("js.baseline.header_description") + end + end + end +end diff --git a/app/components/work_packages/configure_view/modal_dialog_component.html.erb b/app/components/work_packages/configure_view/modal_dialog_component.html.erb new file mode 100644 index 000000000000..7adf098a4c2d --- /dev/null +++ b/app/components/work_packages/configure_view/modal_dialog_component.html.erb @@ -0,0 +1,33 @@ +<%= render(Primer::Alpha::Dialog.new( + title: I18n.t("js.work_packages.table_configuration.modal_title"), + size: :xlarge, + id: MODAL_ID +)) do |dialog| %> + <% dialog.with_header(variant: :large) %> + <% dialog.with_body do %> + <%= render(Primer::Alpha::UnderlinePanels.new(label: "Test navigation")) do |component| + tabs.each do |tab| + component.with_tab(selected: tab[:id] == "columns", id: "tab-#{tab[:id]}") do |tab_content| + tab_content.with_text { "#{tab[:name]}" } + tab_content.with_panel do + if tab[:component].present? + helpers.angular_component_tag tab[:component], inputs: tab[:inputs] + else + "Panel #{tab[:name]}" + end + end + end + end + end %> + + <% end %> + <% dialog.with_footer do %> + <%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %> + <%= render(Primer::ButtonComponent.new( + data: { + "close-dialog-id": MODAL_ID, + }, + scheme: :primary, + type: :submit)) { I18n.t(:button_apply) } %> + <% end %> +<% end %> diff --git a/app/components/work_packages/configure_view/modal_dialog_component.rb b/app/components/work_packages/configure_view/modal_dialog_component.rb new file mode 100644 index 000000000000..8822ca9f7818 --- /dev/null +++ b/app/components/work_packages/configure_view/modal_dialog_component.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +module WorkPackages + module ConfigureView + class ModalDialogComponent < ApplicationComponent + MODAL_ID = "op-work-packages-configure-view-dialog" + EXPORT_FORM_ID = "op-work-packages-configure-view-dialog-form" + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + attr_reader :query, :project, :query_params + + def initialize(query:, project:) + super + + @query = query + @project = project + @query_params = ::API::V3::Queries::QueryParamsRepresenter.new(query).to_url_query(merge_params: { columns: [], title: }) + end + + def tabs + [ + { + id: "columns", + name: I18n.t("js.label_columns"), + component: "opce-wp-table-configuration-tab", + inputs: { + tabId: "columns" + } + }, + { + id: "filters", + name: I18n.t("js.work_packages.query.filters"), + component: "opce-wp-table-configuration-tab", + inputs: { + tabId: "filters" + } + }, + { + id: "sort-by", + name: I18n.t("js.label_sort_by"), + component: "opce-wp-table-configuration-tab", + inputs: { + tabId: "sort-by" + } + }, + { + id: "baseline", + name: I18n.t("js.baseline.toggle_title"), + component: "opce-wp-table-configuration-tab", + inputs: { + tabId: "baseline" + } + }, + { + id: "display-settings", + name: I18n.t("js.work_packages.table_configuration.display_settings"), + component: "opce-wp-table-configuration-tab", + inputs: { + tabId: "display-settings" + } + }, + { + id: "highlighting", + name: I18n.t("js.work_packages.table_configuration.highlighting"), + component: "opce-wp-table-configuration-tab", + inputs: { + tabId: "highlighting" + } + } + ] + end + end + end +end diff --git a/app/components/work_packages/create/wp_create_button_component.html.erb b/app/components/work_packages/create/wp_create_button_component.html.erb new file mode 100644 index 000000000000..83a4ce71c3fe --- /dev/null +++ b/app/components/work_packages/create/wp_create_button_component.html.erb @@ -0,0 +1,22 @@ +<% if can_create_work_packages? %> + <%= + render Primer::Alpha::ActionMenu.new do |menu| + menu.with_show_button(scheme: :primary, + aria: { + label: I18n.t(:label_work_package_new) + }) do |button| + button.with_trailing_action_icon(icon: :"triangle-down") + button.with_leading_visual_icon(icon: :plus) + I18n.t(:button_create) + end + + items.each do |type| + menu.with_item(label: type.name, + tag: :a, + href: create_href_for_type(type), + content_arguments: { target: "_top" }, + label_arguments: { classes: "__hl_inline_type_#{type.id}" }) + end + end + %> +<% end %> diff --git a/app/components/work_packages/create/wp_create_button_component.rb b/app/components/work_packages/create/wp_create_button_component.rb new file mode 100644 index 000000000000..93e50ea3d207 --- /dev/null +++ b/app/components/work_packages/create/wp_create_button_component.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +module WorkPackages + module Create + class WpCreateButtonComponent < ApplicationComponent + include ApplicationHelper + + def initialize(project: nil) + super + @project = project + end + + def items + if @project + @project.types.all + else + Type.all + end + end + + def create_href_for_type(type) + # TODO: make configurable for other modules + if @project + split_create_project_work_packages_path(@project, type: type.id) + else + split_create_work_packages_path(type: type.id) + end + end + + def can_create_work_packages? + if @project + helpers.current_user.allowed_in_project?(:add_work_packages, @project) + else + helpers.current_user.allowed_in_any_project?(:add_work_packages) + end + end + end + end +end diff --git a/app/components/work_packages/filter_button_component.rb b/app/components/work_packages/filter_button_component.rb new file mode 100644 index 000000000000..b173fe25eb71 --- /dev/null +++ b/app/components/work_packages/filter_button_component.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# -- 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. +# ++ +module WorkPackages + class FilterButtonComponent < Filter::FilterButtonComponent + def filters_count + @filters_count ||= query.filters.count + end + end +end diff --git a/app/components/work_packages/filters_component.rb b/app/components/work_packages/filters_component.rb new file mode 100644 index 000000000000..e187513db1f2 --- /dev/null +++ b/app/components/work_packages/filters_component.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +class WorkPackages::FiltersComponent < Filter::FilterComponent + def allowed_filters + super.sort_by(&:human_name) + end + + def turbo_requests? + true + end + + def show_apply? + false + end + + protected + + def additional_filter_attributes(filter) + case filter + when Queries::WorkPackages::Filter::FilterForWpMixin + { + autocomplete_options: { + component: "opce-autocompleter", + resource: "work_packages" + } + } + else + super + end + end +end diff --git a/app/components/work_packages/full_view/copy_component.html.erb b/app/components/work_packages/full_view/copy_component.html.erb new file mode 100644 index 000000000000..faa040cce734 --- /dev/null +++ b/app/components/work_packages/full_view/copy_component.html.erb @@ -0,0 +1,11 @@ +<%= + helpers.angular_component_tag "opce-wp-full-copy", + inputs: { + type: @type, + copiedFromWorkPackageId: @copied_from_work_package_id, + projectIdentifier: @project.present? ? @project.identifier : nil, + resizerClass: helpers.container_class, + routedFromAngular: false, + } + +%> diff --git a/app/components/work_packages/full_view/copy_component.rb b/app/components/work_packages/full_view/copy_component.rb new file mode 100644 index 000000000000..e2d142a7603d --- /dev/null +++ b/app/components/work_packages/full_view/copy_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WorkPackages + module FullView + class CopyComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:, copied_from_work_package_id:, project:) + super + + @type = type + @copied_from_work_package_id = copied_from_work_package_id + @project = project + end + end + end +end diff --git a/app/components/work_packages/full_view/create_component.html.erb b/app/components/work_packages/full_view/create_component.html.erb new file mode 100644 index 000000000000..ec6842ae08fd --- /dev/null +++ b/app/components/work_packages/full_view/create_component.html.erb @@ -0,0 +1,7 @@ +<%= helpers.angular_component_tag "opce-wp-full-create", + inputs: { + type: @type, + projectIdentifier: @project.present? ? @project.identifier : nil, + routedFromAngular: false, + } +%> diff --git a/app/components/work_packages/full_view/create_component.rb b/app/components/work_packages/full_view/create_component.rb new file mode 100644 index 000000000000..7bbec152bef1 --- /dev/null +++ b/app/components/work_packages/full_view/create_component.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WorkPackages + module FullView + class CreateComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:, project:) + super + + @type = type + @project = project + end + end + end +end diff --git a/app/components/work_packages/full_view/show_component.html.erb b/app/components/work_packages/full_view/show_component.html.erb new file mode 100644 index 000000000000..319a6427c0a0 --- /dev/null +++ b/app/components/work_packages/full_view/show_component.html.erb @@ -0,0 +1,18 @@ +<%= + 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 + #render(WorkPackages::Details::TabComponent.new(work_package: @work_package, tab: @tab, base_route: @base_route)) + helpers.angular_component_tag "opce-wp-full-view", + inputs: { + workPackageId: @id, + activeTab: @tab, + routedFromAngular: false + } + end +%> diff --git a/app/components/work_packages/full_view/show_component.rb b/app/components/work_packages/full_view/show_component.rb new file mode 100644 index 000000000000..2547c4a3e68e --- /dev/null +++ b/app/components/work_packages/full_view/show_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module WorkPackages + module FullView + class ShowComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def self.wrapper_key = :"work-package-full-view" + + def initialize(id:, tab: "activity") + super + + @id = id + @tab = tab + @work_package = WorkPackage.visible.find_by(id:) + end + + def wrapper_uniq_by + @id + end + end + end +end diff --git a/app/components/work_packages/include_projects/modal_dialog_component.html.erb b/app/components/work_packages/include_projects/modal_dialog_component.html.erb new file mode 100644 index 000000000000..c73f6ebf44ed --- /dev/null +++ b/app/components/work_packages/include_projects/modal_dialog_component.html.erb @@ -0,0 +1,22 @@ +<%= render(Primer::Alpha::Dialog.new( + title: I18n.t("js.include_projects.title"), + size: :medium_portrait, + id: MODAL_ID +)) do |dialog| %> + <% dialog.with_header(variant: :large) %> + <% dialog.with_body do %> + <%= helpers.angular_component_tag "opce-include-projects", + inputs: { + showHeaderText: false, + } %> + <% end %> + <% dialog.with_footer do %> + <%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %> + <%= render(Primer::ButtonComponent.new( + data: { + "close-dialog-id": MODAL_ID, + }, + scheme: :primary, + type: :submit)) { I18n.t(:button_apply) } %> + <% end %> +<% end %> diff --git a/app/components/work_packages/include_projects/modal_dialog_component.rb b/app/components/work_packages/include_projects/modal_dialog_component.rb new file mode 100644 index 000000000000..b3b12737b39c --- /dev/null +++ b/app/components/work_packages/include_projects/modal_dialog_component.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +module WorkPackages + module IncludeProjects + class ModalDialogComponent < ApplicationComponent + MODAL_ID = "op-work-packages-include-projects-dialog" + EXPORT_FORM_ID = "op-work-packages-include-projects-dialog-form" + include OpTurbo::Streamable + include OpPrimer::ComponentHelpers + + attr_reader :query, :project, :query_params + + def initialize(query:, project:, title:) + super + + @query = query + @project = project + @query_params = ::API::V3::Queries::QueryParamsRepresenter.new(query).to_url_query(merge_params: { columns: [], title: }) + end + end + end +end diff --git a/app/components/work_packages/index_page_header_component.html.erb b/app/components/work_packages/index_page_header_component.html.erb new file mode 100644 index 000000000000..5896e8fbf2da --- /dev/null +++ b/app/components/work_packages/index_page_header_component.html.erb @@ -0,0 +1,124 @@ +<%= + render(Primer::OpenProject::PageHeader.new(state: :show)) do |header| + header.with_title do |component| + component.with_editable_form(update_path: "/todo", cancel_path: "/todo") + title + end + header.with_breadcrumbs(breadcrumb_items) + + header.with_action_button(tag: :a, + mobile_icon: :"op-include-projects", + mobile_label: I18n.t("js.include_projects.toggle_title"), + href: @project.present? ? include_projects_dialog_project_work_packages_path(@project) : include_projects_dialog_work_packages_path(), + aria: { label: I18n.t("js.include_projects.toggle_title") }, + title: I18n.t("js.include_projects.toggle_title"), + data: { controller: "async-dialog" }, + test_selector: "project-include-button", + rel: "nofollow") do |button| + button.with_leading_visual_icon(icon: :"op-include-projects") + I18n.t("js.include_projects.toggle_title") + end + + header.with_action_button(tag: :a, + mobile_icon: :"op-baseline", + mobile_label: I18n.t("js.baseline.toggle_title"), + href: @project.present? ? baseline_dialog_project_work_packages_path(@project) : baseline_dialog_work_packages_path(), + aria: { label: I18n.t("js.baseline.toggle_title") }, + title: I18n.t("js.baseline.toggle_title"), + data: { controller: "async-dialog" }, + rel: "nofollow") do |button| + button.with_leading_visual_icon(icon: :"op-baseline") + I18n.t("js.baseline.toggle_title") + end + + + + header.with_action_zen_mode_button + + header.with_action_menu( + menu_arguments: { + anchor_align: :end + }, + button_arguments: { + icon: "kebab-horizontal", + "aria-label": t(:label_more), + data: { "test-selector": "project-more-dropdown-menu" } + } + ) do |menu| + menu.with_item( + tag: :a, + label: t(:'queries.configure_view.heading'), + href: @project.present? ? configure_view_dialog_project_work_packages_path(@project) : configure_view_dialog_work_packages_path(), + content_arguments: { data: { controller: "async-dialog" }, rel: "nofollow" } + ) do |item| + item.with_leading_visual_icon(icon: :gear) + end + + if can_rename? + menu.with_item( + label: t("button_rename"), + href: "TODO", + ) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + end + + if can_save? + menu.with_item( + label: t("button_save"), + href: "TODO", + content_arguments: { + data: { "turbo-stream": true, method: :patch } + } + ) do |item| + item.with_leading_visual_icon(icon: :"op-save") + end + end + + if can_save_as? + menu.with_item( + label: t("button_save_as"), + href: "TODO", + content_arguments: { + data: { "turbo-stream": true, method: :patch } + } + ) do |item| + item.with_leading_visual_icon(icon: :"op-save") + end + end + + if can_change_visibility? + menu.with_item( + label: t("js.label_visibility_settings"), + href: "TODO", + content_arguments: { + data: { "turbo-stream": true, method: :patch } + } + ) do |item| + item.with_leading_visual_icon(icon: :"eye") + end + end + + menu.with_item( + tag: :a, + label: t('js.label_export'), + href: @project.present? ? export_dialog_project_work_packages_path(@project) : export_dialog_work_packages_path(), + content_arguments: { data: { controller: "async-dialog" }, rel: "nofollow" } + ) do |item| + item.with_leading_visual_icon(icon: 'sign-out') + end + + if can_delete? + menu.with_item( + tag: :a, + label: t(:button_delete), + scheme: :danger, + href: "TODO", + content_arguments: { data: { controller: "async-dialog" }, rel: "nofollow"} + ) do |item| + item.with_leading_visual_icon(icon: 'trash') + end + end + end + end +%> diff --git a/app/components/work_packages/index_page_header_component.rb b/app/components/work_packages/index_page_header_component.rb new file mode 100644 index 000000000000..535d9b070a77 --- /dev/null +++ b/app/components/work_packages/index_page_header_component.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +class WorkPackages::IndexPageHeaderComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include ApplicationHelper + + def initialize(query: nil, project: nil) + super + + @query = query + @project = project + end + + def breadcrumb_items + items = [{ href: @project ? project_work_packages_path : work_packages_path, text: t(:label_work_package_plural) }, + title] + + if @project + items.prepend({ href: project_overview_path(@project.id), text: @project.name }) + end + + items + end + + def title + @query&.name ? @query.name : t(:label_work_package_plural) + end + + def can_rename? + # TODO + true + end + + def can_save? + # TODO + true + end + + def can_save_as? + # TODO + true + end + + def can_change_visibility? + # TODO + true + end + + def can_delete? + # TODO + true + end +end diff --git a/app/components/work_packages/index_sub_header_component.html.erb b/app/components/work_packages/index_sub_header_component.html.erb new file mode 100644 index 000000000000..acf26fe56d06 --- /dev/null +++ b/app/components/work_packages/index_sub_header_component.html.erb @@ -0,0 +1,17 @@ +<%= render(Primer::OpenProject::SubHeader.new(data: sub_header_data_attributes)) do |subheader| %> + <%= + subheader.with_filter_component do + render(WorkPackages::FilterButtonComponent.new(query: @query)) + end + %> + + <%= + subheader.with_bottom_pane_component(mt: 0) do + render(WorkPackages::FiltersComponent.new(query: @query)) + end + %> + + <% subheader.with_action_component do %> + <%= render WorkPackages::Create::WpCreateButtonComponent.new(project: @project) %> + <% end %> +<% end %> diff --git a/app/components/work_packages/index_sub_header_component.rb b/app/components/work_packages/index_sub_header_component.rb new file mode 100644 index 000000000000..47db9103505f --- /dev/null +++ b/app/components/work_packages/index_sub_header_component.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# -- 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. +# ++ + +module WorkPackages + class IndexSubHeaderComponent < ApplicationComponent + include ApplicationHelper + + def initialize(query: nil, project: nil) + super + @query = query + @project = project + end + + def can_create_work_packages? + # TODO + true + end + + def sub_header_data_attributes + { + controller: "filter--filters-form", + "application-target": "dynamic", + "filter--filters-form-perform-turbo-requests-value": true + } + end + end +end diff --git a/app/components/work_packages/split_view/create_component.html.erb b/app/components/work_packages/split_view/create_component.html.erb new file mode 100644 index 000000000000..4d8bfe33f5bc --- /dev/null +++ b/app/components/work_packages/split_view/create_component.html.erb @@ -0,0 +1,15 @@ +<%= + component_wrapper(class: helpers.container_class) do + flex_layout(h: :full) do |flex| + flex.with_row(flex: 1) do + helpers.angular_component_tag "opce-wp-split-create", + inputs: { + type: @type, + projectIdentifier: @project.present? ? @project.identifier : nil, + resizerClass: helpers.container_class, + routedFromAngular: false, + } + end + end + end +%> diff --git a/app/components/work_packages/split_view/create_component.rb b/app/components/work_packages/split_view/create_component.rb new file mode 100644 index 000000000000..ca7a37b0135e --- /dev/null +++ b/app/components/work_packages/split_view/create_component.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module WorkPackages + module SplitView + class CreateComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:, project:, base_route:) + super + + @type = type + @project = project + @base_route = base_route + end + end + end +end diff --git a/app/components/work_packages/split_view_component.html.erb b/app/components/work_packages/split_view/show_component.html.erb similarity index 79% rename from app/components/work_packages/split_view_component.html.erb rename to app/components/work_packages/split_view/show_component.html.erb index 754c80dadea0..a23b678ef749 100644 --- a/app/components/work_packages/split_view_component.html.erb +++ b/app/components/work_packages/split_view/show_component.html.erb @@ -1,5 +1,5 @@ <%= - component_wrapper(class: "op-work-package-split-view") do + component_wrapper(class: helpers.container_class) do flex_layout(h: :full) do |flex| if @work_package.nil? flex.with_row do @@ -17,8 +17,8 @@ flex.with_row(flex: 1) do helpers.angular_component_tag "opce-wp-split-view", inputs: { - work_package_id: @id, - resizerClass: "op-work-package-split-view", + workPackageId: @id, + resizerClass: helpers.container_class, activeTab: @tab } end diff --git a/app/components/work_packages/split_view/show_component.rb b/app/components/work_packages/split_view/show_component.rb new file mode 100644 index 000000000000..844a36a4e31e --- /dev/null +++ b/app/components/work_packages/split_view/show_component.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module WorkPackages + module SplitView + class ShowComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def self.wrapper_key = :"work-package-details" + + def initialize(id:, base_route:, tab: "overview") + super + + @id = id + @tab = tab + @work_package = WorkPackage.visible.find_by(id:) + @base_route = base_route + end + + def wrapper_uniq_by + @id + end + end + end +end diff --git a/app/components/work_packages/split_view_component.sass b/app/components/work_packages/split_view/show_component.sass similarity index 100% rename from app/components/work_packages/split_view_component.sass rename to app/components/work_packages/split_view/show_component.sass diff --git a/app/components/work_packages/split_view_component.rb b/app/components/work_packages/split_view_component.rb deleted file mode 100644 index 26a84335524f..000000000000 --- a/app/components/work_packages/split_view_component.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class WorkPackages::SplitViewComponent < ApplicationComponent - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable - - def self.wrapper_key = :"work-package-details" - - def initialize(id:, base_route:, tab: "overview") - super - - @id = id - @tab = tab - @work_package = WorkPackage.visible.find_by(id:) - @base_route = base_route - end - - def wrapper_uniq_by - @id - end -end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 609198f726f0..65c730052a22 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -252,6 +252,12 @@ def find_project_by_project_id render_404 end + 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 def find_project_from_association render_404 if @object.blank? diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index c17fec995245..cd3e05992c0e 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -33,28 +33,34 @@ class WorkPackagesController < ApplicationController include WorkPackagesControllerHelper include OpTurbo::DialogStreamHelper include OpTurbo::ComponentStream + include WorkPackages::WithSplitView accept_key_auth :index, :show before_action :authorize_on_work_package, :project, only: :show - 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 :authorize, only: :show_conflict_flash_message - authorization_checked! :index, :show, :export_dialog - - before_action :load_and_validate_query, only: :index, unless: -> { request.format.html? } + before_action :find_optional_project, + only: %i[split_view split_create baseline_dialog include_projects_dialog configure_view_dialog] + before_action :load_and_authorize_in_optional_project, only: %i[index export_dialog new copy] + authorization_checked! :index, :show, :new, :copy, :export_dialog, :split_view, :split_create, :baseline_dialog, + :include_projects_dialog, :configure_view_dialog + + before_action :load_and_validate_query, + only: %i[index split_view split_create copy baseline_dialog include_projects_dialog configure_view_dialog] before_action :load_work_packages, only: :index, if: -> { request.format.atom? } before_action :load_and_validate_query_for_export, only: :export_dialog + no_authorization_required! :share_upsale + def index respond_to do |format| format.html do render :index, - locals: { query: @query, project: @project, menu_name: project_or_global_menu }, - layout: "angular/angular" + locals: { query: @query, project: @project, menu_name: project_or_global_menu } end format.any(*supported_list_formats) do @@ -71,8 +77,7 @@ def show respond_to do |format| format.html do 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 @@ -89,8 +94,62 @@ def show end end + def split_view + render_split_view + end + + def split_create + render_split_view + end + + 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 + + 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 + + def share_upsale + respond_to do |format| + format.html do + render :share_upsale, + locals: { query: @query, project: @project, menu_name: project_or_global_menu } + end + end + end + def export_dialog - respond_with_dialog WorkPackages::Exports::ModalDialogComponent.new(query: @query, project: @project, title: params[:title]) + respond_with_dialog WorkPackages::Exports::ModalDialogComponent.new(query: @query, + project: @project, + title: params[:title]) + end + + def baseline_dialog + respond_with_dialog WorkPackages::Baseline::ModalDialogComponent.new(query: @query, + project: @project, + title: params[:title]) + end + + def include_projects_dialog + respond_with_dialog WorkPackages::IncludeProjects::ModalDialogComponent.new(query: @query, + project: @project, + title: params[:title]) + end + + def configure_view_dialog + respond_with_dialog WorkPackages::ConfigureView::ModalDialogComponent.new(query: @query, + project: @project) end def show_conflict_flash_message @@ -108,6 +167,14 @@ def show_conflict_flash_message protected + def split_view_base_route + if @project + project_work_packages_path(@project, request.query_parameters) + else + work_packages_path(request.query_parameters) + end + end + def load_and_validate_query_for_export load_and_validate_query end @@ -161,7 +228,7 @@ def per_page_param end def project - @project ||= work_package ? work_package.project : nil + @project ||= work_package&.project end def work_package @@ -209,4 +276,16 @@ def load_work_packages def login_back_url_params params.permit(:query_id, :state, :query_props) end + + def render_split_view + respond_to do |format| + format.html do + if turbo_frame_request? + render "work_packages/split_view", layout: false + else + render :index, locals: { query: @query, project: @project, menu_name: project_or_global_menu } + end + end + end + end end diff --git a/app/helpers/work_packages/split_view_helper.rb b/app/helpers/work_packages/split_view_helper.rb index 5f82668bd199..3a790b8da7c7 100644 --- a/app/helpers/work_packages/split_view_helper.rb +++ b/app/helpers/work_packages/split_view_helper.rb @@ -1,11 +1,24 @@ module WorkPackages::SplitViewHelper def render_work_package_split_view? - params[:work_package_split_view].present? + params[:view_type].present? end - def split_view_instance - WorkPackages::SplitViewComponent.new(id: params[:work_package_id], - tab: params[:tab], - base_route: split_view_base_route) + def split_view_instance(view_type:, project: nil) + case view_type + when "work_package_split_view" + WorkPackages::SplitView::ShowComponent.new(id: params[:work_package_id], + tab: params[:tab], + base_route: split_view_base_route) + when "work_package_split_create" + WorkPackages::SplitView::CreateComponent.new(type: params[:type], + project:, + base_route: split_view_base_route) + else + # TODO + end + end + + def container_class + "op-work-package-split-view" end end diff --git a/app/models/queries/work_packages/filter/manual_sort_filter.rb b/app/models/queries/work_packages/filter/manual_sort_filter.rb index 79e526d466bb..15892a284848 100644 --- a/app/models/queries/work_packages/filter/manual_sort_filter.rb +++ b/app/models/queries/work_packages/filter/manual_sort_filter.rb @@ -34,6 +34,10 @@ def available_operators [Queries::Operators::OrderedWorkPackages] end + def default_operator + Queries::Operators::OrderedWorkPackages + end + def available? true end diff --git a/app/models/query.rb b/app/models/query.rb index 47eabd5b19d6..ec12db25054b 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -197,6 +197,10 @@ def filter_for(field) filter end + def find_active_filter(name) + filters.detect { |f| f.name == name } + end + # Removes the filter with the given name # from the query without persisting the change. # diff --git a/app/seeders/demo_data/references.rb b/app/seeders/demo_data/references.rb index 60752ae2260a..029686ccdeea 100644 --- a/app/seeders/demo_data/references.rb +++ b/app/seeders/demo_data/references.rb @@ -131,7 +131,7 @@ def work_package_link(work_package) url_helpers.project_work_package_path( id: work_package.id, project_id: work_package.project.identifier, - state: "activity" + tab: "activity" ) end end diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index b7dab8db5892..0e61c0bee2d9 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -16,5 +16,5 @@ <% content_for :content_body_right do %> <%# When we update the split screen from a turbo frame requset, the title is not correctly updated (Hack for #57705) %> <%= turbo_stream.set_title(title: page_title(*html_title_parts)) if turbo_frame_request? %> - <%= render(split_view_instance) if render_work_package_split_view? %> + <%= render(split_view_instance(view_type: "work_package_split_view")) if render_work_package_split_view? %> <% end %> diff --git a/app/views/work_packages/copy.html.erb b/app/views/work_packages/copy.html.erb new file mode 100644 index 000000000000..03b4ce0786ce --- /dev/null +++ b/app/views/work_packages/copy.html.erb @@ -0,0 +1,45 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<% html_title(t('activerecord.attributes.project.work_packages')) -%> + +<% content_for :sidebar do %> + <%= render partial: 'sidebar' %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {query_id: query, format: 'atom', page: nil, key: User.current.rss_key}, title: t(:label_work_package_plural)) %> + <%= auto_discovery_link_tag(:atom, {controller: '/journals', action: 'index', query_id: query, format: 'atom', page: nil, key: User.current.rss_key}, title: t(:label_changes_details)) %> +<% end %> + +<% content_for :content_body do %> + <%= render WorkPackages::FullView::CopyComponent.new(type: params[:type], + copied_from_work_package_id: params[:id], + project: @project) %> +<% end %> diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index f6c050d4b9ff..84bc6afe065d 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -28,7 +28,6 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title(t('activerecord.attributes.project.work_packages')) -%> -<%= call_hook(:view_work_packages_index_bottom, { project: project, query: query }) %> <% content_for :sidebar do %> <%= render partial: 'sidebar' %> @@ -38,3 +37,18 @@ See COPYRIGHT and LICENSE files for more details. <%= 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_header do %> + <%= render WorkPackages::IndexPageHeaderComponent.new(project: @project) %> + <%= render WorkPackages::IndexSubHeaderComponent.new(project: @project, query: @query) %> +<% end %> + +<% content_for :content_body do %> + <%= angular_component_tag "opce-work-packages-list" %> +<% end %> + +<% content_for :content_body_right do %> + <%= render(split_view_instance(view_type: params[:view_type], project: @project)) if render_work_package_split_view? %> +<% end %> + +<%= call_hook(:view_work_packages_index_bottom, { project: project, query: query }) %> diff --git a/app/views/work_packages/new.html.erb b/app/views/work_packages/new.html.erb new file mode 100644 index 000000000000..035ba2bc36d7 --- /dev/null +++ b/app/views/work_packages/new.html.erb @@ -0,0 +1,44 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<% html_title(t('activerecord.attributes.project.work_packages')) -%> + +<% content_for :sidebar do %> + <%= render partial: 'sidebar' %> +<% end %> + +<% content_for :header_tags do %> + <%= auto_discovery_link_tag(:atom, {query_id: query, format: 'atom', page: nil, key: User.current.rss_key}, title: t(:label_work_package_plural)) %> + <%= auto_discovery_link_tag(:atom, {controller: '/journals', action: 'index', query_id: query, format: 'atom', page: nil, key: User.current.rss_key}, title: t(:label_changes_details)) %> +<% end %> + +<% content_for :content_body do %> + <%= render WorkPackages::FullView::CreateComponent.new(type: params[:type], + project: @project) %> +<% end %> diff --git a/app/views/work_packages/share_upsale.html.erb b/app/views/work_packages/share_upsale.html.erb new file mode 100644 index 000000000000..2a88e6777242 --- /dev/null +++ b/app/views/work_packages/share_upsale.html.erb @@ -0,0 +1,5 @@ +<% html_title(t('activerecord.attributes.project.work_packages')) -%> + +<% content_for :content_body do %> + <%= angular_component_tag "opce-share-upsale" %> +<% end %> diff --git a/app/views/work_packages/show.html.erb b/app/views/work_packages/show.html.erb index 136812d91752..9280f9b60a99 100644 --- a/app/views/work_packages/show.html.erb +++ b/app/views/work_packages/show.html.erb @@ -38,3 +38,7 @@ See COPYRIGHT and LICENSE files for more details. { format: 'atom', key: User.current.rss_key }, title: "#{work_package.project} - #{work_package.to_s}") %> <% end %> + +<% content_for :content_body do %> + <%= render WorkPackages::FullView::ShowComponent.new(id: work_package.id, tab: params[:tab] || "activity") %> +<% end %> diff --git a/app/views/work_packages/split_view.html.erb b/app/views/work_packages/split_view.html.erb index 9f27d274a393..6c972f12f4dc 100644 --- a/app/views/work_packages/split_view.html.erb +++ b/app/views/work_packages/split_view.html.erb @@ -1,3 +1,3 @@ <%= turbo_frame_tag "content-bodyRight" do %> - <%= render(split_view_instance) %> + <%= render(split_view_instance(view_type: params[:view_type], project: @project)) %> <% end %> diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 51b4d6184a41..3d3e07d5c21a 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -224,13 +224,16 @@ "work_packages/reports": %i[report report_details], "work_packages/activities_tab": %i[index update_streams update_sorting update_filter], "work_packages/menus": %i[show], + "work_packages/page_header": %i[index], "work_packages/hover_card": %i[show] }, permissible_on: %i[work_package project], contract_actions: { work_packages: %i[read] } wpt.permission :add_work_packages, - {}, + { + work_packages: %i[new] + }, permissible_on: :project, dependencies: :view_work_packages, contract_actions: { work_packages: %i[create] } @@ -252,7 +255,9 @@ contract_actions: { work_packages: %i[move] } wpt.permission :copy_work_packages, - {}, + { + work_packages: %i[copy] + }, permissible_on: %i[work_package project], require: :loggedin, dependencies: :view_work_packages diff --git a/config/routes.rb b/config/routes.rb index 02699c025748..5b9b7b106ec1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -93,6 +93,21 @@ end end + concern :with_split_view do |options| + get "details/:work_package_id(/:tab)", + action: options.fetch(:action, :split_view), + defaults: { tab: :overview }, + as: :details, + view_type: "work_package_split_view" + end + + concern :with_split_create do |options| + get "split_create", + action: options.fetch(:action, :split_create), + as: :split_create, + view_type: "work_package_split_create" + end + scope controller: "account" do get "/account/force_password_change", action: "force_password_change" post "/account/change_password", action: "change_password" @@ -321,21 +336,22 @@ # work as a catchall for everything under /wiki get "wiki" => "wiki#show" - resources :work_packages, only: [] do + resources :work_packages, only: %i[index show new] do collection do + concerns :with_split_view, base_route: :project_work_packages_path + concerns :with_split_create, base_route: :project_work_packages_path + get "/report/:detail" => "work_packages/reports#report_details" get "/report" => "work_packages/reports#report" get "menu" => "work_packages/menus#show" get "/export_dialog" => "work_packages#export_dialog" + get "/baseline_dialog" => "work_packages#baseline_dialog" + get "/include_projects_dialog" => "work_packages#include_projects_dialog" + get "/configure_view_dialog" => "work_packages#configure_view_dialog" end - # states managed by client-side routing on work_package#index - get "(/*state)" => "work_packages#index", on: :collection, as: "" - 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: "" + get "/copy" => "work_packages#copy", on: :member, as: "copy" + get "(/:tab)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|split_view|split_create|new|copy)).+/ } end resources :activity, :activities, only: :index, controller: "activities" do @@ -561,6 +577,7 @@ namespace :work_packages do get "menu" => "menus#show" + get "index_page_header" => "page_header#index" match "auto_complete" => "auto_completes#index", via: %i[get post] resource :bulk, controller: "bulk", only: %i[edit update destroy] @@ -580,9 +597,6 @@ # move individual wp resource :move, controller: "work_packages/moves", only: %i[new create] - # states managed by client-side routing on work_package#index - get "details/*state" => "work_packages#index", on: :collection, as: :details - resources :activities, controller: "work_packages/activities_tab", only: %i[index create edit update] do member do get :cancel_edit @@ -597,24 +611,33 @@ resource :progress, only: %i[new edit update], controller: "work_packages/progress" collection do + concerns :with_split_view, base_route: :work_packages_path + concerns :with_split_create, base_route: :work_packages_path + resource :progress, only: :create, controller: "work_packages/progress", as: :work_package_progress end + get "/export_dialog" => "work_packages#export_dialog", on: :collection, as: "export_dialog" + get "/baseline_dialog" => "work_packages#baseline_dialog", on: :collection, as: "baseline_dialog" + get "/include_projects_dialog" => "work_packages#include_projects_dialog", on: :collection, as: "include_projects_dialog" + get "/configure_view_dialog" => "work_packages#configure_view_dialog", on: :collection, as: "configure_view_dialog" get :show_conflict_flash_message, on: :collection # we don't need a specific work package for this get "/split_view/update_counter" => "work_packages/split_view#update_counter", on: :member + 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|split_view|split_create|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_upsale" => "work_packages#index", on: :collection, as: "share_upsale" + #get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|split_view|split_create|copy)).+/ } + get "/share_upsale" => "work_packages#share_upsale", on: :collection, as: "share_upsale" get "/edit" => "work_packages#show", on: :member, as: "edit" end @@ -746,14 +769,6 @@ root to: "account#login" - concern :with_split_view do |options| - get "details/:work_package_id(/:tab)", - action: options.fetch(:action, :split_view), - defaults: { tab: :overview }, - as: :details, - work_package_split_view: true - end - resources :notifications, only: :index do collection do concerns :with_split_view, base_route: :notifications_path diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 51d140173014..38e7ee99814b 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -26,13 +26,21 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { APP_INITIALIZER, ApplicationRef, DoBootstrap, Injector, NgModule } from '@angular/core'; +import { + APP_INITIALIZER, + ApplicationRef, + DoBootstrap, + Injector, + NgModule, +} from '@angular/core'; import { A11yModule } from '@angular/cdk/a11y'; -import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; -import { ReactiveFormsModule } from '@angular/forms'; import { - OpContextMenuTrigger, -} from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive'; + HTTP_INTERCEPTORS, + HttpClient, + HttpClientModule, +} from '@angular/common/http'; +import { ReactiveFormsModule } from '@angular/forms'; +import { OpContextMenuTrigger } from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive'; import { States } from 'core-app/core/states/states.service'; import { OpenprojectFieldsModule } from 'core-app/shared/components/fields/openproject-fields.module'; import { OpSharedModule } from 'core-app/shared/shared.module'; @@ -43,46 +51,32 @@ import { OpenprojectAttachmentsModule } from 'core-app/shared/components/attachm import { OpenprojectEditorModule } from 'core-app/shared/components/editor/openproject-editor.module'; import { OpenprojectGridsModule } from 'core-app/shared/components/grids/openproject-grids.module'; import { OpenprojectRouterModule } from 'core-app/core/routing/openproject-router.module'; -import { - OpenprojectWorkPackageRoutesModule, -} from 'core-app/features/work-packages/openproject-work-package-routes.module'; +import { OpenprojectWorkPackageRoutesModule } from 'core-app/features/work-packages/openproject-work-package-routes.module'; import { BrowserModule } from '@angular/platform-browser'; import { OpenprojectCalendarModule } from 'core-app/features/calendar/openproject-calendar.module'; import { OpenprojectGlobalSearchModule } from 'core-app/core/global_search/openproject-global-search.module'; import { OpenprojectDashboardsModule } from 'core-app/features/dashboards/openproject-dashboards.module'; -import { - OpenprojectWorkPackageGraphsModule, -} from 'core-app/shared/components/work-package-graphs/openproject-work-package-graphs.module'; +import { OpenprojectWorkPackageGraphsModule } from 'core-app/shared/components/work-package-graphs/openproject-work-package-graphs.module'; import { HoverCardTriggerService } from 'core-app/core/setup/globals/global-listeners/hover-card-trigger.service'; import { OpenprojectOverviewModule } from 'core-app/features/overview/openproject-overview.module'; import { OpenprojectMyPageModule } from 'core-app/features/my-page/openproject-my-page.module'; import { OpenprojectProjectsModule } from 'core-app/features/projects/openproject-projects.module'; import { KeyboardShortcutService } from 'core-app/shared/directives/a11y/keyboard-shortcut.service'; import { CopyToClipboardService } from 'core-app/shared/components/copy-to-clipboard/copy-to-clipboard.service'; -import { - OpenprojectMembersModule, -} from 'core-app/shared/components/autocompleter/members-autocompleter/members.module'; +import { OpenprojectMembersModule } from 'core-app/shared/components/autocompleter/members-autocompleter/members.module'; import { OpenprojectAugmentingModule } from 'core-app/core/augmenting/openproject-augmenting.module'; import { OpenprojectInviteUserModalModule } from 'core-app/features/invite-user-modal/invite-user-modal.module'; import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.module'; -import { - RevitAddInSettingsButtonService, -} from 'core-app/features/bim/revit_add_in/revit-add-in-settings-button.service'; +import { RevitAddInSettingsButtonService } from 'core-app/features/bim/revit_add_in/revit-add-in-settings-button.service'; import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openproject-enterprise.module'; import { MainMenuToggleComponent } from 'core-app/core/main-menu/main-menu-toggle.component'; import { ConfirmDialogService } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.service'; import { ConfirmDialogModalComponent } from 'core-app/shared/components/modals/confirm-dialog/confirm-dialog.modal'; import { DynamicContentModalComponent } from 'core-app/shared/components/modals/modal-wrapper/dynamic-content.modal'; -import { - PasswordConfirmationModalComponent, -} from 'core-app/shared/components/modals/request-for-confirmation/password-confirmation.modal'; +import { PasswordConfirmationModalComponent } from 'core-app/shared/components/modals/request-for-confirmation/password-confirmation.modal'; import { HoverCardComponent } from 'core-app/shared/components/modals/preview-modal/hover-card-modal/hover-card.modal'; -import { - OpHeaderProjectSelectComponent, -} from 'core-app/shared/components/header-project-select/header-project-select.component'; -import { - OpHeaderProjectSelectListComponent, -} from 'core-app/shared/components/header-project-select/list/header-project-select-list.component'; +import { OpHeaderProjectSelectComponent } from 'core-app/shared/components/header-project-select/header-project-select.component'; +import { OpHeaderProjectSelectListComponent } from 'core-app/shared/components/header-project-select/list/header-project-select-list.component'; import { PaginationService } from 'core-app/shared/components/table-pagination/pagination-service'; import { MainMenuResizerComponent } from 'core-app/shared/components/resizer/resizer/main-menu-resizer.component'; @@ -91,14 +85,10 @@ import { OpenprojectAdminModule } from 'core-app/features/admin/openproject-admi import { OpenprojectHalModule } from 'core-app/features/hal/openproject-hal.module'; import { OpenprojectPluginsModule } from 'core-app/features/plugins/openproject-plugins.module'; import { LinkedPluginsModule } from 'core-app/features/plugins/linked-plugins.module'; -import { - OpenProjectInAppNotificationsModule, -} from 'core-app/features/in-app-notifications/in-app-notifications.module'; +import { OpenProjectInAppNotificationsModule } from 'core-app/features/in-app-notifications/in-app-notifications.module'; import { OpenProjectBackupService } from './core/backup/op-backup.service'; import { OpenProjectStateModule } from 'core-app/core/state/openproject-state.module'; -import { - OpenprojectContentLoaderModule, -} from 'core-app/shared/components/op-content-loader/openproject-content-loader.module'; +import { OpenprojectContentLoaderModule } from 'core-app/shared/components/op-content-loader/openproject-content-loader.module'; import { OpenProjectHeaderInterceptor } from 'core-app/features/hal/http/openproject-header-interceptor'; import { TopMenuService } from 'core-app/core/top-menu/top-menu.service'; import { OpUploadService } from 'core-app/core/upload/upload.service'; @@ -106,137 +96,84 @@ import { ConfigurationService } from 'core-app/core/config/configuration.service import { FogUploadService } from 'core-app/core/upload/fog-upload.service'; import { LocalUploadService } from 'core-app/core/upload/local-upload.service'; import { registerCustomElement } from 'core-app/shared/helpers/angular/custom-elements.helper'; -import { - EmbeddedTablesMacroComponent, -} from 'core-app/features/work-packages/components/wp-table/embedded/embedded-tables-macro.component'; +import { EmbeddedTablesMacroComponent } from 'core-app/features/work-packages/components/wp-table/embedded/embedded-tables-macro.component'; import { OpPrincipalComponent } from 'core-app/shared/components/principal/principal.component'; -import { - OpBasicSingleDatePickerComponent, -} from 'core-app/shared/components/datepicker/basic-single-date-picker/basic-single-date-picker.component'; -import { - OpBasicRangeDatePickerComponent, -} from 'core-app/shared/components/datepicker/basic-range-date-picker/basic-range-date-picker.component'; +import { OpBasicSingleDatePickerComponent } from 'core-app/shared/components/datepicker/basic-single-date-picker/basic-single-date-picker.component'; +import { OpBasicRangeDatePickerComponent } from 'core-app/shared/components/datepicker/basic-range-date-picker/basic-range-date-picker.component'; import { GlobalSearchInputComponent } from 'core-app/core/global_search/input/global-search-input.component'; -import { - OpAutocompleterComponent, -} from 'core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component'; -import { - ProjectAutocompleterComponent, -} from 'core-app/shared/components/autocompleter/project-autocompleter/project-autocompleter.component'; -import { - AutocompleteSelectDecorationComponent, -} from 'core-app/shared/components/autocompleter/autocomplete-select-decoration/autocomplete-select-decoration.component'; -import { - MembersAutocompleterComponent, -} from 'core-app/shared/components/autocompleter/members-autocompleter/members-autocompleter.component'; -import { - UserAutocompleterComponent, -} from 'core-app/shared/components/autocompleter/user-autocompleter/user-autocompleter.component'; +import { OpAutocompleterComponent } from 'core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component'; +import { ProjectAutocompleterComponent } from 'core-app/shared/components/autocompleter/project-autocompleter/project-autocompleter.component'; +import { AutocompleteSelectDecorationComponent } from 'core-app/shared/components/autocompleter/autocomplete-select-decoration/autocomplete-select-decoration.component'; +import { MembersAutocompleterComponent } from 'core-app/shared/components/autocompleter/members-autocompleter/members-autocompleter.component'; +import { UserAutocompleterComponent } from 'core-app/shared/components/autocompleter/user-autocompleter/user-autocompleter.component'; import { AttributeValueMacroComponent } from 'core-app/shared/components/fields/macros/attribute-value-macro.component'; import { AttributeLabelMacroComponent } from 'core-app/shared/components/fields/macros/attribute-label-macro.component'; -import { - WorkPackageQuickinfoMacroComponent, -} from 'core-app/shared/components/fields/macros/work-package-quickinfo-macro.component'; -import { - CkeditorAugmentedTextareaComponent, -} from 'core-app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component'; -import { - DraggableAutocompleteComponent, -} from 'core-app/shared/components/autocompleter/draggable-autocomplete/draggable-autocomplete.component'; -import { - AttributeHelpTextComponent, -} from 'core-app/shared/components/attribute-help-texts/attribute-help-text.component'; +import { WorkPackageQuickinfoMacroComponent } from 'core-app/shared/components/fields/macros/work-package-quickinfo-macro.component'; +import { CkeditorAugmentedTextareaComponent } from 'core-app/shared/components/editor/components/ckeditor-augmented-textarea/ckeditor-augmented-textarea.component'; +import { DraggableAutocompleteComponent } from 'core-app/shared/components/autocompleter/draggable-autocomplete/draggable-autocomplete.component'; +import { AttributeHelpTextComponent } from 'core-app/shared/components/attribute-help-texts/attribute-help-text.component'; import { OpExclusionInfoComponent } from 'core-app/shared/components/fields/display/info/op-exclusion-info.component'; import { NewProjectComponent } from 'core-app/features/projects/components/new-project/new-project.component'; import { CopyProjectComponent } from 'core-app/features/projects/components/copy-project/copy-project.component'; import { ProjectsComponent } from 'core-app/features/projects/components/projects/projects.component'; import { OpenProjectJobStatusModule } from 'core-app/features/job-status/openproject-job-status.module'; -import { - NotificationsSettingsPageComponent, -} from 'core-app/features/user-preferences/notifications-settings/page/notifications-settings-page.component'; -import { - ReminderSettingsPageComponent, -} from 'core-app/features/user-preferences/reminder-settings/page/reminder-settings-page.component'; +import { NotificationsSettingsPageComponent } from 'core-app/features/user-preferences/notifications-settings/page/notifications-settings-page.component'; +import { ReminderSettingsPageComponent } from 'core-app/features/user-preferences/reminder-settings/page/reminder-settings-page.component'; import { OpenProjectMyAccountModule } from 'core-app/features/user-preferences/user-preferences.module'; import { OpAttachmentsComponent } from 'core-app/shared/components/attachments/attachments.component'; -import { - InAppNotificationCenterComponent, -} from 'core-app/features/in-app-notifications/center/in-app-notification-center.component'; -import { - WorkPackageSplitViewEntryComponent, -} from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component'; -import { - InAppNotificationsDateAlertsUpsaleComponent, -} from 'core-app/features/in-app-notifications/date-alerts-upsale/ian-date-alerts-upsale.component'; +import { InAppNotificationCenterComponent } from 'core-app/features/in-app-notifications/center/in-app-notification-center.component'; +import { WorkPackageSplitViewEntryComponent } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component'; +import { InAppNotificationsDateAlertsUpsaleComponent } from 'core-app/features/in-app-notifications/date-alerts-upsale/ian-date-alerts-upsale.component'; import { ShareUpsaleComponent } from 'core-app/features/enterprise/share-upsale/share-upsale.component'; -import { - StorageLoginButtonComponent, -} from 'core-app/shared/components/storages/storage-login-button/storage-login-button.component'; +import { StorageLoginButtonComponent } from 'core-app/shared/components/storages/storage-login-button/storage-login-button.component'; import { OpCustomModalOverlayComponent } from 'core-app/shared/components/modal/custom-modal-overlay.component'; import { TimerAccountMenuComponent } from 'core-app/shared/components/time_entries/timer/timer-account-menu.component'; -import { - RemoteFieldUpdaterComponent, -} from 'core-app/shared/components/remote-field-updater/remote-field-updater.component'; -import { - OpModalSingleDatePickerComponent, -} from 'core-app/shared/components/datepicker/modal-single-date-picker/modal-single-date-picker.component'; +import { RemoteFieldUpdaterComponent } from 'core-app/shared/components/remote-field-updater/remote-field-updater.component'; +import { OpModalSingleDatePickerComponent } from 'core-app/shared/components/datepicker/modal-single-date-picker/modal-single-date-picker.component'; import { SpotDropModalPortalComponent } from 'core-app/spot/components/drop-modal/drop-modal-portal.component'; import { OpModalOverlayComponent } from 'core-app/shared/components/modal/modal-overlay.component'; -import { - InAppNotificationBellComponent, -} from 'core-app/features/in-app-notifications/bell/in-app-notification-bell.component'; +import { InAppNotificationBellComponent } from 'core-app/features/in-app-notifications/bell/in-app-notification-bell.component'; import { BackupComponent } from 'core-app/core/setup/globals/components/admin/backup.component'; -import { - EditableQueryPropsComponent, -} from 'core-app/features/admin/editable-query-props/editable-query-props.component'; -import { - TriggerActionsEntryComponent, -} from 'core-app/shared/components/time_entries/edit/trigger-actions-entry.component'; -import { - WorkPackageOverviewGraphComponent, -} from 'core-app/shared/components/work-package-graphs/overview/wp-overview-graph.component'; -import { - EEActiveSavedTrialComponent, -} from 'core-app/features/enterprise/enterprise-active-trial/ee-active-saved-trial.component'; +import { EditableQueryPropsComponent } from 'core-app/features/admin/editable-query-props/editable-query-props.component'; +import { TriggerActionsEntryComponent } from 'core-app/shared/components/time_entries/edit/trigger-actions-entry.component'; +import { WorkPackageOverviewGraphComponent } from 'core-app/shared/components/work-package-graphs/overview/wp-overview-graph.component'; +import { EEActiveSavedTrialComponent } from 'core-app/features/enterprise/enterprise-active-trial/ee-active-saved-trial.component'; import { FreeTrialButtonComponent } from 'core-app/features/enterprise/free-trial-button/free-trial-button.component'; import { EnterpriseBaseComponent } from 'core-app/features/enterprise/enterprise-base.component'; import { NoResultsComponent } from 'core-app/shared/components/no-results/no-results.component'; -import { - OpNonWorkingDaysListComponent, -} from 'core-app/shared/components/op-non-working-days-list/op-non-working-days-list.component'; +import { OpNonWorkingDaysListComponent } from 'core-app/shared/components/op-non-working-days-list/op-non-working-days-list.component'; import { EnterpriseBannerComponent } from 'core-app/shared/components/enterprise-banner/enterprise-banner.component'; -import { - CollapsibleSectionComponent, -} from 'core-app/shared/components/collapsible-section/collapsible-section.component'; +import { CollapsibleSectionComponent } from 'core-app/shared/components/collapsible-section/collapsible-section.component'; import { CopyToClipboardComponent } from 'core-app/shared/components/copy-to-clipboard/copy-to-clipboard.component'; import { GlobalSearchTitleComponent } from 'core-app/core/global_search/title/global-search-title.component'; import { ContentTabsComponent } from 'core-app/shared/components/tabs/content-tabs/content-tabs.component'; -import { - AddSectionDropdownComponent, -} from 'core-app/shared/components/hide-section/add-section-dropdown/add-section-dropdown.component'; -import { - HideSectionLinkComponent, -} from 'core-app/shared/components/hide-section/hide-section-link/hide-section-link.component'; +import { AddSectionDropdownComponent } from 'core-app/shared/components/hide-section/add-section-dropdown/add-section-dropdown.component'; +import { HideSectionLinkComponent } from 'core-app/shared/components/hide-section/hide-section-link/hide-section-link.component'; import { PersistentToggleComponent } from 'core-app/shared/components/persistent-toggle/persistent-toggle.component'; import { TypeFormConfigurationComponent } from 'core-app/features/admin/types/type-form-configuration.component'; import { ToastsContainerComponent } from 'core-app/shared/components/toaster/toasts-container.component'; import { GlobalSearchWorkPackagesComponent } from 'core-app/core/global_search/global-search-work-packages.component'; -import { - CustomDateActionAdminComponent, -} from 'core-app/features/work-packages/components/wp-custom-actions/date-action/custom-date-action-admin.component'; +import { CustomDateActionAdminComponent } from 'core-app/features/work-packages/components/wp-custom-actions/date-action/custom-date-action-admin.component'; import { HomescreenNewFeaturesBlockComponent } from 'core-app/features/homescreen/blocks/new-features.component'; import { GlobalSearchTabsComponent } from 'core-app/core/global_search/tabs/global-search-tabs.component'; -import { - ZenModeButtonComponent, -} from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; +import { ZenModeButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; import { ColorsAutocompleterComponent } from 'core-app/shared/components/colors/colors-autocompleter.component'; +import { StaticAttributeHelpTextComponent } from 'core-app/shared/components/attribute-help-texts/static-attribute-help-text.component'; import { - StaticAttributeHelpTextComponent, -} from 'core-app/shared/components/attribute-help-texts/static-attribute-help-text.component'; -import { appBaseSelector, ApplicationBaseComponent } from 'core-app/core/routing/base/application-base.component'; + appBaseSelector, + ApplicationBaseComponent, +} from 'core-app/core/routing/base/application-base.component'; import { SpotSwitchComponent } from 'core-app/spot/components/switch/switch.component'; import { OPContextMenuService } from 'core-app/shared/components/op-context-menu/op-context-menu.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { WorkPackagePrimerizedListViewComponent } from 'core-app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component'; +import { WorkPackageSplitCreateEntryComponent } from 'core-app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component'; +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 { OpBaselineEntryComponent } from 'core-app/features/work-packages/components/wp-baseline/baseline/baseline-entry.component'; +import { OpIncludeProjectsEntryComponent } from 'core-app/shared/components/project-include/project-include-entry.component'; +import { WpTableConfigurationTabEntryComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/tab-entry.component'; export function initializeServices(injector:Injector) { return () => { @@ -451,6 +388,13 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-ian-date-alerts-upsale', InAppNotificationsDateAlertsUpsaleComponent, { injector }); registerCustomElement('opce-share-upsale', ShareUpsaleComponent, { injector }); registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector }); + registerCustomElement('opce-wp-split-create', WorkPackageSplitCreateEntryComponent, { injector }); + registerCustomElement('opce-wp-full-view', WorkPackageFullViewEntryComponent, { injector }); + registerCustomElement('opce-wp-full-create', WorkPackageFullCreateEntryComponent, { injector }); + registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector }); + registerCustomElement('opce-baseline', OpBaselineEntryComponent, { injector }); + registerCustomElement('opce-include-projects', OpIncludeProjectsEntryComponent, { injector }); + registerCustomElement('opce-wp-table-configuration-tab', WpTableConfigurationTabEntryComponent, { injector }); registerCustomElement('opce-timer-account-menu', TimerAccountMenuComponent, { injector }); registerCustomElement('opce-remote-field-updater', RemoteFieldUpdaterComponent, { injector }); registerCustomElement('opce-modal-single-date-picker', OpModalSingleDatePickerComponent, { injector }); @@ -487,5 +431,6 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-global-search-tabs', GlobalSearchTabsComponent, { injector }); registerCustomElement('opce-zen-mode-toggle-button', ZenModeButtonComponent, { injector }); registerCustomElement('opce-colors-autocompleter', ColorsAutocompleterComponent, { injector }); + registerCustomElement('opce-work-packages-list', WorkPackagePrimerizedListViewComponent, { injector }); } } diff --git a/frontend/src/app/core/loading-indicator/loading-indicator.service.ts b/frontend/src/app/core/loading-indicator/loading-indicator.service.ts index 40cab7fb20bf..0e3159f0cc23 100644 --- a/frontend/src/app/core/loading-indicator/loading-indicator.service.ts +++ b/frontend/src/app/core/loading-indicator/loading-indicator.service.ts @@ -106,6 +106,10 @@ export class LoadingIndicatorService { return this.indicator('table'); } + public get list():LoadingIndicator { + return this.indicator('list'); + } + public get wpDetails() { return this.indicator('wpDetails'); } diff --git a/frontend/src/app/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts index c6228adeb946..57a1d2fcef07 100644 --- a/frontend/src/app/core/path-helper/path-helper.service.ts +++ b/frontend/src/app/core/path-helper/path-helper.service.ts @@ -256,14 +256,30 @@ 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.projectWorkPackagesPath(projectIdentifier)}/${workPackageId}/copy`; + } + return `${this.workPackagePath(workPackageId)}/copy`; } + public workPackageNewPath():string { + return `${this.staticBase}/work_packages/new`; + } + public workPackageDetailsPath(projectIdentifier:string, workPackageId:string|number, tab?:string) { if (tab) { return `${this.projectWorkPackagePath(projectIdentifier, workPackageId)}/details/${tab}`; @@ -272,6 +288,15 @@ export class PathHelperService { return `${this.projectWorkPackagesPath(projectIdentifier)}/details/${workPackageId}`; } + public workPackagePrimerDetailsPath(projectIdentifier:string|null, workPackageId:string|number, tab = 'activity') { + if (projectIdentifier) { + return `${this.projectWorkPackagesPath(projectIdentifier)}/details/${workPackageId}/${tab}`; + } + + return `${this.workPackagesPath()}/details/${workPackageId}/${tab}`; + } + + // Todo: Remove? public workPackageDetailsCopyPath(projectIdentifier:string, workPackageId:string|number) { return this.workPackageDetailsPath(projectIdentifier, workPackageId, 'copy'); } diff --git a/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts b/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts index a1838a14ad9f..55e7bf455c1c 100644 --- a/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts +++ b/frontend/src/app/features/bim/ifc_models/pages/viewer/ifc-viewer-page.component.ts @@ -109,7 +109,6 @@ export class IFCViewerPageComponent extends PartitionedQuerySpacePageComponent i component: WorkPackageCreateButtonComponent, inputs: { stateName$: of(this.newRoute), - allowed: ['work_packages.createWorkPackage', 'work_package.copy'], }, }, { diff --git a/frontend/src/app/features/boards/board/board-list/board-list.component.ts b/frontend/src/app/features/boards/board/board-list/board-list.component.ts index f16e3fe0d14c..e290384a86ef 100644 --- a/frontend/src/app/features/boards/board/board-list/board-list.component.ts +++ b/frontend/src/app/features/boards/board/board-list/board-list.component.ts @@ -510,7 +510,7 @@ export class BoardListComponent extends AbstractWidgetComponent implements OnIni if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts index 9aa35c277222..0b8f018f4798 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-partitioned-page.component.ts @@ -1,37 +1,31 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Injector, -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Injector } from '@angular/core'; import { DynamicComponentDefinition, ToolbarButtonComponentDefinition, ViewPartitionState, } from 'core-app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component'; -import { - StateService, - TransitionService, -} from '@uirouter/core'; +import { StateService, TransitionService } from '@uirouter/core'; import { BoardFilterComponent } from 'core-app/features/boards/board/board-filter/board-filter.component'; import { ToastService } from 'core-app/shared/components/toaster/toast.service'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { BoardService } from 'core-app/features/boards/board/board.service'; import { DragAndDropService } from 'core-app/shared/helpers/drag-and-drop/drag-and-drop.service'; -import { WorkPackageFilterButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-filter-button/wp-filter-button.component'; -import { ZenModeButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; -import { BoardsMenuButtonComponent } from 'core-app/features/boards/board/toolbar-menu/boards-menu-button.component'; import { - catchError, - finalize, - take, -} from 'rxjs/operators'; + WorkPackageFilterButtonComponent, +} from 'core-app/features/work-packages/components/wp-buttons/wp-filter-button/wp-filter-button.component'; +import { + ZenModeButtonComponent, +} from 'core-app/features/work-packages/components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; +import { BoardsMenuButtonComponent } from 'core-app/features/boards/board/toolbar-menu/boards-menu-button.component'; +import { catchError, finalize, take } from 'rxjs/operators'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { QueryResource } from 'core-app/features/hal/resources/query-resource'; import { Ng2StateDeclaration } from '@uirouter/angular'; import { BoardFiltersService } from 'core-app/features/boards/board/board-filter/board-filters.service'; -import { CardViewHandlerRegistry } from 'core-app/features/work-packages/components/wp-card-view/event-handler/card-view-handler-registry'; +import { + CardViewHandlerRegistry, +} from 'core-app/features/work-packages/components/wp-card-view/event-handler/card-view-handler-registry'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; import { OpTitleService } from 'core-app/core/html/op-title.service'; import { EMPTY } from 'rxjs'; diff --git a/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts b/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts index 0f60b209f7ee..9c12761284b8 100644 --- a/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts +++ b/frontend/src/app/features/calendar/wp-calendar-page/wp-calendar-page.component.ts @@ -44,10 +44,10 @@ import { ZenModeButtonComponent } from 'core-app/features/work-packages/componen import { WorkPackageSettingsButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component'; import { QueryResource } from 'core-app/features/hal/resources/query-resource'; import { QueryParamListenerService } from 'core-app/features/work-packages/components/wp-query/query-param-listener.service'; -import { OpProjectIncludeComponent } from 'core-app/shared/components/project-include/project-include.component'; import { calendarRefreshRequest } from 'core-app/features/calendar/calendar.actions'; import { ActionsService } from 'core-app/core/state/actions/actions.service'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { OpProjectIncludeModalComponent } from 'core-app/shared/components/project-include/project-include-modal.component'; @Component({ templateUrl: '../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html', @@ -97,7 +97,7 @@ export class WorkPackagesCalendarPageComponent extends PartitionedQuerySpacePage /** Define the buttons shown in the toolbar */ toolbarButtonComponents:ToolbarButtonComponentDefinition[] = [ { - component: OpProjectIncludeComponent, + component: OpProjectIncludeModalComponent, }, { component: WorkPackageFilterButtonComponent, diff --git a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts index b2bedf695353..1aef1cf5ad7b 100644 --- a/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/page/team-planner-page.component.ts @@ -16,7 +16,6 @@ import { QueryParamListenerService } from 'core-app/features/work-packages/compo import { QueryResource } from 'core-app/features/hal/resources/query-resource'; import { WorkPackageSettingsButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component'; import { CalendarDragDropService } from 'core-app/features/team-planner/team-planner/calendar-drag-drop.service'; -import { OpProjectIncludeComponent } from 'core-app/shared/components/project-include/project-include.component'; import { EffectCallback, registerEffectCallbacks, @@ -29,6 +28,7 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora import { ActionsService } from 'core-app/core/state/actions/actions.service'; import { OpWorkPackagesCalendarService } from 'core-app/features/calendar/op-work-packages-calendar.service'; import { OpCalendarService } from 'core-app/features/calendar/op-calendar.service'; +import { OpProjectIncludeModalComponent } from 'core-app/shared/components/project-include/project-include-modal.component'; @Component({ templateUrl: '../../../work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html', @@ -79,7 +79,7 @@ export class TeamPlannerPageComponent extends PartitionedQuerySpacePageComponent /** Define the buttons shown in the toolbar */ toolbarButtonComponents:ToolbarButtonComponentDefinition[] = [ { - component: OpProjectIncludeComponent, + component: OpProjectIncludeModalComponent, }, { component: WorkPackageFilterButtonComponent, diff --git a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts index 2bcfadd1e9e3..54adf1babd1d 100644 --- a/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts +++ b/frontend/src/app/features/team-planner/team-planner/planner/team-planner.component.ts @@ -828,7 +828,7 @@ export class TeamPlannerComponent extends UntilDestroyedMixin implements OnInit, if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } diff --git a/frontend/src/app/features/work-packages/components/edit-actions-bar/wp-edit-actions-bar.component.ts b/frontend/src/app/features/work-packages/components/edit-actions-bar/wp-edit-actions-bar.component.ts index 86211659f311..3ac4c4a55e25 100644 --- a/frontend/src/app/features/work-packages/components/edit-actions-bar/wp-edit-actions-bar.component.ts +++ b/frontend/src/app/features/work-packages/components/edit-actions-bar/wp-edit-actions-bar.component.ts @@ -27,10 +27,15 @@ //++ import { - ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Output, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Output, } from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { EditFormComponent } from 'core-app/shared/components/fields/edit/edit-form/edit-form.component'; +import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; @Component({ templateUrl: './wp-edit-actions-bar.html', @@ -38,7 +43,7 @@ import { EditFormComponent } from 'core-app/shared/components/fields/edit/edit-f selector: 'wp-edit-actions-bar', }) export class WorkPackageEditActionsBarComponent { - @Output('onSave') public onSave = new EventEmitter(); + @Output('onSave') public onSave = new EventEmitter(); @Output('onCancel') public onCancel = new EventEmitter(); @@ -49,10 +54,11 @@ export class WorkPackageEditActionsBarComponent { cancel: this.I18n.t('js.button_cancel'), }; - constructor(private I18n:I18nService, + constructor( + private I18n:I18nService, private editForm:EditFormComponent, - private cdRef:ChangeDetectorRef) { - } + private cdRef:ChangeDetectorRef, + ) {} public set saving(active:boolean) { this._saving = active; @@ -71,9 +77,9 @@ export class WorkPackageEditActionsBarComponent { this.saving = true; this.editForm .submit() - .then(() => { + .then((value:WorkPackageResource) => { this.saving = false; - this.onSave.emit(); + this.onSave.emit(value); }) .catch(() => { this.saving = false; diff --git a/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline-entry.component.ts b/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline-entry.component.ts new file mode 100644 index 000000000000..4b1e054c2124 --- /dev/null +++ b/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline-entry.component.ts @@ -0,0 +1,59 @@ +//-- 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 { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; +import { WorkPackageIsolatedQuerySpaceDirective } from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; + +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OpBaselineEntryComponent { + @Input() showActionBar? = false; + @Input() visible = true; + @Input() showHeaderText = true; + + constructor( + readonly elementRef:ElementRef, + ) { + populateInputsFromDataset(this); + } +} diff --git a/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline.component.html b/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline.component.html index ec8b14a386ad..4a9e16e64658 100644 --- a/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline.component.html +++ b/frontend/src/app/features/work-packages/components/wp-baseline/baseline/baseline.component.html @@ -2,11 +2,11 @@ [class.op-baseline_tab]="!showActionBar">
    + *ngIf="eeShowBanners && showHeaderText"> {{ text.baseline_comparison }}
    - {{ eeShowBanners ? text.enterprise_header_description : text.header_description }} + {{ eeShowBanners ? text.enterprise_header_description : text.header_description }} = this.wpTableBaseline.nonWorkingDays$; diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts index 954b659bc692..5da202c5dd83 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.component.ts @@ -42,10 +42,10 @@ import { CurrentUserService } from 'core-app/core/current-user/current-user.serv templateUrl: './wp-create-button.html', }) export class WorkPackageCreateButtonComponent extends UntilDestroyedMixin implements OnInit, OnDestroy { - @Input('allowed') allowedWhen:string[]; - @Input('stateName$') stateName$:Observable; + @Input() routedFromAngular:boolean = true; + allowed:boolean; disabled:boolean; diff --git a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html index 032e66fcd211..1d1c18b77a56 100644 --- a/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html +++ b/frontend/src/app/features/work-packages/components/wp-buttons/wp-create-button/wp-create-button.html @@ -8,6 +8,7 @@ [stateName]="stateName$ | async" [dropdownActive]="allowed" [title]="text.title" + [routedFromAngular]="routedFromAngular" > ('type')?.href); - void this.$state.go('work-packages.new', { ...this.$state.params, type }); + const link = this.stateParams.projectPath ? this.pathHelper.projectWorkPackageNewPath(this.stateParams.projectPath) : this.pathHelper.workPackageNewPath(); + window.location.href = link + window.location.search; } public onSaved(params:{ savedResource:WorkPackageResource, isInitial:boolean }) { @@ -184,7 +193,25 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O public cancelAndBackToList() { 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.projectWorkPackagesPath(this.stateParams.projectPath) : this.pathHelper.workPackagesPath(); + Turbo.visit(link + window.location.search, { frame: 'content-bodyRight', action: 'advance' }); + } + } + + public showNewSplitWorkPackage(wp:WorkPackageResource) { + if (wp.id) { + const link = this.pathHelper.workPackageDetailsPath(wp.project.identifier, wp.id) + window.location.search; + Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' }); + } + } + + public showNewFullWorkPackage(wp:WorkPackageResource) { + if (wp.id) { + window.location.href = this.pathHelper.projectWorkPackagePath(wp.project.identifier, wp.id) + window.location.search; + } } protected createdWorkPackage() { diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts index 346263fe27c7..d1d9eaceb34c 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.component.ts @@ -36,5 +36,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorkPackageNewFullViewComponent extends WorkPackageCreateComponent { - public successState = this.$state.current.data.successState as string; + public successState = (this.$state?.current?.data?.successState as string) || ''; } diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html index 235c38a6dcb0..557854821eb7 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-full-view.html @@ -27,7 +27,9 @@ - + diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts index 5727097f307d..a776dcbdf20c 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.component.ts @@ -27,7 +27,11 @@ //++ import { WorkPackageCreateComponent } from 'core-app/features/work-packages/components/wp-new/wp-create.component'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, +} from '@angular/core'; @Component({ selector: 'wp-new-split-view', @@ -35,4 +39,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorkPackageNewSplitViewComponent extends WorkPackageCreateComponent { + @Input() public resizerClass:string = 'work-packages-partitioned-page--content-right'; } diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html index a41ba87e5323..ddf6ff913d82 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html @@ -23,12 +23,14 @@
    + (onCancel)="cancelAndBackToList()" + (onSave)="showNewSplitWorkPackage($event)" + >
    -
    diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts index 7a4e6b2e9529..ca4a2edaeef7 100644 --- a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts +++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/keep-tab/keep-tab.service.ts @@ -32,6 +32,8 @@ import { 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 +41,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 +69,8 @@ export class KeepTabService { return this.currentDetailsTab; } - public goCurrentShowState(params:Record = {}):void { - this.$state.go( - 'work-packages.show.tabs', - { - ...this.uiRouterGlobals.params, - ...params, - tabIdentifier: this.currentShowTab, - }, - ); + public goCurrentShowState(workPackageId:string):void { + window.location.href = this.pathHelper.genericWorkPackagePath(this.currentProject.identifier, workPackageId, this.currentShowTab) + window.location.search; } public goCurrentDetailsState(params:Record = {}):void { diff --git a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts index b65d307b2f27..ed50056e588e 100644 --- a/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-subject/wp-subject.component.ts @@ -26,7 +26,10 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { Component, Input, OnInit } from '@angular/core'; +import { + Component, + Input, +} from '@angular/core'; import { UIRouterGlobals } from '@uirouter/core'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { randomString } from 'core-app/shared/helpers/random-string'; @@ -37,29 +40,15 @@ import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; selector: 'wp-subject', templateUrl: './wp-subject.html', }) -export class WorkPackageSubjectComponent extends UntilDestroyedMixin implements OnInit { +export class WorkPackageSubjectComponent extends UntilDestroyedMixin { @Input('workPackage') workPackage:WorkPackageResource; public readonly uniqueElementIdentifier = `work-packages--subject-type-row-${randomString(16)}`; - constructor(protected uiRouterGlobals:UIRouterGlobals, - protected apiV3Service:ApiV3Service) { + constructor( + protected uiRouterGlobals:UIRouterGlobals, + protected apiV3Service:ApiV3Service, + ) { super(); } - - ngOnInit() { - if (!this.workPackage) { - this - .apiV3Service - .work_packages - .id(this.uiRouterGlobals.params.workPackageId) - .requireAndStream() - .pipe( - this.untilDestroyed(), - ) - .subscribe((wp:WorkPackageResource) => { - this.workPackage = wp; - }); - } - } } diff --git a/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/tab-entry.component.ts b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/tab-entry.component.ts new file mode 100644 index 000000000000..f359cc488686 --- /dev/null +++ b/frontend/src/app/features/work-packages/components/wp-table/configuration-modal/tabs/tab-entry.component.ts @@ -0,0 +1,75 @@ +//-- 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, + Injector, + Input, + OnInit, + Type, +} from '@angular/core'; +import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; +import { WorkPackageIsolatedQuerySpaceDirective } from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { WpTableConfigurationService } from 'core-app/features/work-packages/components/wp-table/configuration-modal/wp-table-configuration.service'; +import { TabInterface } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tab-portal-outlet'; + +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + `, + providers: [WpTableConfigurationService], +}) +export class WpTableConfigurationTabEntryComponent implements OnInit { + @Input() showActionBar? = false; + @Input() showHeaderText = true; + @Input() tabId:string; + + tabs:TabInterface[]; + component:Type|null; + + constructor( + readonly elementRef:ElementRef, + readonly injector:Injector, + private readonly wpTableConfiguration:WpTableConfigurationService, + ) { + populateInputsFromDataset(this); + } + + ngOnInit():void { + this.tabs = this.wpTableConfiguration.tabs; + const currentTab = this.tabs.find((tab) => tab.id === this.tabId); + this.component = currentTab ? currentTab.componentClass : null; + } +} diff --git a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts index 37b9593e0c40..37bf986060f7 100644 --- a/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-table/embedded/wp-embedded-table.component.ts @@ -196,7 +196,7 @@ export class WorkPackageEmbeddedTableComponent extends WorkPackageEmbeddedBaseCo if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } } diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts index 875cfe40db36..04eae9c08e6a 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component.ts @@ -29,6 +29,7 @@ import { UIRouterGlobals } from '@uirouter/core'; import { Component, + Input, OnInit, } from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; @@ -44,6 +45,9 @@ import { WorkPackageResource } from 'core-app/features/hal/resources/work-packag selector: 'op-wp-tab', }) export class WpTabWrapperComponent implements OnInit { + @Input() public workPackageId:string; + @Input() public tabIdentifier:string; + workPackage:WorkPackageResource; ndcDynamicInputs$:Observable<{ @@ -51,17 +55,19 @@ 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() { + const { workPackageId } = this.uiRouterGlobals.params as unknown as { workPackageId:string }; + const { tabIdentifier } = this.uiRouterGlobals.params as unknown as { tabIdentifier:string }; + this.workPackageId = (workPackageId || this.workPackageId); + this.tabIdentifier = (tabIdentifier || this.tabIdentifier); + this.ndcDynamicInputs$ = this .apiV3Service .work_packages @@ -76,8 +82,6 @@ export class WpTabWrapperComponent implements OnInit { } findTab(workPackage:WorkPackageResource):WpTabDefinition | undefined { - const { tabIdentifier } = this.uiRouterGlobals.params as unknown as { tabIdentifier:string }; - - return this.wpTabsService.getTab(tabIdentifier, workPackage); + return this.wpTabsService.getTab(this.tabIdentifier, workPackage); } } diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html index 6a0f82d5b4cd..08992e031735 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.html @@ -1,5 +1,6 @@ diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts index e43f8fba14af..83614250b46a 100644 --- a/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-tabs/components/wp-tabs/wp-tabs.component.ts @@ -9,6 +9,8 @@ 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'; @Component({ selector: 'op-wp-tabs', @@ -21,9 +23,11 @@ export class WpTabsComponent implements OnInit { @Input() view:'full'|'split'; - public tabs:TabDefinition[]; + @Input() routedFromAngular:boolean = true; + + @Input() public currentTabId:string|null = null; - public uiSrefBase:string; + public tabs:TabDefinition[]; public canViewWatchers = false; @@ -41,11 +45,12 @@ 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(); } @@ -54,15 +59,24 @@ export class WpTabsComponent implements OnInit { return this .wpTabsService .getDisplayableTabs(this.workPackage) - .map((tab) => ({ - ...tab, - route: `${this.uiSrefBase}.tabs`, - routeParams: { workPackageId: this.workPackage.id, tabIdentifier: tab.id }, - })); + .map((tab) => { + if (this.routedFromAngular) { + return ({ + ...tab, + route: '.tabs', + routeParams: { workPackageId: this.workPackage.id, tabIdentifier: tab.id }, + }); + } + + return ({ + ...tab, + path: this.pathHelper.genericWorkPackagePath(this.currentProject.identifier, this.workPackage.id!, tab.id), + }); + }); } public switchToFullscreen():void { - this.keepTab.goCurrentShowState(); + this.keepTab.goCurrentShowState(this.workPackage.id!); } public close():void { diff --git a/frontend/src/app/features/work-packages/openproject-work-packages.module.ts b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts index 03f4398e73d5..98e087601310 100644 --- a/frontend/src/app/features/work-packages/openproject-work-packages.module.ts +++ b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts @@ -310,9 +310,6 @@ import { import { RevisionActivityComponent, } from 'core-app/features/work-packages/components/wp-activity/revision/revision-activity.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'; @@ -416,6 +413,15 @@ import { import { WorkPackageSplitViewEntryComponent, } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component'; +import { + WorkPackagePrimerizedListViewComponent, +} from 'core-app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component'; +import { WorkPackageSplitCreateEntryComponent } from 'core-app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component'; +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 { OpBaselineEntryComponent } from 'core-app/features/work-packages/components/wp-baseline/baseline/baseline-entry.component'; +import { WpTableConfigurationTabEntryComponent } from 'core-app/features/work-packages/components/wp-table/configuration-modal/tabs/tab-entry.component'; @NgModule({ imports: [ @@ -501,7 +507,6 @@ import { // WP Copy WorkPackageCopyFullViewComponent, - WorkPackageCopySplitViewComponent, // Embedded table WorkPackageEmbeddedTableComponent, @@ -609,6 +614,7 @@ import { WorkPackageDetailsViewButtonComponent, WorkPackageSplitViewComponent, WorkPackageSplitViewEntryComponent, + WorkPackageSplitCreateEntryComponent, WorkPackageBreadcrumbComponent, WorkPackageSplitViewToolbarComponent, WorkPackageWatcherButtonComponent, @@ -617,6 +623,9 @@ import { // Full view WorkPackagesFullViewComponent, + WorkPackageFullViewEntryComponent, + WorkPackageFullCopyEntryComponent, + WorkPackageFullCreateEntryComponent, // Modals WpTableConfigurationModalComponent, @@ -626,6 +635,7 @@ import { WpTableConfigurationSortByTabComponent, WpTableConfigurationTimelinesTabComponent, WpTableConfigurationHighlightingTabComponent, + WpTableConfigurationTabEntryComponent, WpTableConfigurationRelationSelectorComponent, QuerySharingFormComponent, QuerySharingModalComponent, @@ -653,8 +663,12 @@ import { // Timestamps OpBaselineModalComponent, OpBaselineComponent, + OpBaselineEntryComponent, OpBaselineLoadingComponent, OpBaselineLegendsComponent, + + // Primerized work packages list + WorkPackagePrimerizedListViewComponent, ], exports: [ WorkPackagesTableComponent, diff --git a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts index 317c477e9375..3bdc8063325d 100644 --- a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts +++ b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.ts @@ -26,30 +26,32 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { - ChangeDetectionStrategy, - Component, - OnDestroy, - OnInit, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { QueryResource } from 'core-app/features/hal/resources/query-resource'; import { OpTitleService } from 'core-app/core/html/op-title.service'; import { WorkPackagesViewBase } from 'core-app/features/work-packages/routing/wp-view-base/work-packages-view.base'; import { take } from 'rxjs/operators'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; -import { WorkPackageNotificationService } from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; -import { QueryParamListenerService } from 'core-app/features/work-packages/components/wp-query/query-param-listener.service'; +import { + WorkPackageNotificationService, +} from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; +import { + QueryParamListenerService, +} from 'core-app/features/work-packages/components/wp-query/query-param-listener.service'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; import { ComponentType } from '@angular/cdk/overlay'; import { Ng2StateDeclaration } from '@uirouter/angular'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { OpModalService } from 'core-app/shared/components/modal/modal.service'; import { InviteUserModalComponent } from 'core-app/features/invite-user-modal/invite-user.component'; -import { WorkPackageFilterContainerComponent } from 'core-app/features/work-packages/components/filters/filter-container/filter-container.directive'; +import { + WorkPackageFilterContainerComponent, +} from 'core-app/features/work-packages/components/filters/filter-container/filter-container.directive'; import isPersistedResource from 'core-app/features/hal/helpers/is-persisted-resource'; import { UIRouterGlobals } from '@uirouter/core'; import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { firstValueFrom } from 'rxjs'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; export interface DynamicComponentDefinition { component:ComponentType; @@ -87,6 +89,8 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp @InjectField() configuration:ConfigurationService; + @InjectField() pathHelper:PathHelperService; + text:{ [key:string]:string } = { 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'), diff --git a/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts b/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts index de7769300324..0cccd336bc25 100644 --- a/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts +++ b/frontend/src/app/features/work-packages/routing/split-view-routes.helper.ts @@ -35,6 +35,6 @@ import { StateService } from '@uirouter/angular'; */ export function splitViewRoute(state:StateService, target:'details'|'new' = 'details'):string { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment - const baseRoute:string = state.current.data.baseRoute || ''; + const baseRoute:string = state?.current?.data?.baseRoute || ''; return `${baseRoute}.${target}`; } diff --git a/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts b/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts index 83c9b26cb378..81e3d5c2efdf 100644 --- a/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts +++ b/frontend/src/app/features/work-packages/routing/split-view-routes.template.ts @@ -30,7 +30,6 @@ import { WorkPackageNewSplitViewComponent } from 'core-app/features/work-package import { Ng2StateDeclaration } from '@uirouter/angular'; import { ComponentType } from '@angular/cdk/overlay'; import { WpTabWrapperComponent } from 'core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component'; -import { WorkPackageCopySplitViewComponent } from 'core-app/features/work-packages/components/wp-copy/wp-copy-split-view.component'; /** * Return a set of routes for a split view mounted under the given base route, @@ -125,23 +124,5 @@ export function makeSplitViewRoutes(baseRoute:string, 'content-right@^.^': { component: newComponent }, }, }, - // Split copy route - { - name: `${routeName}.copy`, - url: '/details/{copiedFromWorkPackageId:[0-9]+}/copy', - views: { - 'content-right@^.^': { component: WorkPackageCopySplitViewComponent }, - }, - reloadOnSearch: false, - data: { - baseRoute, - parent: baseRoute, - allowMovingInEditMode: true, - bodyClasses: 'router--work-packages-partitioned-split-view', - menuItem: menuItemClass, - partition: '-split', - mobileAlternative: showMobileAlternative ? 'work-packages.show' : undefined, - }, - }, ]; } diff --git a/frontend/src/app/features/work-packages/routing/work-packages-routes.ts b/frontend/src/app/features/work-packages/routing/work-packages-routes.ts index 1d32cd86c837..f9e952f5bb56 100644 --- a/frontend/src/app/features/work-packages/routing/work-packages-routes.ts +++ b/frontend/src/app/features/work-packages/routing/work-packages-routes.ts @@ -27,7 +27,6 @@ //++ import { WpTabWrapperComponent } from 'core-app/features/work-packages/components/wp-tabs/components/wp-tab-wrapper/wp-tab-wrapper.component'; -import { WorkPackageNewFullViewComponent } from 'core-app/features/work-packages/components/wp-new/wp-new-full-view.component'; import { WorkPackagesFullViewComponent } from 'core-app/features/work-packages/routing/wp-full-view/wp-full-view.component'; import { WorkPackageSplitViewComponent } from 'core-app/features/work-packages/routing/wp-split-view/wp-split-view.component'; import { Ng2StateDeclaration } from '@uirouter/angular'; @@ -47,126 +46,94 @@ export const sideMenuOptions = { }; export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [ - { - name: 'work-packages', - parent: 'optional_project', - url: '/work_packages?query_id&query_props&name&start_onboarding_tour', - redirectTo: 'work-packages.partitioned.list', - views: { - '!$default': { component: WorkPackagesBaseComponent }, - }, - data: { - bodyClasses: 'router--work-packages-base', - menuItem: menuItemClass, - sideMenuOptions, - }, - params: { - query_id: { type: 'query', dynamic: true }, - // Use custom encoder/decoder that ensures validity of URL string - query_props: { type: 'opQueryString' }, - // Optional initial tour param - start_onboarding_tour: { type: 'query', squash: true, value: undefined }, - 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, - url: '', - data: { - // This has to be empty to avoid inheriting the parent bodyClasses - bodyClasses: '', - sideMenuOptions, - }, - }, - { - name: 'work-packages.partitioned.list', - url: '', - reloadOnSearch: false, - views: { - 'content-left': { component: WorkPackageListViewComponent }, - }, - data: { - bodyClasses: ['router--work-packages-partitioned-split-view', 'router--work-packages-base'], - menuItem: menuItemClass, - partition: '-left-only', - sideMenuOptions, - }, - }, - ...makeSplitViewRoutes( - 'work-packages.partitioned.list', - menuItemClass, - WorkPackageSplitViewComponent, - ), - { - url: '/share_upsale', - name: 'work-packages.share_upsale', - component: ShareUpsaleComponent, - }, + // { + // name: 'work-packages', + // parent: 'optional_project', + // url: '/work_packages?query_id&query_props&name&start_onboarding_tour', + // redirectTo: 'work-packages.partitioned.list', + // views: { + // '!$default': { component: WorkPackagesBaseComponent }, + // }, + // data: { + // bodyClasses: 'router--work-packages-base', + // menuItem: menuItemClass, + // sideMenuOptions, + // }, + // params: { + // query_id: { type: 'query', dynamic: true }, + // // Use custom encoder/decoder that ensures validity of URL string + // query_props: { type: 'opQueryString' }, + // // Optional initial tour param + // start_onboarding_tour: { type: 'query', squash: true, value: undefined }, + // name: { type: 'string', dynamic: true }, + // }, + // }, + // { + // 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, + // url: '', + // data: { + // // This has to be empty to avoid inheriting the parent bodyClasses + // bodyClasses: '', + // sideMenuOptions, + // }, + // }, + // { + // name: 'work-packages.partitioned.list', + // url: '', + // reloadOnSearch: false, + // views: { + // 'content-left': { component: WorkPackageListViewComponent }, + // }, + // data: { + // bodyClasses: ['router--work-packages-partitioned-split-view', 'router--work-packages-base'], + // menuItem: menuItemClass, + // partition: '-left-only', + // sideMenuOptions, + // }, + // }, + // ...makeSplitViewRoutes( + // 'work-packages.partitioned.list', + // menuItemClass, + // WorkPackageSplitViewComponent, + // ), + // { + // url: '/share_upsale', + // name: 'work-packages.share_upsale', + // component: ShareUpsaleComponent, + // }, // Avoid lazy-loading the routes for now // { // name: 'work-packages.calendar.**', diff --git a/frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts new file mode 100644 index 000000000000..f2fe9a7e7f2c --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts @@ -0,0 +1,61 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; + +/** + * An entry component to be rendered by Rails which opens an isolated query space + * for the work package split view + */ +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkPackageFullCopyEntryComponent { + @Input() type:string; + @Input() copiedFromWorkPackageId:string; + @Input() parentId?:string; + @Input() projectIdentifier?:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-full-create'); + } +} diff --git a/frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts new file mode 100644 index 000000000000..55bef996ecad --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts @@ -0,0 +1,60 @@ +//-- 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], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkPackageFullCreateEntryComponent { + @Input() type:string; + @Input() parentId?:string; + @Input() projectIdentifier?:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-full-create'); + } +} diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts new file mode 100644 index 000000000000..a4be33db05d0 --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts @@ -0,0 +1,60 @@ +//-- 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 full view + */ +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkPackageFullViewEntryComponent { + @Input() workPackageId:string; + @Input() activeTab:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-full-view', 'router--work-packages-base'); + } +} diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts index a6fa95583355..968b2ee66bf0 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.component.ts @@ -32,9 +32,10 @@ import { Component, HostListener, Injector, + Input, OnInit, } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Observable } from 'rxjs'; import { WorkPackageViewSelectionService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-selection.service'; import { WorkPackageSingleViewBase } from 'core-app/features/work-packages/routing/wp-view-base/work-package-single-view.base'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; @@ -42,12 +43,11 @@ import { WorkPackageNotificationService } from 'core-app/features/work-packages/ import { WpSingleViewService } from 'core-app/features/work-packages/routing/wp-view-base/state/wp-single-view.service'; import { CommentService } from 'core-app/features/work-packages/components/wp-activity/comment-service'; import { RecentItemsService } from 'core-app/core/recent-items.service'; -import { ConfigurationService } from 'core-app/core/config/configuration.service'; import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; @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: [ @@ -57,6 +57,8 @@ import { CurrentUserService } from 'core-app/core/current-user/current-user.serv ], }) export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase implements OnInit { + @Input() routedFromAngular:boolean = true; + // Watcher properties public isWatched:boolean; @@ -74,17 +76,14 @@ export class WorkPackagesFullViewComponent extends WorkPackageSingleViewBase imp }, }; - stateName$ = of('work-packages.new'); - constructor( public injector:Injector, public wpTableSelection:WorkPackageViewSelectionService, public recentItemsService:RecentItemsService, readonly $state:StateService, readonly currentUserService:CurrentUserService, - private readonly configurationService:ConfigurationService, ) { - super(injector, $state.params.workPackageId); + super(injector); } // enable other parts of the application to trigger an immediate update diff --git a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html index a364194a4c85..cee90bfa75d8 100644 --- a/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view.html @@ -16,14 +16,14 @@
    - +
    • + [routedFromAngular]="routedFromAngular" + >
    • - + +
      + data-notification-selector='notification-scroll-container'> +
      diff --git a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.html b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.html index 12bb25353f7a..de93b45aed7f 100644 --- a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.html +++ b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.html @@ -1,7 +1,7 @@ -
      - - + [ngClass]="{ '-with-resizer': showResizerInCardView() }"> + +
      \ No newline at end of file + + diff --git a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts index 994f41786bec..ab89990745ef 100644 --- a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-list-view.component.ts @@ -30,10 +30,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - Injector, - OnInit, ElementRef, + Injector, NgZone, + OnInit, } from '@angular/core'; import { take } from 'rxjs/operators'; import { CausedUpdatesService } from 'core-app/features/boards/board/caused-updates/caused-updates.service'; @@ -42,18 +42,26 @@ import { WorkPackageViewDisplayRepresentationService, wpDisplayCardRepresentation, } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-display-representation.service'; -import { WorkPackageTableConfigurationObject } from 'core-app/features/work-packages/components/wp-table/wp-table-configuration'; +import { + WorkPackageTableConfigurationObject, +} from 'core-app/features/work-packages/components/wp-table/wp-table-configuration'; import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.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 { I18nService } from 'core-app/core/i18n/i18n.service'; import { IsolatedQuerySpace } from 'core-app/features/work-packages/directives/query-space/isolated-query-space'; import { DeviceService } from 'core-app/core/browser/device.service'; import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; -import { WorkPackageViewFiltersService } from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service'; +import { + WorkPackageViewFiltersService, +} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { QueryResource } from 'core-app/features/hal/resources/query-resource'; import { StateService } from '@uirouter/core'; -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 { WorkPackageViewBaselineService } from '../wp-view-base/view-services/wp-view-baseline.service'; import { combineLatest } from 'rxjs'; @@ -90,6 +98,9 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements public baselineEnabled:boolean; + workPackageTableClass = ''; + listClsas = ''; + /** */ readonly wpTableConfiguration:WorkPackageTableConfigurationObject = { dragAndDropEnabled: true, @@ -191,7 +202,7 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements if (event.requestedState === 'split') { this.keepTab.goCurrentDetailsState(params); } else { - this.keepTab.goCurrentShowState(params); + this.keepTab.goCurrentShowState(params.workPackageId); } } @@ -207,7 +218,7 @@ export class WorkPackageListViewComponent extends UntilDestroyedMixin implements } } - private openInFullView(workPackageId:string) { + openInFullView(workPackageId:string) { this.$state.go( 'work-packages.show', { workPackageId }, diff --git a/frontend/src/app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component.ts new file mode 100644 index 000000000000..742150d6db7d --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component.ts @@ -0,0 +1,127 @@ +//-- 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, OnInit } from '@angular/core'; +import { CausedUpdatesService } from 'core-app/features/boards/board/caused-updates/caused-updates.service'; +import { DragAndDropService } from 'core-app/shared/helpers/drag-and-drop/drag-and-drop.service'; +import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; +import { + WorkPackageNotificationService, +} from 'core-app/features/work-packages/services/notifications/work-package-notification.service'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { + WorkPackageListViewComponent, +} from 'core-app/features/work-packages/routing/wp-list-view/wp-list-view.component'; +import { LoadingIndicatorService } from 'core-app/core/loading-indicator/loading-indicator.service'; +import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; +import { QueryResource } from 'core-app/features/hal/resources/query-resource'; +import { firstValueFrom } from 'rxjs'; +import { WorkPackagesListService } from 'core-app/features/work-packages/components/wp-list/wp-list.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; + +@Component({ + templateUrl: './wp-list-view.component.html', + styleUrls: ['./wp-list-view.component.sass'], + host: { class: 'op-primerized-work-packages-list work-packages-split-view--tabletimeline-side' }, + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { provide: HalResourceNotificationService, useClass: WorkPackageNotificationService }, + DragAndDropService, + CausedUpdatesService, + ], +}) +export class WorkPackagePrimerizedListViewComponent extends WorkPackageListViewComponent implements OnInit { + @InjectField() loadingIndicatorService:LoadingIndicatorService; + @InjectField() wpListService:WorkPackagesListService; + @InjectField() currentProject:CurrentProjectService; + @InjectField() pathHelper:PathHelperService; + + currentQuery:QueryResource|undefined; + + ngOnInit() { + this.loadInitialQuery(); + document.body.classList.add('router--work-packages-base'); + super.ngOnInit(); + } + + openStateLink(event:{ workPackageId:string; requestedState:'show'|'split' }) { + if (event.requestedState === 'split') { + this.openSplitScreen(event.workPackageId, this.keepTab.currentTabIdentifier); + } else { + this.openInFullView(event.workPackageId); + } + } + + openSplitScreen(workPackageId:string, tabIdentifier:string = 'overview'):void { + let link = this.pathHelper.workPackagePrimerDetailsPath(this.currentProject.identifier, workPackageId, tabIdentifier); + Turbo.visit(link + window.location.search, { frame: 'content-bodyRight', action: 'advance' }); + } + + openInFullView(workPackageId:string) { + window.location.href = this.pathHelper.workPackagePath(workPackageId); + } + + protected loadInitialQuery():void { + const isFirstLoad = !this.querySpace.initialized.hasValue(); + this.loadingIndicator = this.loadQuery(isFirstLoad); + } + + protected set loadingIndicator(promise:Promise) { + this.loadingIndicatorService.list.promise = promise; + } + + protected loadQuery(firstPage = false):Promise { + let promise:Promise; + const query = this.currentQuery; + + if (firstPage || !query) { + promise = this.loadFirstPage(); + } else { + const pagination = this.wpListService.getPaginationInfo(); + promise = firstValueFrom(this.wpListService.loadQueryFromExisting(query, pagination, this.projectIdentifier)); + } + + return promise; + } + + protected loadFirstPage():Promise { + if (this.currentQuery) { + return firstValueFrom(this.wpListService.reloadQuery(this.currentQuery, this.projectIdentifier)); + } + return this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier); + } + + public get projectIdentifier() { + return this.currentProject.identifier || undefined; + } + +} diff --git a/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts new file mode 100644 index 000000000000..53fb9cfee7fa --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts @@ -0,0 +1,62 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See COPYRIGHT and LICENSE files for more details. +//++ + +import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; +import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; + +/** + * An entry component to be rendered by Rails which opens an isolated query space + * for the work package split view + */ +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class WorkPackageSplitCreateEntryComponent { + @Input() type:string; + @Input() parentId?:string; + @Input() projectIdentifier?:string; + @Input() resizerClass:string; + @Input() routedFromAngular:boolean; + + constructor(readonly elementRef:ElementRef) { + populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-partitioned-split-view-new'); + } +} diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts index 3ed93189d8da..74567c1e3568 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view-entry.component.ts @@ -55,5 +55,7 @@ export class WorkPackageSplitViewEntryComponent { constructor(readonly elementRef:ElementRef) { populateInputsFromDataset(this); + + document.body.classList.add('router--work-packages-partitioned-split-view-details'); } } diff --git a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts index 9bb6d9ad11c2..885e6439fc6c 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-view/wp-split-view.component.ts @@ -71,9 +71,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'; @@ -90,7 +88,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 @@ -102,22 +100,22 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp } ngOnInit():void { + this.workPackageId = (this.$state.params.workPackageId || this.workPackageId) as string; 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)); + const isSameID = this.firstRoute.params && this.workPackageId === this.firstRoute.params.workPackageI; + this.wpTableFocus.updateFocus(this.workPackageId, (isFirstRoute && isSameID)); } else { - this.wpTableFocus.updateFocus(wpId, false); + this.wpTableFocus.updateFocus(this.workPackageId, false); } if (this.wpTableSelection.isEmpty) { - this.wpTableSelection.setRowState(wpId, true); + this.wpTableSelection.setRowState(this.workPackageId, true); } this.wpTableFocus.whenChanged() @@ -125,7 +123,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp this.untilDestroyed(), ) .subscribe((newId) => { - const idSame = wpId.toString() === newId.toString(); + const idSame = this.workPackageId.toString() === newId.toString(); if (!idSame && this.$state.includes(`${this.baseRoute}.details`)) { this.$state.go( (this.$state.current.name as string), @@ -133,7 +131,7 @@ export class WorkPackageSplitViewComponent extends WorkPackageSingleViewBase imp ); } }); - this.recentItemsService.add(wpId); + this.recentItemsService.add(this.workPackageId); } get shouldFocus():boolean { diff --git a/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts b/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts index 89851497d456..083e9bc1d4b7 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-base/work-package-single-view.base.ts @@ -26,7 +26,12 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { ChangeDetectorRef, Injector } from '@angular/core'; +import { + ChangeDetectorRef, + Directive, + Injector, + Input, +} from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import { @@ -63,7 +68,13 @@ 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'; -export class WorkPackageSingleViewBase extends UntilDestroyedMixin { +@Directive() +export abstract class WorkPackageSingleViewBase extends UntilDestroyedMixin { + // TODO: Get variable from angular routes + @Input() workPackageId:string; + + @Input() activeTab:string = 'activity'; + @InjectField() states:States; @InjectField() i18n:I18nService; @@ -115,7 +126,6 @@ export class WorkPackageSingleViewBase extends UntilDestroyedMixin { constructor( public injector:Injector, - protected workPackageId:string, ) { super(); } diff --git a/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts b/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts index b39d87b18526..c077acc253a0 100644 --- a/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-view-page/wp-view-page.component.ts @@ -43,8 +43,8 @@ import { ZenModeButtonComponent } from 'core-app/features/work-packages/componen import { WorkPackageSettingsButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-settings-button/wp-settings-button.component'; import { of } from 'rxjs'; import { WorkPackageFoldToggleButtonComponent } from 'core-app/features/work-packages/components/wp-buttons/wp-fold-toggle-button/wp-fold-toggle-button.component'; -import { OpProjectIncludeComponent } from 'core-app/shared/components/project-include/project-include.component'; import { OpBaselineModalComponent } from 'core-app/features/work-packages/components/wp-baseline/baseline-modal/baseline-modal.component'; +import { OpProjectIncludeModalComponent } from 'core-app/shared/components/project-include/project-include-modal.component'; @Component({ selector: 'wp-view-page', @@ -66,11 +66,10 @@ export class WorkPackageViewPageComponent extends PartitionedQuerySpacePageCompo component: WorkPackageCreateButtonComponent, inputs: { stateName$: of(this.stateName), - allowed: ['work_packages.createWorkPackage'], }, }, { - component: OpProjectIncludeComponent, + component: OpProjectIncludeModalComponent, }, { component: OpBaselineModalComponent, diff --git a/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts b/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts index 5e4aadf7f185..dba82f99d5fc 100644 --- a/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts +++ b/frontend/src/app/features/work-packages/services/work-package-authorization.service.ts @@ -82,7 +82,7 @@ export class WorkPackageAuthorization { if (stateName.indexOf('work-packages.partitioned.list.details') === 0) { return this.PathHelper.workPackageDetailsCopyPath(this.project.identifier, this.workPackage.id as string); } - return this.PathHelper.workPackageCopyPath(this.workPackage.id as string); + return this.PathHelper.workPackageCopyPath(this.project.identifier, this.workPackage.id as string); } private shortLink() { diff --git a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts index 1ab1c700e0f4..acc3ec2f09a0 100644 --- a/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/handlers/op-types-context-menu.directive.ts @@ -29,17 +29,18 @@ 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 { 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'; + 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 { 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'; @Directive({ selector: '[opTypesCreateDropdown]', @@ -51,14 +52,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); } @@ -69,11 +73,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) { @@ -112,12 +111,20 @@ 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 { + const link = new URL(`${window.location.origin}${this.pathHelper.projectWorkPackageNewPath(this.currentProject.id!)}`); + if (type.id) { + link.searchParams.set('type', type.id); + } + window.location.href = link.href; + } return true; }, })); diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts index 2dfd8da37984..407613fb1a98 100644 --- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts +++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-single-context-menu.ts @@ -62,7 +62,9 @@ export class WorkPackageSingleContextMenuDirective extends OpContextMenuTrigger break; case 'copy': - this.$state.go('work-packages.copy', { copiedFromWorkPackageId: this.workPackage.id }); + if (this.workPackage.id) { + window.location.href = `${this.PathHelper.workPackageCopyPath(this.workPackage.project.identifier, this.workPackage.id)}`; + } break; case 'delete': this.opModalService.show(WpDestroyModalComponent, this.injector, { workPackages: [this.workPackage] }); diff --git a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts index 51357461b0c0..d5ff1fa0d82f 100644 --- a/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts +++ b/frontend/src/app/shared/components/op-context-menu/wp-context-menu/wp-view-context-menu.directive.ts @@ -98,7 +98,7 @@ export class WorkPackageViewContextMenu extends OpContextMenuHandler { this.editSelectedWorkPackages(link!); break; - case 'copy': + case 'duplicate': this.copySelectedWorkPackages(link!); break; @@ -152,11 +152,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() { diff --git a/frontend/src/app/shared/components/project-include/project-include-entry.component.ts b/frontend/src/app/shared/components/project-include/project-include-entry.component.ts new file mode 100644 index 000000000000..233ea34b5101 --- /dev/null +++ b/frontend/src/app/shared/components/project-include/project-include-entry.component.ts @@ -0,0 +1,57 @@ +//-- 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 { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; +import { WorkPackageIsolatedQuerySpaceDirective } from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; + +@Component({ + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OpIncludeProjectsEntryComponent { + @Input() showActionBar = false; + @Input() showHeaderText = true; + + constructor( + readonly elementRef:ElementRef, + ) { + populateInputsFromDataset(this); + } +} diff --git a/frontend/src/app/shared/components/project-include/project-include-modal.component.html b/frontend/src/app/shared/components/project-include/project-include-modal.component.html new file mode 100644 index 000000000000..5ba70860f462 --- /dev/null +++ b/frontend/src/app/shared/components/project-include/project-include-modal.component.html @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/app/shared/components/project-include/project-include-modal.component.ts b/frontend/src/app/shared/components/project-include/project-include-modal.component.ts new file mode 100644 index 000000000000..7c2d31dd2f50 --- /dev/null +++ b/frontend/src/app/shared/components/project-include/project-include-modal.component.ts @@ -0,0 +1,97 @@ +//-- 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, + HostBinding, + Input, + OnInit, +} from '@angular/core'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, + filter, + map, + mergeMap, + shareReplay, + take, +} from 'rxjs/operators'; + +import { I18nService } from 'core-app/core/i18n/i18n.service'; +import { HalResource } from 'core-app/features/hal/resources/hal-resource'; +import { + WorkPackageViewFiltersService, +} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-filters.service'; +import { + WorkPackageViewIncludeSubprojectsService, +} from 'core-app/features/work-packages/routing/wp-view-base/view-services/wp-view-include-subprojects.service'; +import { QueryFilterInstanceResource } from 'core-app/features/hal/resources/query-filter-instance-resource'; +import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; +import { HalResourceService } from 'core-app/features/hal/services/hal-resource.service'; +import { CurrentProjectService } from 'core-app/core/current-project/current-project.service'; +import { IProject } from 'core-app/core/state/projects/project.model'; +import { + SearchableProjectListService, +} from 'core-app/shared/components/searchable-project-list/searchable-project-list.service'; +import { IProjectData } from 'core-app/shared/components/searchable-project-list/project-data'; + +import { insertInList } from './insert-in-list'; +import { recursiveSort } from './recursive-sort'; +import { calculatePositions } from 'core-app/shared/components/project-include/calculate-positions'; + +@Component({ + selector: 'op-project-include-modal', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './project-include-modal.component.html', + providers: [ + SearchableProjectListService, + ], +}) +export class OpProjectIncludeModalComponent { + @HostBinding('class.op-project-include') className = true; + + @Input() public count?:number; + + public text = { + toggle_title: this.I18n.t('js.include_projects.toggle_title'), + title: this.I18n.t('js.include_projects.title'), + }; + + public opened = false; + + constructor( + readonly I18n:I18nService, + ) { + } + + public toggleOpen():void { + this.opened = !this.opened; + } +} diff --git a/frontend/src/app/shared/components/project-include/project-include.component.html b/frontend/src/app/shared/components/project-include/project-include.component.html index ba29d1e0eaf7..f247d915b21f 100644 --- a/frontend/src/app/shared/components/project-include/project-include.component.html +++ b/frontend/src/app/shared/components/project-include/project-include.component.html @@ -1,116 +1,90 @@ - +

      {{ text.title }}

      + + +
      - + slot="before" + class="spot-icon spot-icon_search" + > + - -
      -

      {{ text.title }}

      - -
      - - - - - - -
        + +
          - - - {{text.no_results}} - - -
          - -
          -
          - -
          -
          - - -
          -
          - + + + {{text.no_results}} + +
          +
          +
          + +
          +
          + + +
          +
          + + -
          diff --git a/frontend/src/app/shared/components/project-include/project-include.component.ts b/frontend/src/app/shared/components/project-include/project-include.component.ts index 6a3b9f1d4fa3..48db05c190de 100644 --- a/frontend/src/app/shared/components/project-include/project-include.component.ts +++ b/frontend/src/app/shared/components/project-include/project-include.component.ts @@ -30,6 +30,7 @@ import { ChangeDetectionStrategy, Component, HostBinding, + Input, OnInit, } from '@angular/core'; import { BehaviorSubject, combineLatest } from 'rxjs'; @@ -77,8 +78,11 @@ import { calculatePositions } from 'core-app/shared/components/project-include/c export class OpProjectIncludeComponent extends UntilDestroyedMixin implements OnInit { @HostBinding('class.op-project-include') className = true; + @Input() public showActionBar = true; + + @Input() public showHeaderText = true; + public text = { - toggle_title: this.I18n.t('js.include_projects.toggle_title'), title: this.I18n.t('js.include_projects.title'), filter_all: this.I18n.t('js.include_projects.selected_filter.all'), filter_selected: this.I18n.t('js.include_projects.selected_filter.selected'), @@ -89,8 +93,6 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin implements On no_results: this.I18n.t('js.include_projects.no_results'), }; - public opened = false; - public textFieldFocused = false; public query$ = this.wpTableFilters.querySpace.query.values$(); @@ -158,8 +160,6 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin implements On }), ); - public numberOfProjectsInFilter$ = this.projectsInFilter$.pipe(map((selected) => selected.length)); - public projects$ = combineLatest([ this.searchableProjectListService.allProjects$, this.displayMode$.pipe(distinctUntilChanged()), @@ -299,29 +299,23 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin implements On .subscribe((includeSubprojects) => { this.includeSubprojects = includeSubprojects; }); + + this.searchableProjectListService.loadAllProjects(); + this.projectsInFilter$ + .pipe( + take(1), + ) + .subscribe((selectedProjects) => { + this.displayMode = 'all'; + this.searchableProjectListService.searchText = ''; + this.selectedProjects = selectedProjects as string[]; + }); } public toggleIncludeSubprojects():void { this.wpIncludeSubprojects.setIncludeSubprojects(!this.wpIncludeSubprojects.current); } - public toggleOpen():void { - this.opened = !this.opened; - - if (this.opened) { - this.searchableProjectListService.loadAllProjects(); - this.projectsInFilter$ - .pipe( - take(1), - ) - .subscribe((selectedProjects) => { - this.displayMode = 'all'; - this.searchableProjectListService.searchText = ''; - this.selectedProjects = selectedProjects as string[]; - }); - } - } - public clearSelection():void { this.selectedProjects = [this.currentProjectService.apiv3Path || '']; } @@ -337,11 +331,5 @@ export class OpProjectIncludeComponent extends UntilDestroyedMixin implements On }); this.wpIncludeSubprojects.update(this.includeSubprojects); - - this.close(); - } - - public close():void { - this.opened = false; } } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 00be06fa2270..b2cde2a05c8f 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -100,6 +100,8 @@ import { OpenprojectModalModule } from 'core-app/shared/components/modal/modal.m import { FullCalendarModule } from '@fullcalendar/angular'; import { OpDatePickerModule } from 'core-app/shared/components/datepicker/datepicker.module'; import { ShareUpsaleComponent } from 'core-app/features/enterprise/share-upsale/share-upsale.component'; +import { OpProjectIncludeModalComponent } from 'core-app/shared/components/project-include/project-include-modal.component'; +import { OpIncludeProjectsEntryComponent } from 'core-app/shared/components/project-include/project-include-entry.component'; export function bootstrapModule(injector:Injector):void { // Ensure error reporter is run @@ -193,7 +195,7 @@ export function bootstrapModule(injector:Injector):void { DynamicModule, OpOptionListComponent, - OpProjectIncludeComponent, + OpProjectIncludeModalComponent, OpProjectIncludeListComponent, OpLoadingProjectListComponent, @@ -245,6 +247,8 @@ export function bootstrapModule(injector:Injector):void { HomescreenNewFeaturesBlockComponent, OpOptionListComponent, + OpIncludeProjectsEntryComponent, + OpProjectIncludeModalComponent, OpProjectIncludeComponent, OpProjectIncludeListComponent, OpLoadingProjectListComponent, diff --git a/frontend/src/global_styles/layout/work_packages/_details_view.sass b/frontend/src/global_styles/layout/work_packages/_details_view.sass index bd2e9c6871b6..57d36b9c390a 100644 --- a/frontend/src/global_styles/layout/work_packages/_details_view.sass +++ b/frontend/src/global_styles/layout/work_packages/_details_view.sass @@ -148,5 +148,5 @@ body.router--work-packages-partitioned-split-view-new @media #{$spot-mq-mobile} grid-template-columns: 1fr - .work-packages-partitioned-page--content-right & + .work-packages--details & grid-template-columns: 1fr diff --git a/frontend/src/global_styles/layout/work_packages/_index.sass b/frontend/src/global_styles/layout/work_packages/_index.sass index f5f2c2342d74..87080160b931 100644 --- a/frontend/src/global_styles/layout/work_packages/_index.sass +++ b/frontend/src/global_styles/layout/work_packages/_index.sass @@ -2,6 +2,7 @@ @import table_baseline @import table_baseline_overrides @import table_embedded +@import table_primerized @import details_view @import full_view @import mobile diff --git a/frontend/src/global_styles/layout/work_packages/_mobile.sass b/frontend/src/global_styles/layout/work_packages/_mobile.sass index 8955fe375fcb..c79dd4b54c32 100644 --- a/frontend/src/global_styles/layout/work_packages/_mobile.sass +++ b/frontend/src/global_styles/layout/work_packages/_mobile.sass @@ -121,5 +121,7 @@ padding: 15px 0 !important .toolbar-container, - .work-packages--filters-optional-container + .work-packages--filters-optional-container, + page-header, + sub-header margin-left: 15px diff --git a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts b/frontend/src/global_styles/layout/work_packages/_table_primerized.sass similarity index 74% rename from frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts rename to frontend/src/global_styles/layout/work_packages/_table_primerized.sass index d49151dfcf9b..cc8994ccbecb 100644 --- a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts +++ b/frontend/src/global_styles/layout/work_packages/_table_primerized.sass @@ -26,13 +26,10 @@ // 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'; +.op-primerized-work-packages-list + .op-wp-list-view + height: 100% -@Component({ - selector: 'wp-copy-split-view', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: '../wp-new/wp-new-split-view.html', -}) -export class WorkPackageCopySplitViewComponent extends WorkPackageCopyController { -} + + wp-table + height: calc(100% - 55px) diff --git a/frontend/src/global_styles/openproject/_mixins.sass b/frontend/src/global_styles/openproject/_mixins.sass index 5939b79e33d6..6711f5a7a666 100644 --- a/frontend/src/global_styles/openproject/_mixins.sass +++ b/frontend/src/global_styles/openproject/_mixins.sass @@ -242,7 +242,9 @@ $scrollbar-size: 10px .toolbar-container padding-right: 15px - .work-packages--filters-optional-container + .work-packages--filters-optional-container, + page-header, + sub-header margin-right: 15px @mixin modifying--placeholder diff --git a/frontend/src/global_styles/primer/_overrides.sass b/frontend/src/global_styles/primer/_overrides.sass index 2ef53fc4dd50..ae2c285eead3 100644 --- a/frontend/src/global_styles/primer/_overrides.sass +++ b/frontend/src/global_styles/primer/_overrides.sass @@ -51,6 +51,7 @@ action-menu anchored-position margin-left: 0 ul.SegmentedControl, +ul.UnderlineNav-body, .Box > ul:not(.op-uc-list) margin-left: 0 diff --git a/lib/open_project/text_formatting/filters/macros/create_work_package_link.rb b/lib/open_project/text_formatting/filters/macros/create_work_package_link.rb index 22da70090192..d031cd03b885 100644 --- a/lib/open_project/text_formatting/filters/macros/create_work_package_link.rb +++ b/lib/open_project/text_formatting/filters/macros/create_work_package_link.rb @@ -63,13 +63,13 @@ def work_package_link(macro, context) ApplicationController.helpers.link_to( I18n.t("macros.create_work_package_link.link_name_type", type_name:), - new_project_work_packages_path(project_id: project.identifier, type: type.id), + new_project_work_package_path(project_id: project.identifier, type: type.id), class: class_name ) else ApplicationController.helpers.link_to( I18n.t("macros.create_work_package_link.link_name"), - new_project_work_packages_path(project_id: project.identifier), + new_project_work_package_path(project_id: project.identifier), class: class_name ) end diff --git a/lib/redmine/menu_manager/top_menu/quick_add_menu.rb b/lib/redmine/menu_manager/top_menu/quick_add_menu.rb index bd04963b91bb..f4ed562a9862 100644 --- a/lib/redmine/menu_manager/top_menu/quick_add_menu.rb +++ b/lib/redmine/menu_manager/top_menu/quick_add_menu.rb @@ -94,7 +94,7 @@ def work_package_create_link(type_id, type_name) content_tag(:li, class: "op-menu--item") do if in_project_context? link_to type_name, - new_project_work_packages_path(project_id: @project.identifier, type: type_id), + new_project_work_package_path(project_id: @project.identifier, type: type_id), class: "__hl_inline_type_#{type_id} op-menu--item-action" else link_to type_name, diff --git a/lookbook/previews/open_project/work_packages/split_view_component_preview.rb b/lookbook/previews/open_project/work_packages/split_view_component_preview.rb index cfeedd5442be..d67286173b88 100644 --- a/lookbook/previews/open_project/work_packages/split_view_component_preview.rb +++ b/lookbook/previews/open_project/work_packages/split_view_component_preview.rb @@ -5,8 +5,9 @@ module OpenProject::WorkPackages class SplitViewComponentPreview < ViewComponent::Preview # @display min_height 400px def default - render(WorkPackages::SplitViewComponent.new(id: WorkPackage.visible.pick(:id), tab: "overview", - base_route: work_packages_path)) + render(WorkPackages::SplitView::ShowComponent.new(id: WorkPackage.visible.pick(:id), + tab: "overview", + base_route: work_packages_path)) end end end diff --git a/modules/budgets/app/controllers/budgets_controller.rb b/modules/budgets/app/controllers/budgets_controller.rb index 7f01d8adf16a..d7a4e52fbab7 100644 --- a/modules/budgets/app/controllers/budgets_controller.rb +++ b/modules/budgets/app/controllers/budgets_controller.rb @@ -220,12 +220,6 @@ def find_budgets render_404 end - def find_optional_project - @project = Project.find(params[:project_id]) if params[:project_id].present? - rescue ActiveRecord::RecordNotFound - render_404 - end - def render_item_as_json(element_id, costs, unit, project, permission) response = { "#{element_id}_unit_name" => ActionController::Base.helpers.sanitize(unit), diff --git a/modules/costs/app/controllers/hourly_rates_controller.rb b/modules/costs/app/controllers/hourly_rates_controller.rb index 50feea32249f..7132b92d5329 100644 --- a/modules/costs/app/controllers/hourly_rates_controller.rb +++ b/modules/costs/app/controllers/hourly_rates_controller.rb @@ -174,12 +174,6 @@ def find_project render_404 end - def find_optional_project - @project = params[:project_id].blank? ? nil : Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 - end - def find_user @user = params[:id] ? User.find(params[:id]) : User.current rescue ActiveRecord::RecordNotFound diff --git a/spec/components/work_packages/split_view_component_spec.rb b/spec/components/work_packages/split_view/show_component_spec.rb similarity index 92% rename from spec/components/work_packages/split_view_component_spec.rb rename to spec/components/work_packages/split_view/show_component_spec.rb index fdaac0b991dc..8b83dd57169d 100644 --- a/spec/components/work_packages/split_view_component_spec.rb +++ b/spec/components/work_packages/split_view/show_component_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" -RSpec.describe WorkPackages::SplitViewComponent, type: :component do +RSpec.describe WorkPackages::SplitView::ShowComponent, type: :component do include OpenProject::StaticRouting::UrlHelpers let(:project) { create(:project) } diff --git a/spec/features/attachments/attachments_spec.rb b/spec/features/attachments/attachments_spec.rb index 6d2c1b86f155..2032ae94df4a 100644 --- a/spec/features/attachments/attachments_spec.rb +++ b/spec/features/attachments/attachments_spec.rb @@ -42,7 +42,7 @@ # FIXME rework this spec after implementing fullscreen create view xit "uploading a short text file and viewing it inline" do - visit new_project_work_packages_path(project) + visit new_project_work_package_path(project) select project.types.first.name, from: "work_package_type_id" fill_in "Subject", with: "attachment test" diff --git a/spec/features/menu_items/quick_add_menu_spec.rb b/spec/features/menu_items/quick_add_menu_spec.rb index 503ca56da99b..8c640728c291 100644 --- a/spec/features/menu_items/quick_add_menu_spec.rb +++ b/spec/features/menu_items/quick_add_menu_spec.rb @@ -131,7 +131,7 @@ quick_add.click_link type_bug.name expect(page) - .to have_current_path new_project_work_packages_path(project_id: project_with_permission, type: type_bug.id) + .to have_current_path new_project_work_package_path(project_id: project_with_permission, type: type_bug.id) visit project_path(project_without_permission) diff --git a/spec/features/types/form_configuration_query_spec.rb b/spec/features/types/form_configuration_query_spec.rb index 3ccc66207d8e..a8eb3c6ccfc6 100644 --- a/spec/features/types/form_configuration_query_spec.rb +++ b/spec/features/types/form_configuration_query_spec.rb @@ -134,7 +134,7 @@ expect_flash(message: "Successful update.") # Visit new wp page - visit new_project_work_packages_path(project) + visit new_project_work_package_path(project) wp_page.expect_no_group "Subtasks" expect(page).to have_no_text "Subtasks" diff --git a/spec/features/work_packages/attachments/attachment_upload_spec.rb b/spec/features/work_packages/attachments/attachment_upload_spec.rb index ef7b3362f084..0b681805e982 100644 --- a/spec/features/work_packages/attachments/attachment_upload_spec.rb +++ b/spec/features/work_packages/attachments/attachment_upload_spec.rb @@ -201,7 +201,7 @@ let(:post_conditions) { nil } before do - visit new_project_work_packages_path(project.identifier, type: type.id) + visit new_project_work_package_path(project.identifier, type: type.id) end it "can upload an image via drag & drop (Regression #28189)" do |example| diff --git a/spec/features/work_packages/cancel_editing_spec.rb b/spec/features/work_packages/cancel_editing_spec.rb index 55a84035532a..a8afbbfccbca 100644 --- a/spec/features/work_packages/cancel_editing_spec.rb +++ b/spec/features/work_packages/cancel_editing_spec.rb @@ -39,7 +39,7 @@ [ new_work_packages_path, new_split_work_packages_path, - new_project_work_packages_path(project), + new_project_work_package_path(project), new_split_project_work_packages_path(project) ] end diff --git a/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb b/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb index cf7cea9d4e32..2b69764a6d55 100644 --- a/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb +++ b/spec/features/work_packages/details/relations/hierarchy_custom_fields_spec.rb @@ -45,7 +45,7 @@ before do login_as user - visit new_project_work_packages_path(project.identifier, type: type.id) + visit new_project_work_package_path(project.identifier, type: type.id) expect_angular_frontend_initialized loading_indicator_saveguard end diff --git a/spec/features/work_packages/new/new_work_package_spec.rb b/spec/features/work_packages/new/new_work_package_spec.rb index 88b8b70ec960..9eae4a457a8e 100644 --- a/spec/features/work_packages/new/new_work_package_spec.rb +++ b/spec/features/work_packages/new/new_work_package_spec.rb @@ -395,7 +395,7 @@ def create_work_package_globally(type, project_name) [ new_work_packages_path, new_split_work_packages_path, - new_project_work_packages_path(project), + new_project_work_package_path(project), new_split_project_work_packages_path(project) ] end @@ -414,7 +414,7 @@ def create_work_package_globally(type, project_name) let(:wp_page) { Pages::FullWorkPackageCreate.new } before do - visit new_project_work_packages_path(project) + visit new_project_work_package_path(project) end it "can create the work package, but not update it after saving" do @@ -442,7 +442,7 @@ def create_work_package_globally(type, project_name) [ new_work_packages_path, new_split_work_packages_path, - new_project_work_packages_path(project), + new_project_work_package_path(project), new_split_project_work_packages_path(project) ] end diff --git a/spec/features/work_packages/table/switch_types_spec.rb b/spec/features/work_packages/table/switch_types_spec.rb index 57be6c1b4c35..4d19c482a1b3 100644 --- a/spec/features/work_packages/table/switch_types_spec.rb +++ b/spec/features/work_packages/table/switch_types_spec.rb @@ -272,7 +272,7 @@ def req_text_field workflow login_as(user) - visit new_project_work_packages_path(project.identifier, type: type.id) + visit new_project_work_package_path(project.identifier, type: type.id) expect_angular_frontend_initialized SeleniumHubWaiter.wait end diff --git a/spec/features/work_packages/work_packages_page.rb b/spec/features/work_packages/work_packages_page.rb index f370045241d7..3da52707038f 100644 --- a/spec/features/work_packages/work_packages_page.rb +++ b/spec/features/work_packages/work_packages_page.rb @@ -43,7 +43,7 @@ def visit_index(work_package = nil) end def visit_new - visit new_project_work_packages_path(@project) + visit new_project_work_package_path(@project) end def visit_show(id) diff --git a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb index fe62052c5c99..638b6255dfdd 100644 --- a/spec/lib/api/v3/work_packages/work_package_representer_spec.rb +++ b/spec/lib/api/v3/work_packages/work_package_representer_spec.rb @@ -1094,7 +1094,7 @@ describe "move" do it_behaves_like "has a titled action link" do let(:link) { "move" } - let(:href) { work_package_path(work_package, "move/new") } + let(:href) { "/work_packages/#{work_package.id}/move/new" } let(:permission) { :move_work_packages } let(:title) { "Move work package '#{work_package.subject}'" } end diff --git a/spec/routing/work_packages_spec.rb b/spec/routing/work_packages_spec.rb index 1b090cd16ca6..11e15cc5b9e4 100644 --- a/spec/routing/work_packages_spec.rb +++ b/spec/routing/work_packages_spec.rb @@ -40,49 +40,52 @@ action: "index") end - it "connects GET /work_packages/new to work_packages#index" do + it "connects GET /work_packages/new to work_packages#new" do expect(get("/work_packages/new")) .to route_to(controller: "work_packages", - action: "index", - state: "new") + action: "new") end - it "connects GET /projects/:project_id/work_packages/new to work_packages#index" do + it "connects GET /projects/:project_id/work_packages/new to work_packages#new" do expect(get("/projects/1/work_packages/new")) .to route_to(controller: "work_packages", - action: "index", - project_id: "1", - state: "new") + action: "new", + project_id: "1") end it "connects GET /work_packages/:id/overview to work_packages#show" do expect(get("/work_packages/1/overview")) .to route_to(controller: "work_packages", - action: "show", id: "1", state: "overview") + action: "show", id: "1", tab: "overview") end - it "connects GET /projects/:project_id/work_packages/:id/overview to work_packages#index" do + it "connects GET /projects/:project_id/work_packages/:id/overview to work_packages#show" do expect(get("/projects/1/work_packages/2/overview")) .to route_to(controller: "work_packages", - action: "index", + action: "show", project_id: "1", - state: "2/overview") + id: "2", + tab: "overview") end it "connects GET /work_packages/details/:state to work_packages#index" do expect(get("/work_packages/details/5/overview")) .to route_to(controller: "work_packages", - action: "index", - state: "5/overview") + action: "split_view", + work_package_id: "5", + view_type: "work_package_split_view", + tab: "overview") end it "connects GET /projects/:project_id/work_packages/details/:id/:state " \ "to work_packages#index" do expect(get("/projects/1/work_packages/details/2/overview")) .to route_to(controller: "work_packages", - action: "index", + action: "split_view", project_id: "1", - state: "details/2/overview") + work_package_id: "2", + view_type: "work_package_split_view", + tab: "overview") end it "connects GET /work_packages/:id to work_packages#show" do diff --git a/spec/support/pages/work_packages/full_work_package_create.rb b/spec/support/pages/work_packages/full_work_package_create.rb index 1bc1ee6112f8..9f5f9f2dd841 100644 --- a/spec/support/pages/work_packages/full_work_package_create.rb +++ b/spec/support/pages/work_packages/full_work_package_create.rb @@ -41,10 +41,10 @@ def path if original_work_package project_work_package_path(original_work_package.project, original_work_package.id) + "/copy" elsif parent_work_package - new_project_work_packages_path(parent_work_package.project.identifier, - parent_id: parent_work_package.id) + new_project_work_package_path(parent_work_package.project.identifier, + parent_id: parent_work_package.id) elsif project - new_project_work_packages_path(project.identifier) + new_project_work_package_path(project.identifier) end end end