From afc9b030085bf81096465b5c1c84521126874051 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Wed, 11 Sep 2024 11:03:38 +0200 Subject: [PATCH 01/28] Add a turboFrame in the partitioned query space to render a Primer PageHeader above the work package table view. This is work in progress --- .../index_page_header_component.html.erb | 99 +++++++++++++++++++ .../index_page_header_component.rb | 90 +++++++++++++++++ .../index_sub_header_component.html.erb | 34 +++++++ .../index_sub_header_component.rb | 46 +++++++++ .../work_packages/page_header_controller.rb | 37 +++++++ .../work_packages/page_header/index.html.erb | 4 + config/initializers/permissions.rb | 3 +- config/routes.rb | 2 + .../board-partitioned-page.component.ts | 3 + ...artitioned-query-space-page.component.html | 7 +- .../partitioned-query-space-page.component.ts | 12 +++ .../layout/work_packages/_mobile.sass | 4 +- .../global_styles/openproject/_mixins.sass | 4 +- 13 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 app/components/work_packages/index_page_header_component.html.erb create mode 100644 app/components/work_packages/index_page_header_component.rb create mode 100644 app/components/work_packages/index_sub_header_component.html.erb create mode 100644 app/components/work_packages/index_sub_header_component.rb create mode 100644 app/controllers/work_packages/page_header_controller.rb create mode 100644 app/views/work_packages/page_header/index.html.erb 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..cb814ab9a2a7 --- /dev/null +++ b/app/components/work_packages/index_page_header_component.html.erb @@ -0,0 +1,99 @@ +<%= + 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_dialog(mobile_icon: :"op-include-projects", + mobile_label: I18n.t("js.include_projects.toggle_title"), + dialog_arguments: { id: "my_dialog", title: "A great dialog" }, + button_arguments: { button_block: project_include_button_callback, test_selector: "project-include-button" }) do |d| + d.with_body { "TODO" } + end + + header.with_action_dialog(mobile_icon: :"op-baseline", + mobile_label: I18n.t("js.baseline.toggle_title"), + dialog_arguments: { id: "my_dialog2", title: "A great dialog" }, + button_arguments: { button_block: baseline_button_callback, test_selector: "baseline-button" }) do |d| + d.with_body { "TODO" } + 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: "TODO", + content_arguments: { data: { controller: "async-dialog" }} + ) 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 + + menu.with_item( + tag: :a, + label: t('js.label_export'), + href: "TODO", + 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" }} + ) 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..405ae979aa23 --- /dev/null +++ b/app/components/work_packages/index_page_header_component.rb @@ -0,0 +1,90 @@ +# 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 baseline_button_callback + lambda do |button| + button.with_leading_visual_icon(icon: :"op-baseline") + I18n.t("js.baseline.toggle_title") + end + end + + def project_include_button_callback + lambda do |button| + button.with_leading_visual_icon(icon: :"op-include-projects") + I18n.t("js.include_projects.toggle_title") + end + end + + def can_rename? + # TODO + true + end + + def can_save? + # TODO + true + end + + def can_save_as? + # 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..2f0c59055d55 --- /dev/null +++ b/app/components/work_packages/index_sub_header_component.html.erb @@ -0,0 +1,34 @@ +<%= render(Primer::OpenProject::SubHeader.new) do |subheader| %> + <%= + subheader.with_filter_component do + # TODO replace with FilterButtonComponent + render(Primer::Beta::Button.new(scheme: :secondary)) do |button| + button.with_trailing_visual_counter(count: 10) + t(:label_filter) + end + end + %> + + <%= + subheader.with_bottom_pane_component(mt: 0) do + # TODO render(Projects::ProjectsFiltersComponent.new(query: @query)) + end + %> + + <% if can_create_work_packages? %> + <% subheader.with_action_component do %> + <%= + 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 + menu.with_item(label: "TODO Subitem 1") + menu.with_item(label: "TODO Subitem 2") + end + %> + <% end %> + <% 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..afdbe0ffa4aa --- /dev/null +++ b/app/components/work_packages/index_sub_header_component.rb @@ -0,0 +1,46 @@ +# 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 + end +end diff --git a/app/controllers/work_packages/page_header_controller.rb b/app/controllers/work_packages/page_header_controller.rb new file mode 100644 index 000000000000..c17c11aa0af7 --- /dev/null +++ b/app/controllers/work_packages/page_header_controller.rb @@ -0,0 +1,37 @@ +#-- 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 PageHeaderController < ApplicationController + before_action :load_and_authorize_in_optional_project, only: [:index] + # TODO before_action :load_query, only: %i[index] + + def index + render layout: nil + end + end +end diff --git a/app/views/work_packages/page_header/index.html.erb b/app/views/work_packages/page_header/index.html.erb new file mode 100644 index 000000000000..f099eec596ed --- /dev/null +++ b/app/views/work_packages/page_header/index.html.erb @@ -0,0 +1,4 @@ +<%= turbo_frame_tag "work-package-index-page-header" do %> + <%= render WorkPackages::IndexPageHeaderComponent.new(project: @project) %> + <%= render WorkPackages::IndexSubHeaderComponent.new(project: @project) %> +<% end %> diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index d0ea7360287d..2ea7f8222882 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -217,7 +217,8 @@ work_packages: %i[show index], work_packages_api: [:get], "work_packages/reports": %i[report report_details], - "work_packages/menus": %i[show] + "work_packages/menus": %i[show], + "work_packages/page_header": %i[index] }, permissible_on: %i[work_package project], contract_actions: { work_packages: %i[read] } diff --git a/config/routes.rb b/config/routes.rb index f1c68581dc47..eb9293fc1745 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -319,6 +319,7 @@ get "/report/:detail" => "work_packages/reports#report_details" get "/report" => "work_packages/reports#report" get "menu" => "work_packages/menus#show" + get "index_page_header" => "work_packages/page_header#index" get "/export_dialog" => "work_packages#export_dialog" end @@ -549,6 +550,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] 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..cde7290db476 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 @@ -136,6 +136,9 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { }, ]; + // TODO + PageHeaderTurboFrameSrc:string = ''; + constructor( readonly I18n:I18nService, readonly cdRef:ChangeDetectorRef, diff --git a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html index ba769626b4ca..0db12c5a85af 100644 --- a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html +++ b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html @@ -1,6 +1,6 @@
-
+
+ + +
; @@ -87,6 +88,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'), @@ -130,9 +133,18 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp component: WorkPackageFilterContainerComponent, }; + PageHeaderTurboFrameSrc:string = ''; + ngOnInit():void { super.ngOnInit(); + // TODO: Limit to WP module + if (this.currentProject.inProjectContext) { + this.PageHeaderTurboFrameSrc = `${this.pathHelper.projectWorkPackagesPath(this.currentProject.identifier!)}/index_page_header`; + } else { + this.PageHeaderTurboFrameSrc = `${this.pathHelper.workPackagesPath()}/index_page_header`; + } + this.showToolbarSaveButton = !!this.$state.params.query_props; this.setPartition(this.$state.current); this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => { 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/global_styles/openproject/_mixins.sass b/frontend/src/global_styles/openproject/_mixins.sass index 3f60adeef5ba..5b4cb4c3cbe4 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 From 5877cd64e5138c45e8f0023624c1d049ad1589d5 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Wed, 11 Sep 2024 14:59:28 +0200 Subject: [PATCH 02/28] A rails based WP create button --- .../wp_create_button_component.html.erb | 19 ++++++ .../create/wp_create_button_component.rb | 67 +++++++++++++++++++ .../index_sub_header_component.html.erb | 17 +---- 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 app/components/work_packages/create/wp_create_button_component.html.erb create mode 100644 app/components/work_packages/create/wp_create_button_component.rb 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..552202489a4a --- /dev/null +++ b/app/components/work_packages/create/wp_create_button_component.html.erb @@ -0,0 +1,19 @@ +<% 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..11330c4831b6 --- /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 + new_split_project_work_packages_path(@project, type: type.id) + else + new_split_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/index_sub_header_component.html.erb b/app/components/work_packages/index_sub_header_component.html.erb index 2f0c59055d55..16d3f9d760b0 100644 --- a/app/components/work_packages/index_sub_header_component.html.erb +++ b/app/components/work_packages/index_sub_header_component.html.erb @@ -15,20 +15,7 @@ end %> - <% if can_create_work_packages? %> - <% subheader.with_action_component do %> - <%= - 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 - menu.with_item(label: "TODO Subitem 1") - menu.with_item(label: "TODO Subitem 2") - end - %> - <% end %> + <% subheader.with_action_component do %> + <%= render WorkPackages::Create::WpCreateButtonComponent.new(project: @project) %> <% end %> <% end %> From a4fbf4934ba5e859424e52e7280c076152f4d6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 11 Sep 2024 15:20:48 +0200 Subject: [PATCH 03/28] Use base filter component for projects and work packages --- .../filter/filter_component.html.erb | 6 +- app/components/filter/filter_component.rb | 4 + .../projects_filters_component.html.erb | 109 ------------------ .../projects/projects_filters_component.rb | 4 + .../work_packages/filter_button_component.rb | 36 ++++++ .../work_packages/filters_component.rb | 59 ++++++++++ .../index_sub_header_component.html.erb | 10 +- .../index_sub_header_component.rb | 8 ++ .../work_packages/page_header_controller.rb | 16 ++- .../filter/manual_sort_filter.rb | 4 + app/models/query.rb | 4 + .../work_packages/page_header/index.html.erb | 2 +- .../partitioned-query-space-page.component.ts | 21 ++-- 13 files changed, 152 insertions(+), 131 deletions(-) delete mode 100644 app/components/projects/projects_filters_component.html.erb create mode 100644 app/components/work_packages/filter_button_component.rb create mode 100644 app/components/work_packages/filters_component.rb 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) %> -
      - <% each_filter do |filter, filter_active, additional_options| %> - <% filter_boolean = filter.is_a?(Queries::Filters::Shared::BooleanFilter) %> - <% autocomplete_filter = additional_options.key?(:autocomplete_options) %> -
    • - - <% selected_operator = filter.operator || filter.default_operator.symbol %> - <%= content_tag :div, class: "advanced-filters--filter-operator", style: filter_boolean ? 'display:none' : '' do %> - <%= select_tag :operator, - options_from_collection_for_select( - filter.available_operators, - :symbol, - :human_name, - selected_operator), - class: 'advanced-filters--select', - data: { - action: 'change->filter--filters-form#setValueVisibility', - 'filter--filters-form-filter-name-param': filter.name, - 'filter--filters-form-target': 'operator', - 'filter-name': filter.name - } %> - <% end %> - <% value_visibility = operators_without_values.include?(selected_operator) ? 'hidden' : '' %> - <% if autocomplete_filter %> - <%= render partial: 'filters/autocomplete', - locals: { value_visibility: value_visibility, - filter: filter, - autocomplete_options: additional_options[:autocomplete_options] } %> - <% elsif filter_boolean %> - <%= render partial: 'filters/boolean', - locals: { value_visibility: value_visibility, - filter: filter } %> - <% elsif %i(list list_optional list_all).include? filter.type %> - <%= render partial: 'filters/list/input_options', - locals: { value_visibility: value_visibility, - filter: filter } %> - <% elsif [:datetime_past, :date].include? filter.type %> - <%= render partial: 'filters/date/input_options', - locals: { value_visibility: value_visibility, - filter: filter, - selected_operator: selected_operator } %> - <% else %> - <%# All other simple types %> - <%= render partial: 'filters/text', - locals: { value_visibility: value_visibility, - filter: filter } %> - <% end %> - -
    • - <% end %> -
    • -
    • - - - -
      - <%= select_tag 'add_filter_select', - options_from_collection_for_select( - allowed_filters, - :name, - :human_name, - disabled: query.filters.map(&:name) - ), - prompt: t(:actionview_instancetag_blank_option), - class: 'advanced-filters--select', - focus: "false", - 'aria-invalid': "false", - data: { - 'filter--filters-form-target': 'addFilterSelect', - action: 'change->filter--filters-form#addFilter:prevent' - } %> -
      -
    • -
    -
    -<% 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/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/index_sub_header_component.html.erb b/app/components/work_packages/index_sub_header_component.html.erb index 16d3f9d760b0..acf26fe56d06 100644 --- a/app/components/work_packages/index_sub_header_component.html.erb +++ b/app/components/work_packages/index_sub_header_component.html.erb @@ -1,17 +1,13 @@ -<%= render(Primer::OpenProject::SubHeader.new) do |subheader| %> +<%= render(Primer::OpenProject::SubHeader.new(data: sub_header_data_attributes)) do |subheader| %> <%= subheader.with_filter_component do - # TODO replace with FilterButtonComponent - render(Primer::Beta::Button.new(scheme: :secondary)) do |button| - button.with_trailing_visual_counter(count: 10) - t(:label_filter) - end + render(WorkPackages::FilterButtonComponent.new(query: @query)) end %> <%= subheader.with_bottom_pane_component(mt: 0) do - # TODO render(Projects::ProjectsFiltersComponent.new(query: @query)) + render(WorkPackages::FiltersComponent.new(query: @query)) end %> diff --git a/app/components/work_packages/index_sub_header_component.rb b/app/components/work_packages/index_sub_header_component.rb index afdbe0ffa4aa..47db9103505f 100644 --- a/app/components/work_packages/index_sub_header_component.rb +++ b/app/components/work_packages/index_sub_header_component.rb @@ -42,5 +42,13 @@ 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/controllers/work_packages/page_header_controller.rb b/app/controllers/work_packages/page_header_controller.rb index c17c11aa0af7..ac63fae9730f 100644 --- a/app/controllers/work_packages/page_header_controller.rb +++ b/app/controllers/work_packages/page_header_controller.rb @@ -28,10 +28,24 @@ module WorkPackages class PageHeaderController < ApplicationController before_action :load_and_authorize_in_optional_project, only: [:index] - # TODO before_action :load_query, only: %i[index] + before_action :load_query, only: %i[index] def index render layout: nil end + + private + + def load_query + @query = + if params[:query_id] + Query.visible(current_user).find(params[:query_id]) + else + # TODO: we need to parse the unsaved query + Query.new_default(project: @project, user: current_user) + end + rescue ActiveRecord::RecordNotFound + render_404 + end 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 d8051b6b1d06..0b6e1eebb65a 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/views/work_packages/page_header/index.html.erb b/app/views/work_packages/page_header/index.html.erb index f099eec596ed..028723870c9b 100644 --- a/app/views/work_packages/page_header/index.html.erb +++ b/app/views/work_packages/page_header/index.html.erb @@ -1,4 +1,4 @@ <%= turbo_frame_tag "work-package-index-page-header" do %> <%= render WorkPackages::IndexPageHeaderComponent.new(project: @project) %> - <%= render WorkPackages::IndexSubHeaderComponent.new(project: @project) %> + <%= render WorkPackages::IndexSubHeaderComponent.new(project: @project, query: @query) %> <% end %> 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 70e029a7f3da..7a7dd6fa1e4d 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,26 +26,27 @@ // 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'; @@ -145,6 +146,8 @@ export class PartitionedQuerySpacePageComponent extends WorkPackagesViewBase imp this.PageHeaderTurboFrameSrc = `${this.pathHelper.workPackagesPath()}/index_page_header`; } + this.PageHeaderTurboFrameSrc += window.location.search; + this.showToolbarSaveButton = !!this.$state.params.query_props; this.setPartition(this.$state.current); this.removeTransitionSubscription = this.$transitions.onSuccess({}, (transition):any => { From e8a7891a73acaaa96c3d6eab1eaa233498154e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 27 Sep 2024 09:54:20 +0200 Subject: [PATCH 04/28] Render as controller --- .../work_packages/page_header_controller.rb | 51 ------------------- app/controllers/work_packages_controller.rb | 5 +- app/views/notifications/index.html.erb | 1 - app/views/work_packages/index.html.erb | 17 ++++++- .../work_packages/page_header/index.html.erb | 4 -- config/routes.rb | 1 - 6 files changed, 18 insertions(+), 61 deletions(-) delete mode 100644 app/controllers/work_packages/page_header_controller.rb delete mode 100644 app/views/work_packages/page_header/index.html.erb diff --git a/app/controllers/work_packages/page_header_controller.rb b/app/controllers/work_packages/page_header_controller.rb deleted file mode 100644 index ac63fae9730f..000000000000 --- a/app/controllers/work_packages/page_header_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -#-- 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 PageHeaderController < ApplicationController - before_action :load_and_authorize_in_optional_project, only: [:index] - before_action :load_query, only: %i[index] - - def index - render layout: nil - end - - private - - def load_query - @query = - if params[:query_id] - Query.visible(current_user).find(params[:query_id]) - else - # TODO: we need to parse the unsaved query - Query.new_default(project: @project, user: current_user) - end - rescue ActiveRecord::RecordNotFound - render_404 - end - end -end diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 3d8e12986034..d69281d80dd6 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -42,7 +42,7 @@ class WorkPackagesController < ApplicationController :protect_from_unauthorized_export, only: %i[index export_dialog] authorization_checked! :index, :show, :export_dialog - before_action :load_and_validate_query, only: :index, unless: -> { request.format.html? } + before_action :load_and_validate_query, only: :index before_action :load_work_packages, only: :index, if: -> { request.format.atom? } before_action :load_and_validate_query_for_export, only: :export_dialog @@ -50,8 +50,7 @@ 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 diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index c41256b40582..b7dab8db5892 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -11,7 +11,6 @@ <% content_for :content_body do %> <%= angular_component_tag "opce-notification-center" %> -dd <% end %> <% content_for :content_body_right do %> diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index f6c050d4b9ff..871abe558ee1 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,19 @@ 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-table" %> +<% end %> + +<% 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) %> + <%= render(split_view_instance) 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/page_header/index.html.erb b/app/views/work_packages/page_header/index.html.erb deleted file mode 100644 index 028723870c9b..000000000000 --- a/app/views/work_packages/page_header/index.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -<%= turbo_frame_tag "work-package-index-page-header" do %> - <%= render WorkPackages::IndexPageHeaderComponent.new(project: @project) %> - <%= render WorkPackages::IndexSubHeaderComponent.new(project: @project, query: @query) %> -<% end %> diff --git a/config/routes.rb b/config/routes.rb index 1c55493c90d9..15fcf1fe6066 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -322,7 +322,6 @@ get "/report/:detail" => "work_packages/reports#report_details" get "/report" => "work_packages/reports#report" get "menu" => "work_packages/menus#show" - get "index_page_header" => "work_packages/page_header#index" get "/export_dialog" => "work_packages#export_dialog" end From 9d7cf59248e6ec29cb667ab2159eb31b77a6ef9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 27 Sep 2024 09:59:24 +0200 Subject: [PATCH 05/28] Remove page header turbo --- .../board-partitioned-page.component.ts | 33 +-- .../openproject-work-packages.module.ts | 6 + ...artitioned-query-space-page.component.html | 7 +- .../partitioned-query-space-page.component.ts | 11 - .../wp-primerized-list-view.component.ts | 227 ++++++++++++++++++ 5 files changed, 246 insertions(+), 38 deletions(-) create mode 100644 frontend/src/app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component.ts 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 cde7290db476..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'; @@ -136,9 +130,6 @@ export class BoardPartitionedPageComponent extends UntilDestroyedMixin { }, ]; - // TODO - PageHeaderTurboFrameSrc:string = ''; - constructor( readonly I18n:I18nService, readonly cdRef:ChangeDetectorRef, 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..0450fa271aee 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 @@ -416,6 +416,9 @@ 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'; @NgModule({ imports: [ @@ -655,6 +658,9 @@ import { OpBaselineComponent, 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.html b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html index 0db12c5a85af..ba769626b4ca 100644 --- a/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html +++ b/frontend/src/app/features/work-packages/routing/partitioned-query-space-page/partitioned-query-space-page.component.html @@ -1,6 +1,6 @@
    -
    +
    - - -
    { 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..449528bd1614 --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component.ts @@ -0,0 +1,227 @@ +//-- 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, + ChangeDetectorRef, + Component, + 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'; +import { DragAndDropService } from 'core-app/shared/helpers/drag-and-drop/drag-and-drop.service'; +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 { 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 { 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 { 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 { WorkPackageViewBaselineService } from '../wp-view-base/view-services/wp-view-baseline.service'; +import { combineLatest } from 'rxjs'; +import { + WorkPackageIsolatedQuerySpaceDirective, +} from 'core-app/features/work-packages/directives/query-space/wp-isolated-query-space.directive'; + +@Component({ + templateUrl: './wp-list-view.component.html', + styleUrls: ['./wp-list-view.component.sass'], + host: { class: 'op-wp-list-view work-packages-split-view--tabletimeline-side' }, + hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { provide: HalResourceNotificationService, useClass: WorkPackageNotificationService }, + DragAndDropService, + CausedUpdatesService, + ], +}) +export class WorkPackagePrimerizedListViewComponent extends UntilDestroyedMixin implements OnInit { + text = { + jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.pagination'), + text_jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.label_pagination'), + button_settings: this.I18n.t('js.button_settings'), + }; + + /** Switch between list and card view */ + showTableView = true; + + /** Determine when query is initially loaded */ + tableInformationLoaded = false; + + /** If loaded list of work packages is empty */ + noResults = false; + + /** Whether we should render a blocked view */ + showResultOverlay$ = this.wpViewFilters.incomplete$; + + public baselineEnabled:boolean; + + /** */ + readonly wpTableConfiguration:WorkPackageTableConfigurationObject = { + dragAndDropEnabled: true, + }; + + constructor( + readonly I18n:I18nService, + readonly injector:Injector, + readonly $state:StateService, + readonly keepTab:KeepTabService, + readonly querySpace:IsolatedQuerySpace, + readonly wpViewFilters:WorkPackageViewFiltersService, + readonly deviceService:DeviceService, + readonly CurrentProject:CurrentProjectService, + readonly wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService, + readonly cdRef:ChangeDetectorRef, + readonly elementRef:ElementRef, + private ngZone:NgZone, + readonly wpTableBaseline:WorkPackageViewBaselineService, + ) { + super(); + } + + ngOnInit() { + // Mark tableInformationLoaded when initially loading done + this.setupInformationLoadedListener(); + const statesCombined = combineLatest([ + this.querySpace.query.values$(), + this.wpTableBaseline.live$(), + ]); + statesCombined.pipe( + this.untilDestroyed(), + ).subscribe(([query]) => { + // Update the visible representation + this.updateViewRepresentation(query); + this.baselineEnabled = this.wpTableBaseline.isActive(); + this.noResults = query.results.total === 0; + this.cdRef.detectChanges(); + }); + + // Scroll into view the card/row that represents the last selected WorkPackage + // so when the user opens a WP detail page on a split-view and then clicks on + // the 'back button', the last selected card is visible on this list. + // ngAfterViewInit doesn't find the .-checked elements on components + // that inherit from this class (BcfListContainerComponent) so + // opting for a timeout 'runOutsideAngular' to avoid running change + // detection on the entire app + this.ngZone.runOutsideAngular(() => { + setTimeout(() => { + const selectedRow = this.elementRef.nativeElement.querySelector('.wp-table--row.-checked'); + const selectedCard = this.elementRef.nativeElement.querySelector('[data-test-selector="op-wp-single-card"].-checked'); + + // The header of the table hides the scrolledIntoView element + // so we scrollIntoView the previous element, if any + if (selectedRow && selectedRow.previousSibling) { + selectedRow.previousSibling.scrollIntoView({ block: 'start' }); + } + + if (selectedCard) { + selectedCard.scrollIntoView({ block: 'start' }); + } + }, 0); + }); + } + + protected setupInformationLoadedListener() { + this + .querySpace + .initialized + .values$() + .pipe(take(1)) + .subscribe(() => { + this.tableInformationLoaded = true; + this.cdRef.detectChanges(); + }); + } + + public showResizerInCardView():boolean { + return false; + } + + protected updateViewRepresentation(query:QueryResource) { + this.showTableView = !(this.deviceService.isMobile + || this.wpDisplayRepresentation.valueFromQuery(query) === wpDisplayCardRepresentation); + } + + handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) { + if (event.double) { + this.openInFullView(event.workPackageId); + } + } + + openStateLink(event:{ workPackageId:string; requestedState:'show'|'split' }) { + const params = { + workPackageId: event.workPackageId, + focus: true, + }; + + if (event.requestedState === 'split') { + this.keepTab.goCurrentDetailsState(params); + } else { + this.keepTab.goCurrentShowState(params); + } + } + + /** + * Special handling for clicking on cards. + * If we are on mobile, a click on the card should directly open the full view + */ + handleWorkPackageCardClicked(event:{ workPackageId:string; double:boolean }):void { + if (this.deviceService.isMobile) { + this.openInFullView(event.workPackageId); + } else { + this.handleWorkPackageClicked(event); + } + } + + private openInFullView(workPackageId:string) { + this.$state.go( + 'work-packages.show', + { workPackageId }, + ); + } +} From f004a796f5718891eb7ba436d82e829fbed3cc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 27 Sep 2024 10:27:34 +0200 Subject: [PATCH 06/28] custom list component rendered from rails --- app/views/work_packages/index.html.erb | 2 +- frontend/src/app/app.module.ts | 4 + .../loading-indicator.service.ts | 4 + .../wp-list-view/wp-list-view.component.html | 62 ++--- .../wp-list-view/wp-list-view.component.ts | 25 +- .../wp-primerized-list-view.component.ts | 218 +++++------------- .../layout/work_packages/_index.sass | 1 + .../work_packages/_table_primerized.sass | 36 +++ 8 files changed, 158 insertions(+), 194 deletions(-) create mode 100644 frontend/src/global_styles/layout/work_packages/_table_primerized.sass diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index 871abe558ee1..70ada0979a7f 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -44,7 +44,7 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% content_for :content_body do %> - <%= angular_component_tag "opce-work-packages-table" %> + <%= angular_component_tag "opce-work-packages-list" %> <% end %> <% content_for :content_body_right do %> diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f8d39a6f527c..e3bd260c5664 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -237,6 +237,9 @@ import { } 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'; import { SpotSwitchComponent } from 'core-app/spot/components/switch/switch.component'; +import { + WorkPackagePrimerizedListViewComponent, +} from 'core-app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.component'; export function initializeServices(injector:Injector) { return () => { @@ -466,5 +469,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/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..c31f5dcbd34b 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,32 +1,34 @@ -
    - - - - +
    +
    + + + + - -
    - - -
    + +
    + + +
    - - \ 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..bf14f9426be0 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, @@ -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 index 449528bd1614..db2cfb40c248 100644 --- 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 @@ -26,52 +26,31 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - Injector, - NgZone, - OnInit, -} from '@angular/core'; -import { take } from 'rxjs/operators'; +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 { - 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 { 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 { 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 { 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 { WorkPackageViewBaselineService } from '../wp-view-base/view-services/wp-view-baseline.service'; -import { combineLatest } from 'rxjs'; 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-wp-list-view work-packages-split-view--tabletimeline-side' }, + host: { class: 'op-primerized-work-packages-list work-packages-split-view--tabletimeline-side' }, hostDirectives: [WorkPackageIsolatedQuerySpaceDirective], changeDetection: ChangeDetectionStrategy.OnPush, providers: [ @@ -80,148 +59,75 @@ import { CausedUpdatesService, ], }) -export class WorkPackagePrimerizedListViewComponent extends UntilDestroyedMixin implements OnInit { - text = { - jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.pagination'), - text_jump_to_pagination: this.I18n.t('js.work_packages.jump_marks.label_pagination'), - button_settings: this.I18n.t('js.button_settings'), - }; - - /** Switch between list and card view */ - showTableView = true; - - /** Determine when query is initially loaded */ - tableInformationLoaded = false; - - /** If loaded list of work packages is empty */ - noResults = false; - - /** Whether we should render a blocked view */ - showResultOverlay$ = this.wpViewFilters.incomplete$; - - public baselineEnabled:boolean; - - /** */ - readonly wpTableConfiguration:WorkPackageTableConfigurationObject = { - dragAndDropEnabled: true, - }; - - constructor( - readonly I18n:I18nService, - readonly injector:Injector, - readonly $state:StateService, - readonly keepTab:KeepTabService, - readonly querySpace:IsolatedQuerySpace, - readonly wpViewFilters:WorkPackageViewFiltersService, - readonly deviceService:DeviceService, - readonly CurrentProject:CurrentProjectService, - readonly wpDisplayRepresentation:WorkPackageViewDisplayRepresentationService, - readonly cdRef:ChangeDetectorRef, - readonly elementRef:ElementRef, - private ngZone:NgZone, - readonly wpTableBaseline:WorkPackageViewBaselineService, - ) { - super(); - } +export class WorkPackagePrimerizedListViewComponent extends WorkPackageListViewComponent implements OnInit { + @InjectField() loadingIndicatorService:LoadingIndicatorService; + @InjectField() wpListService:WorkPackagesListService; + @InjectField() currentProject:CurrentProjectService; + @InjectField() pathHelper:PathHelperService; + + currentQuery:QueryResource|undefined; ngOnInit() { - // Mark tableInformationLoaded when initially loading done - this.setupInformationLoadedListener(); - const statesCombined = combineLatest([ - this.querySpace.query.values$(), - this.wpTableBaseline.live$(), - ]); - statesCombined.pipe( - this.untilDestroyed(), - ).subscribe(([query]) => { - // Update the visible representation - this.updateViewRepresentation(query); - this.baselineEnabled = this.wpTableBaseline.isActive(); - this.noResults = query.results.total === 0; - this.cdRef.detectChanges(); - }); - - // Scroll into view the card/row that represents the last selected WorkPackage - // so when the user opens a WP detail page on a split-view and then clicks on - // the 'back button', the last selected card is visible on this list. - // ngAfterViewInit doesn't find the .-checked elements on components - // that inherit from this class (BcfListContainerComponent) so - // opting for a timeout 'runOutsideAngular' to avoid running change - // detection on the entire app - this.ngZone.runOutsideAngular(() => { - setTimeout(() => { - const selectedRow = this.elementRef.nativeElement.querySelector('.wp-table--row.-checked'); - const selectedCard = this.elementRef.nativeElement.querySelector('[data-test-selector="op-wp-single-card"].-checked'); - - // The header of the table hides the scrolledIntoView element - // so we scrollIntoView the previous element, if any - if (selectedRow && selectedRow.previousSibling) { - selectedRow.previousSibling.scrollIntoView({ block: 'start' }); - } - - if (selectedCard) { - selectedCard.scrollIntoView({ block: 'start' }); - } - }, 0); - }); + this.loadInitialQuery(); + super.ngOnInit(); } - protected setupInformationLoadedListener() { - this - .querySpace - .initialized - .values$() - .pipe(take(1)) - .subscribe(() => { - this.tableInformationLoaded = true; - this.cdRef.detectChanges(); - }); + openStateLink(event:{ workPackageId:string; requestedState:'show'|'split' }) { + if (event.requestedState === 'split') { + this.openSplitScreen(event.workPackageId, this.keepTab.currentTabIdentifier); + } else { + this.openInFullView(event.workPackageId); + } } - public showResizerInCardView():boolean { - return false; + openSplitScreen(workPackageId:string, tabIdentifier:string = 'activity'):void { + let link:string; + + if (this.currentProject.identifier) { + link = this.pathHelper.workPackageDetailsPath(this.currentProject.identifier, workPackageId, tabIdentifier); + } else { + link = this.pathHelper.workPackageDetailsPath(workPackageId, tabIdentifier); + } + + Turbo.visit(link + window.location.search, { frame: 'content-bodyRight', action: 'advance' }); } - protected updateViewRepresentation(query:QueryResource) { - this.showTableView = !(this.deviceService.isMobile - || this.wpDisplayRepresentation.valueFromQuery(query) === wpDisplayCardRepresentation); + openInFullView(workPackageId:string) { + window.location.href = this.pathHelper.workPackagePath(workPackageId); } - handleWorkPackageClicked(event:{ workPackageId:string; double:boolean }) { - if (event.double) { - this.openInFullView(event.workPackageId); - } + protected loadInitialQuery():void { + const isFirstLoad = !this.querySpace.initialized.hasValue(); + this.loadingIndicator = this.loadQuery(isFirstLoad); } - openStateLink(event:{ workPackageId:string; requestedState:'show'|'split' }) { - const params = { - workPackageId: event.workPackageId, - focus: true, - }; + protected set loadingIndicator(promise:Promise) { + this.loadingIndicatorService.list.promise = promise; + } - if (event.requestedState === 'split') { - this.keepTab.goCurrentDetailsState(params); + protected loadQuery(firstPage = false):Promise { + let promise:Promise; + const query = this.currentQuery; + + if (firstPage || !query) { + promise = this.loadFirstPage(); } else { - this.keepTab.goCurrentShowState(params); + const pagination = this.wpListService.getPaginationInfo(); + promise = firstValueFrom(this.wpListService.loadQueryFromExisting(query, pagination, this.projectIdentifier)); } + + return promise; } - /** - * Special handling for clicking on cards. - * If we are on mobile, a click on the card should directly open the full view - */ - handleWorkPackageCardClicked(event:{ workPackageId:string; double:boolean }):void { - if (this.deviceService.isMobile) { - this.openInFullView(event.workPackageId); - } else { - this.handleWorkPackageClicked(event); + protected loadFirstPage():Promise { + if (this.currentQuery) { + return firstValueFrom(this.wpListService.reloadQuery(this.currentQuery, this.projectIdentifier)); } + return this.wpListService.loadCurrentQueryFromParams(this.projectIdentifier); } - private openInFullView(workPackageId:string) { - this.$state.go( - 'work-packages.show', - { workPackageId }, - ); + public get projectIdentifier() { + return this.currentProject.identifier || undefined; } + } 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/_table_primerized.sass b/frontend/src/global_styles/layout/work_packages/_table_primerized.sass new file mode 100644 index 000000000000..0c932d1fc7c2 --- /dev/null +++ b/frontend/src/global_styles/layout/work_packages/_table_primerized.sass @@ -0,0 +1,36 @@ +//-- 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. +//++ + +.op-primerized-work-packages-list + .op-wp-list-view, + .op-wp-list-view--loading + height: 100% + + + wp-table + height: calc(100% - 55px) From 25f1aa590e3482e9a6c3ac35fb2aeef8db659d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Fri, 27 Sep 2024 10:55:21 +0200 Subject: [PATCH 07/28] New split view route --- app/controllers/work_packages_controller.rb | 25 ++++++++++++++-- app/views/work_packages/index.html.erb | 1 - config/routes.rb | 29 ++++++++----------- .../core/path-helper/path-helper.service.ts | 8 +++++ .../wp-primerized-list-view.component.ts | 2 +- .../macros/create_work_package_link.rb | 4 +-- .../menu_manager/top_menu/quick_add_menu.rb | 2 +- spec/features/attachments/attachments_spec.rb | 2 +- .../menu_items/quick_add_menu_spec.rb | 2 +- .../types/form_configuration_query_spec.rb | 2 +- .../attachments/attachment_upload_spec.rb | 2 +- .../work_packages/cancel_editing_spec.rb | 2 +- .../relations/hierarchy_custom_fields_spec.rb | 2 +- .../new/new_work_package_spec.rb | 6 ++-- .../work_packages/table/switch_types_spec.rb | 2 +- .../work_packages/work_packages_page.rb | 2 +- .../work_packages/full_work_package_create.rb | 6 ++-- 17 files changed, 61 insertions(+), 38 deletions(-) diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index d69281d80dd6..c35fb90e8ad4 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -32,6 +32,7 @@ class WorkPackagesController < ApplicationController include Layout include WorkPackagesControllerHelper include OpTurbo::DialogStreamHelper + include WorkPackages::WithSplitView accept_key_auth :index, :show @@ -40,9 +41,9 @@ class WorkPackagesController < ApplicationController before_action :load_and_authorize_in_optional_project, :check_allowed_export, :protect_from_unauthorized_export, only: %i[index export_dialog] - authorization_checked! :index, :show, :export_dialog + authorization_checked! :index, :show, :export_dialog, :split_view - before_action :load_and_validate_query, only: :index + before_action :load_and_validate_query, only: %i[index split_view] before_action :load_work_packages, only: :index, if: -> { request.format.atom? } before_action :load_and_validate_query_for_export, only: :export_dialog @@ -85,12 +86,32 @@ def show end end + def split_view + respond_to do |format| + format.html do + if turbo_frame_request? + render "work_packages/split_view", layout: false + else + render :index + end + end + end + end + def export_dialog respond_with_dialog WorkPackages::Exports::ModalDialogComponent.new(query: @query, project: @project, title: params[:title]) end 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 diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index 70ada0979a7f..049c29b32fab 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -48,7 +48,6 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% 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) %> <%= render(split_view_instance) if render_work_package_split_view? %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 15fcf1fe6066..b90a2309975b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -93,6 +93,14 @@ 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, + work_package_split_view: true + end + scope controller: "account" do get "/account/force_password_change", action: "force_password_change" post "/account/change_password", action: "change_password" @@ -317,21 +325,16 @@ # 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 + 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 :new_split 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: "" end resources :activity, :activities, only: :index, controller: "activities" do @@ -728,14 +731,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/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts index 1b2274db1c3c..bae000eae546 100644 --- a/frontend/src/app/core/path-helper/path-helper.service.ts +++ b/frontend/src/app/core/path-helper/path-helper.service.ts @@ -272,6 +272,14 @@ 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}`; + } + public workPackageDetailsCopyPath(projectIdentifier:string, workPackageId:string|number) { return this.workPackageDetailsPath(projectIdentifier, workPackageId, 'copy'); } 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 index db2cfb40c248..18538b72aba9 100644 --- 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 @@ -84,7 +84,7 @@ export class WorkPackagePrimerizedListViewComponent extends WorkPackageListViewC let link:string; if (this.currentProject.identifier) { - link = this.pathHelper.workPackageDetailsPath(this.currentProject.identifier, workPackageId, tabIdentifier); + link = this.pathHelper.workPackagePrimerDetailsPath(this.currentProject.identifier, workPackageId, tabIdentifier); } else { link = this.pathHelper.workPackageDetailsPath(workPackageId, tabIdentifier); } 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/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 c777fbd811b9..df92640a2ef3 100644 --- a/spec/features/types/form_configuration_query_spec.rb +++ b/spec/features/types/form_configuration_query_spec.rb @@ -134,7 +134,7 @@ expect(page).to have_css(".op-toast.-success", text: "Successful update.", wait: 10) # 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 bef64bedac71..13efc0f96427 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/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 From da1c1cce4aa3cb122c88ac2baaad1fdb202ff3b4 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Thu, 10 Oct 2024 13:34:28 +0200 Subject: [PATCH 08/28] Add a splitCreate component --- .../wp_create_button_component.html.erb | 5 +- .../create/wp_create_button_component.rb | 5 +- .../split_create_component.html.erb | 15 ++ .../work_packages/split_create_component.rb | 14 ++ .../split_view_component.html.erb | 4 +- app/controllers/work_packages_controller.rb | 19 +- .../work_packages/split_view_helper.rb | 20 +- app/views/work_packages/index.html.erb | 2 +- app/views/work_packages/split_view.html.erb | 2 +- config/routes.rb | 9 +- frontend/src/app/app.module.ts | 195 ++++++------------ .../core/path-helper/path-helper.service.ts | 4 + .../wp-edit-actions-bar.component.ts | 20 +- .../components/wp-new/wp-create.component.ts | 46 ++++- .../wp-new/wp-new-split-view.component.ts | 7 +- .../components/wp-new/wp-new-split-view.html | 6 +- .../openproject-work-packages.module.ts | 2 + .../routing/split-view-routes.helper.ts | 2 +- .../wp-split-view-entry.component.ts | 60 ++++++ .../layout/work_packages/_details_view.sass | 2 +- 20 files changed, 269 insertions(+), 170 deletions(-) create mode 100644 app/components/work_packages/split_create_component.html.erb create mode 100644 app/components/work_packages/split_create_component.rb create mode 100644 frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-view-entry.component.ts 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 index 552202489a4a..83a4ce71c3fe 100644 --- a/app/components/work_packages/create/wp_create_button_component.html.erb +++ b/app/components/work_packages/create/wp_create_button_component.html.erb @@ -2,11 +2,14 @@ <%= render Primer::Alpha::ActionMenu.new do |menu| menu.with_show_button(scheme: :primary, - aria: { label: I18n.t(:label_work_package_new) }) do |button| + 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, diff --git a/app/components/work_packages/create/wp_create_button_component.rb b/app/components/work_packages/create/wp_create_button_component.rb index 11330c4831b6..4d81fd978c12 100644 --- a/app/components/work_packages/create/wp_create_button_component.rb +++ b/app/components/work_packages/create/wp_create_button_component.rb @@ -49,9 +49,10 @@ def items def create_href_for_type(type) # TODO: make configurable for other modules if @project - new_split_project_work_packages_path(@project, type: type.id) + split_create_project_work_packages_path(@project, type: type.id) else - new_split_work_packages_path(type: type.id) + # TODO + # split_create_work_packages_path(type: type.id) end end diff --git a/app/components/work_packages/split_create_component.html.erb b/app/components/work_packages/split_create_component.html.erb new file mode 100644 index 000000000000..4d8bfe33f5bc --- /dev/null +++ b/app/components/work_packages/split_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_create_component.rb b/app/components/work_packages/split_create_component.rb new file mode 100644 index 000000000000..e489249bb3a9 --- /dev/null +++ b/app/components/work_packages/split_create_component.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class WorkPackages::SplitCreateComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:, project:, base_route:) + super + + @type = type + @project = project + @base_route = base_route + end +end diff --git a/app/components/work_packages/split_view_component.html.erb b/app/components/work_packages/split_view_component.html.erb index 754c80dadea0..6a5c40d728dc 100644 --- a/app/components/work_packages/split_view_component.html.erb +++ b/app/components/work_packages/split_view_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 @@ -18,7 +18,7 @@ helpers.angular_component_tag "opce-wp-split-view", inputs: { work_package_id: @id, - resizerClass: "op-work-package-split-view", + resizerClass: helpers.container_class, activeTab: @tab } end diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index c35fb90e8ad4..54b32a0d002c 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -41,9 +41,10 @@ class WorkPackagesController < ApplicationController before_action :load_and_authorize_in_optional_project, :check_allowed_export, :protect_from_unauthorized_export, only: %i[index export_dialog] - authorization_checked! :index, :show, :export_dialog, :split_view + authorization_checked! :index, :show, :export_dialog, :split_view, :split_create + before_action :find_project_by_project_id, only: %i[split_view split_create] - before_action :load_and_validate_query, only: %i[index split_view] + before_action :load_and_validate_query, only: %i[index split_view split_create] before_action :load_work_packages, only: :index, if: -> { request.format.atom? } before_action :load_and_validate_query_for_export, only: :export_dialog @@ -92,7 +93,19 @@ def split_view if turbo_frame_request? render "work_packages/split_view", layout: false else - render :index + render :index, locals: { query: @query, project: @project, menu_name: project_or_global_menu } + end + end + end + end + + def split_create + 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 diff --git a/app/helpers/work_packages/split_view_helper.rb b/app/helpers/work_packages/split_view_helper.rb index 5f82668bd199..70fa436ac7e3 100644 --- a/app/helpers/work_packages/split_view_helper.rb +++ b/app/helpers/work_packages/split_view_helper.rb @@ -1,11 +1,21 @@ module WorkPackages::SplitViewHelper def render_work_package_split_view? - params[:work_package_split_view].present? + params[:work_package_split_view].present? || params[:work_package_split_create].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(project: nil) + if params[:work_package_split_view] + WorkPackages::SplitViewComponent.new(id: params[:work_package_id], + tab: params[:tab], + base_route: split_view_base_route) + elsif params[:work_package_split_create].present? + WorkPackages::SplitCreateComponent.new(type: params[:type], + project:, + base_route: split_view_base_route) + end + end + + def container_class + "op-work-package-split-view" end end diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index 049c29b32fab..9c5b8ae9cdfc 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -48,7 +48,7 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% content_for :content_body_right do %> - <%= render(split_view_instance) if render_work_package_split_view? %> + <%= render(split_view_instance(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/split_view.html.erb b/app/views/work_packages/split_view.html.erb index 9f27d274a393..368f108d5391 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(project: @project)) %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 51fdf5bb988f..8d147200ab55 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,6 +101,13 @@ work_package_split_view: true end + concern :with_split_create do |options| + get "split_create", + action: options.fetch(:action, :split_create), + as: :split_create, + work_package_split_create: true + end + scope controller: "account" do get "/account/force_password_change", action: "force_password_change" post "/account/change_password", action: "change_password" @@ -331,12 +338,12 @@ 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 :new_split end end diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index e3bd260c5664..fa30cc4e1621 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,48 +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 { PreviewTriggerService } from 'core-app/core/setup/globals/global-listeners/preview-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 { - WpPreviewModalComponent, -} from 'core-app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.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 { PasswordConfirmationModalComponent } from 'core-app/shared/components/modals/request-for-confirmation/password-confirmation.modal'; +import { WpPreviewModalComponent } from 'core-app/shared/components/modals/preview-modal/wp-preview-modal/wp-preview.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 { PaginationService } from 'core-app/shared/components/table-pagination/pagination-service'; import { MainMenuResizerComponent } from 'core-app/shared/components/resizer/resizer/main-menu-resizer.component'; @@ -93,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'; @@ -108,138 +96,76 @@ 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 { - WorkPackagePrimerizedListViewComponent, -} from 'core-app/features/work-packages/routing/wp-list-view/wp-primerized-list-view.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-view-entry.component'; export function initializeServices(injector:Injector) { return () => { @@ -432,6 +358,7 @@ 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-timer-account-menu', TimerAccountMenuComponent, { injector }); registerCustomElement('opce-remote-field-updater', RemoteFieldUpdaterComponent, { injector }); registerCustomElement('opce-modal-single-date-picker', OpModalSingleDatePickerComponent, { injector }); diff --git a/frontend/src/app/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts index bae000eae546..c51e511a993c 100644 --- a/frontend/src/app/core/path-helper/path-helper.service.ts +++ b/frontend/src/app/core/path-helper/path-helper.service.ts @@ -264,6 +264,10 @@ export class PathHelperService { 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}`; 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-new/wp-create.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts index ad9be787f910..f0a78567b70a 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts @@ -30,6 +30,7 @@ import { ChangeDetectorRef, Directive, Injector, + Input, OnInit, ViewChild, } from '@angular/core'; @@ -52,7 +53,10 @@ import * as URI from 'urijs'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { HalResource, HalSource } from 'core-app/features/hal/resources/hal-resource'; +import { + HalResource, + HalSource, +} from 'core-app/features/hal/resources/hal-resource'; import { OpTitleService } from 'core-app/core/html/op-title.service'; import { WorkPackageCreateService } from './wp-create.service'; import { HalError } from 'core-app/features/hal/services/hal-error'; @@ -62,7 +66,7 @@ import idFromLink from 'core-app/features/hal/helpers/id-from-link'; export class WorkPackageCreateComponent extends UntilDestroyedMixin implements OnInit { public successState:string = splitViewRoute(this.$state); - public cancelState:string = this.$state.current.data.baseRoute; + public cancelState:string = this.$state?.current?.data?.baseRoute; public newWorkPackage:WorkPackageResource; @@ -73,7 +77,10 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O /** Are we in the copying substates ? */ public copying = false; - public stateParams = this.$transition.params('to'); + @Input() public stateParams:any; + //public stateParams:{ parent_id?:string, type:string, projectPath?:string, defaults?:unknown } + + @Input() public routedFromAngular:boolean = true; public text = { button_settings: this.I18n.t('js.button_settings'), @@ -86,7 +93,6 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O constructor( public readonly injector:Injector, - protected readonly $transition:Transition, protected readonly $state:StateService, protected readonly I18n:I18nService, protected readonly titleService:OpTitleService, @@ -97,11 +103,18 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O protected readonly wpTableFilters:WorkPackageViewFiltersService, protected readonly pathHelper:PathHelperService, protected readonly apiV3Service:ApiV3Service, - protected readonly cdRef:ChangeDetectorRef) { + protected readonly cdRef:ChangeDetectorRef, + ) { super(); } public ngOnInit() { + // In case the create form is still routed via Angular, the stateParams are empty. We then read the params from the Transition + if (this.routedFromAngular) { + const transition = this.injector.get(Transition); + this.stateParams = transition.params('to'); + } + this.closeEditFormWhenNewWorkPackageSaved(); this.showForm(); @@ -112,8 +125,13 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O } public switchToFullscreen() { - const type = idFromLink(this.change.value('type')?.href); - void this.$state.go('work-packages.new', { ...this.$state.params, type }); + if (this.routedFromAngular) { + const type = idFromLink(this.change.value('type')?.href); + void this.$state.go('work-packages.new', { ...this.$state.params, type }); + } else { + 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 +202,19 @@ 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 showNewWorkPackage(wp:WorkPackageResource) { + if (wp.id) { + const link = this.pathHelper.workPackageDetailsPath(this.stateParams.projectPath, wp.id) + window.location.search; + Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' }); + } } protected createdWorkPackage() { 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..85557588f181 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)="showNewWorkPackage($event)" + >
    -
    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 0450fa271aee..3e4514c40ddd 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 @@ -419,6 +419,7 @@ import { 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-view-entry.component'; @NgModule({ imports: [ @@ -612,6 +613,7 @@ import { WorkPackageDetailsViewButtonComponent, WorkPackageSplitViewComponent, WorkPackageSplitViewEntryComponent, + WorkPackageSplitCreateEntryComponent, WorkPackageBreadcrumbComponent, WorkPackageSplitViewToolbarComponent, WorkPackageWatcherButtonComponent, 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/wp-split-create/wp-split-view-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-view-entry.component.ts new file mode 100644 index 000000000000..404edc86b5b3 --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-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 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); + } +} 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 From 27ad1d84efbe774b0ef6c797f720258d073a7bc9 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Thu, 10 Oct 2024 14:14:14 +0200 Subject: [PATCH 09/28] Enable splitView and splitCreate on global WorkPackage page --- .../work_packages/create/wp_create_button_component.rb | 3 +-- app/controllers/application_controller.rb | 6 ++++++ app/controllers/work_packages_controller.rb | 2 +- config/routes.rb | 10 +++++----- .../components/wp-new/wp-create.component.ts | 2 +- .../wp-list-view/wp-primerized-list-view.component.ts | 9 +-------- modules/budgets/app/controllers/budgets_controller.rb | 6 ------ .../costs/app/controllers/hourly_rates_controller.rb | 6 ------ 8 files changed, 15 insertions(+), 29 deletions(-) diff --git a/app/components/work_packages/create/wp_create_button_component.rb b/app/components/work_packages/create/wp_create_button_component.rb index 4d81fd978c12..93e50ea3d207 100644 --- a/app/components/work_packages/create/wp_create_button_component.rb +++ b/app/components/work_packages/create/wp_create_button_component.rb @@ -51,8 +51,7 @@ def create_href_for_type(type) if @project split_create_project_work_packages_path(@project, type: type.id) else - # TODO - # split_create_work_packages_path(type: type.id) + split_create_work_packages_path(type: type.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 54b32a0d002c..7fb3dea4ef33 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -41,8 +41,8 @@ class WorkPackagesController < ApplicationController before_action :load_and_authorize_in_optional_project, :check_allowed_export, :protect_from_unauthorized_export, only: %i[index export_dialog] + before_action :find_optional_project, only: %i[split_view split_create] authorization_checked! :index, :show, :export_dialog, :split_view, :split_create - before_action :find_project_by_project_id, only: %i[split_view split_create] before_action :load_and_validate_query, only: %i[index split_view split_create] before_action :load_work_packages, only: :index, if: -> { request.format.atom? } diff --git a/config/routes.rb b/config/routes.rb index 8d147200ab55..d4bf0faf96b1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -588,16 +588,17 @@ # 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 - 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 "/split_view/update_counter" => "work_packages/split_view#update_counter", @@ -605,10 +606,9 @@ # 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 "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|split_view|split_create)).+/ } get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" get "/edit" => "work_packages#show", on: :member, as: "edit" end diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts index f0a78567b70a..cb8af3dbf354 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts @@ -212,7 +212,7 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O public showNewWorkPackage(wp:WorkPackageResource) { if (wp.id) { - const link = this.pathHelper.workPackageDetailsPath(this.stateParams.projectPath, wp.id) + window.location.search; + const link = this.pathHelper.workPackageDetailsPath(wp.project.identifier, wp.id) + window.location.search; Turbo.visit(link, { frame: 'content-bodyRight', action: 'advance' }); } } 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 index 18538b72aba9..ee5264beb462 100644 --- 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 @@ -81,14 +81,7 @@ export class WorkPackagePrimerizedListViewComponent extends WorkPackageListViewC } openSplitScreen(workPackageId:string, tabIdentifier:string = 'activity'):void { - let link:string; - - if (this.currentProject.identifier) { - link = this.pathHelper.workPackagePrimerDetailsPath(this.currentProject.identifier, workPackageId, tabIdentifier); - } else { - link = this.pathHelper.workPackageDetailsPath(workPackageId, tabIdentifier); - } - + let link = this.pathHelper.workPackagePrimerDetailsPath(this.currentProject.identifier, workPackageId, tabIdentifier); Turbo.visit(link + window.location.search, { frame: 'content-bodyRight', action: 'advance' }); } diff --git a/modules/budgets/app/controllers/budgets_controller.rb b/modules/budgets/app/controllers/budgets_controller.rb index e9b626e2792a..18dfafc4956d 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 c9077d4ad5c3..b5d940d2b30e 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 From 3871930e0fffea14544ebc896814aabfe5b57b07 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Fri, 11 Oct 2024 14:09:17 +0200 Subject: [PATCH 10/28] Pass splitViewType along to be able to differentiate between the components to show --- app/controllers/work_packages_controller.rb | 32 ++++++++----------- .../work_packages/split_view_helper.rb | 11 ++++--- app/views/notifications/index.html.erb | 2 +- app/views/work_packages/index.html.erb | 2 +- app/views/work_packages/split_view.html.erb | 2 +- config/routes.rb | 4 +-- .../wp-primerized-list-view.component.ts | 2 +- 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 7fb3dea4ef33..89961a29a731 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -88,27 +88,11 @@ def show end def 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 + render_split_view end def split_create - 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 + render_split_view end def export_dialog @@ -226,4 +210,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 70fa436ac7e3..5c40d1596b10 100644 --- a/app/helpers/work_packages/split_view_helper.rb +++ b/app/helpers/work_packages/split_view_helper.rb @@ -1,17 +1,20 @@ module WorkPackages::SplitViewHelper def render_work_package_split_view? - params[:work_package_split_view].present? || params[:work_package_split_create].present? + params[:view_type].present? end - def split_view_instance(project: nil) - if params[:work_package_split_view] + def split_view_instance(view_type:, project: nil) + case view_type + when "work_package_split_view" WorkPackages::SplitViewComponent.new(id: params[:work_package_id], tab: params[:tab], base_route: split_view_base_route) - elsif params[:work_package_split_create].present? + when "work_package_split_create" WorkPackages::SplitCreateComponent.new(type: params[:type], project:, base_route: split_view_base_route) + else + # TODO 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/index.html.erb b/app/views/work_packages/index.html.erb index 9c5b8ae9cdfc..84bc6afe065d 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -48,7 +48,7 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <% content_for :content_body_right do %> - <%= render(split_view_instance(project: @project)) if render_work_package_split_view? %> + <%= 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/split_view.html.erb b/app/views/work_packages/split_view.html.erb index 368f108d5391..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(project: @project)) %> + <%= render(split_view_instance(view_type: params[:view_type], project: @project)) %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index f63b22492edd..5cbe09d0cf74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -98,14 +98,14 @@ action: options.fetch(:action, :split_view), defaults: { tab: :overview }, as: :details, - work_package_split_view: true + 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, - work_package_split_create: true + view_type: "work_package_split_create" end scope controller: "account" do 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 index ee5264beb462..ae0f6c4977dd 100644 --- 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 @@ -80,7 +80,7 @@ export class WorkPackagePrimerizedListViewComponent extends WorkPackageListViewC } } - openSplitScreen(workPackageId:string, tabIdentifier:string = 'activity'):void { + 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' }); } From d83886e5e6cbf450bbc9a7ed4f1263dc1fa2350b Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Fri, 11 Oct 2024 15:07:52 +0200 Subject: [PATCH 11/28] Create a FullView::CopyComponent for WorkPackages which is routed from rails --- .../full_view/copy_component.html.erb | 11 ++++ .../work_packages/full_view/copy_component.rb | 18 ++++++ app/controllers/work_packages_controller.rb | 15 ++++- app/views/work_packages/copy.html.erb | 45 ++++++++++++++ config/initializers/permissions.rb | 2 +- config/routes.rb | 6 +- frontend/src/app/app.module.ts | 4 +- .../core/path-helper/path-helper.service.ts | 7 ++- .../openproject-work-packages.module.ts | 4 +- .../routing/work-packages-routes.ts | 13 ---- .../wp-full-copy-entry.component.ts | 61 +++++++++++++++++++ ....ts => wp-split-create-entry.component.ts} | 2 + .../wp-split-view-entry.component.ts | 2 + .../work-package-authorization.service.ts | 2 +- .../wp-context-menu/wp-single-context-menu.ts | 4 +- .../wp-view-context-menu.directive.ts | 10 ++- 16 files changed, 177 insertions(+), 29 deletions(-) create mode 100644 app/components/work_packages/full_view/copy_component.html.erb create mode 100644 app/components/work_packages/full_view/copy_component.rb create mode 100644 app/views/work_packages/copy.html.erb create mode 100644 frontend/src/app/features/work-packages/routing/wp-full-copy/wp-full-copy-entry.component.ts rename frontend/src/app/features/work-packages/routing/wp-split-create/{wp-split-view-entry.component.ts => wp-split-create-entry.component.ts} (96%) 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/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 89961a29a731..4ff6b826a321 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -41,10 +41,10 @@ class WorkPackagesController < ApplicationController before_action :load_and_authorize_in_optional_project, :check_allowed_export, :protect_from_unauthorized_export, only: %i[index export_dialog] - before_action :find_optional_project, only: %i[split_view split_create] - authorization_checked! :index, :show, :export_dialog, :split_view, :split_create + before_action :find_optional_project, only: %i[split_view split_create copy] + authorization_checked! :index, :show, :copy, :export_dialog, :split_view, :split_create - before_action :load_and_validate_query, only: %i[index split_view split_create] + before_action :load_and_validate_query, only: %i[index split_view split_create copy] before_action :load_work_packages, only: :index, if: -> { request.format.atom? } before_action :load_and_validate_query_for_export, only: :export_dialog @@ -95,6 +95,15 @@ 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 export_dialog respond_with_dialog WorkPackages::Exports::ModalDialogComponent.new(query: @query, project: @project, title: params[:title]) 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/config/initializers/permissions.rb b/config/initializers/permissions.rb index ba0f17015869..81b755a14767 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -214,7 +214,7 @@ { versions: %i[index show status_by], journals: %i[index], - work_packages: %i[show index], + work_packages: %i[show index copy], work_packages_api: [:get], "work_packages/reports": %i[report report_details], "work_packages/menus": %i[show], diff --git a/config/routes.rb b/config/routes.rb index 5cbe09d0cf74..01d31b285e2a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -345,6 +345,8 @@ get "menu" => "work_packages/menus#show" get "/export_dialog" => "work_packages#export_dialog" end + + get "/copy" => "work_packages#copy", on: :member, as: "copy" end resources :activity, :activities, only: :index, controller: "activities" do @@ -606,11 +608,13 @@ get "/split_view/update_counter" => "work_packages/split_view#update_counter", on: :member + get "/copy" => "work_packages#copy", on: :member, as: "copy" + # states managed by client-side (angular) routing on work_package#show get "/" => "work_packages#index", on: :collection, as: "index" 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|split_create)).+/ } + get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|split_view|split_create|copy)).+/ } get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" get "/edit" => "work_packages#show", on: :member, as: "edit" end diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6d39c3551f92..b632f1400af1 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -164,7 +164,8 @@ import { } from 'core-app/core/routing/base/application-base.component'; import { SpotSwitchComponent } from 'core-app/spot/components/switch/switch.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-view-entry.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'; export function initializeServices(injector:Injector) { return () => { @@ -358,6 +359,7 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-share-upsale', ShareUpsaleComponent, { injector }); registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector }); registerCustomElement('opce-wp-split-create', WorkPackageSplitCreateEntryComponent, { injector }); + registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector }); registerCustomElement('opce-timer-account-menu', TimerAccountMenuComponent, { injector }); registerCustomElement('opce-remote-field-updater', RemoteFieldUpdaterComponent, { injector }); registerCustomElement('opce-modal-single-date-picker', OpModalSingleDatePickerComponent, { injector }); diff --git a/frontend/src/app/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts index a5eeac080334..0e4aa7c4ac63 100644 --- a/frontend/src/app/core/path-helper/path-helper.service.ts +++ b/frontend/src/app/core/path-helper/path-helper.service.ts @@ -260,7 +260,11 @@ export class PathHelperService { 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`; } @@ -284,6 +288,7 @@ export class PathHelperService { 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/work-packages/openproject-work-packages.module.ts b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts index 3e4514c40ddd..e050091ddd7f 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 @@ -419,7 +419,8 @@ import { 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-view-entry.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'; @NgModule({ imports: [ @@ -614,6 +615,7 @@ import { WorkPackageSplitCreateEntryComponent } from 'core-app/features/work-pac WorkPackageSplitViewComponent, WorkPackageSplitViewEntryComponent, WorkPackageSplitCreateEntryComponent, + WorkPackageFullCopyEntryComponent, WorkPackageBreadcrumbComponent, WorkPackageSplitViewToolbarComponent, WorkPackageWatcherButtonComponent, 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..7cfca479ddb4 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 @@ -88,19 +88,6 @@ export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [ 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]+}', 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-split-create/wp-split-view-entry.component.ts b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts similarity index 96% rename from frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-view-entry.component.ts rename to frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts index 404edc86b5b3..53fb9cfee7fa 100644 --- a/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-view-entry.component.ts +++ b/frontend/src/app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component.ts @@ -56,5 +56,7 @@ export class WorkPackageSplitCreateEntryComponent { 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/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/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() { From 2f265770d7530b6c97c70092d06a0e16e8bc2741 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Fri, 11 Oct 2024 15:14:56 +0200 Subject: [PATCH 12/28] Harmonize code structure due to increased number of components --- app/components/_index.sass | 2 +- .../work_packages/split_create_component.rb | 14 ----------- .../create_component.html.erb} | 0 .../split_view/create_component.rb | 18 +++++++++++++ .../show_component.html.erb} | 0 .../split_view/show_component.rb | 25 +++++++++++++++++++ .../show_component.sass} | 0 .../work_packages/split_view_component.rb | 21 ---------------- .../work_packages/split_view_helper.rb | 12 ++++----- .../split_view_component_preview.rb | 5 ++-- .../show_component_spec.rb} | 2 +- 11 files changed, 54 insertions(+), 45 deletions(-) delete mode 100644 app/components/work_packages/split_create_component.rb rename app/components/work_packages/{split_create_component.html.erb => split_view/create_component.html.erb} (100%) create mode 100644 app/components/work_packages/split_view/create_component.rb rename app/components/work_packages/{split_view_component.html.erb => split_view/show_component.html.erb} (100%) create mode 100644 app/components/work_packages/split_view/show_component.rb rename app/components/work_packages/{split_view_component.sass => split_view/show_component.sass} (100%) delete mode 100644 app/components/work_packages/split_view_component.rb rename spec/components/work_packages/{split_view_component_spec.rb => split_view/show_component_spec.rb} (92%) diff --git a/app/components/_index.sass b/app/components/_index.sass index 2864192e81fd..8be940672705 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -3,7 +3,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/work_packages/split_create_component.rb b/app/components/work_packages/split_create_component.rb deleted file mode 100644 index e489249bb3a9..000000000000 --- a/app/components/work_packages/split_create_component.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class WorkPackages::SplitCreateComponent < ApplicationComponent - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable - - def initialize(type:, project:, base_route:) - super - - @type = type - @project = project - @base_route = base_route - end -end diff --git a/app/components/work_packages/split_create_component.html.erb b/app/components/work_packages/split_view/create_component.html.erb similarity index 100% rename from app/components/work_packages/split_create_component.html.erb rename to app/components/work_packages/split_view/create_component.html.erb 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 100% rename from app/components/work_packages/split_view_component.html.erb rename to app/components/work_packages/split_view/show_component.html.erb 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/helpers/work_packages/split_view_helper.rb b/app/helpers/work_packages/split_view_helper.rb index 5c40d1596b10..3a790b8da7c7 100644 --- a/app/helpers/work_packages/split_view_helper.rb +++ b/app/helpers/work_packages/split_view_helper.rb @@ -6,13 +6,13 @@ def render_work_package_split_view? def split_view_instance(view_type:, project: nil) case view_type when "work_package_split_view" - WorkPackages::SplitViewComponent.new(id: params[:work_package_id], - tab: params[:tab], - base_route: split_view_base_route) + WorkPackages::SplitView::ShowComponent.new(id: params[:work_package_id], + tab: params[:tab], + base_route: split_view_base_route) when "work_package_split_create" - WorkPackages::SplitCreateComponent.new(type: params[:type], - project:, - base_route: split_view_base_route) + WorkPackages::SplitView::CreateComponent.new(type: params[:type], + project:, + base_route: split_view_base_route) else # TODO end 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/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) } From 251eb77835b5ed6d843b123c948511faad3fa96b Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Tue, 15 Oct 2024 08:47:40 +0200 Subject: [PATCH 13/28] Remove angular splitCopy route and component as it was overwritten by the angular fullCopy route for quite some time already and nobody complained. So we decided to remove the splitCopy completely --- .../wp-copy/wp-copy-split-view.component.ts | 38 ------------------- .../openproject-work-packages.module.ts | 4 -- .../routing/split-view-routes.template.ts | 19 ---------- 3 files changed, 61 deletions(-) delete mode 100644 frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts diff --git a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts b/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts deleted file mode 100644 index d49151dfcf9b..000000000000 --- a/frontend/src/app/features/work-packages/components/wp-copy/wp-copy-split-view.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -//-- 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 } from '@angular/core'; -import { WorkPackageCopyController } from 'core-app/features/work-packages/components/wp-copy/wp-copy.controller'; - -@Component({ - selector: 'wp-copy-split-view', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: '../wp-new/wp-new-split-view.html', -}) -export class WorkPackageCopySplitViewComponent extends WorkPackageCopyController { -} 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 e050091ddd7f..58e51f2fa4a1 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'; @@ -506,7 +503,6 @@ import { WorkPackageFullCopyEntryComponent } from 'core-app/features/work-packag // WP Copy WorkPackageCopyFullViewComponent, - WorkPackageCopySplitViewComponent, // Embedded table WorkPackageEmbeddedTableComponent, 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, - }, - }, ]; } From 84b50fb25edb4722c8588843fec4b8ff2c98d7c4 Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Tue, 15 Oct 2024 11:32:15 +0200 Subject: [PATCH 14/28] Create FullView::CreateComponent for WorkPackages which is now routed from rails instead of Angular --- .../full_view/create_component.html.erb | 7 +++ .../full_view/create_component.rb | 17 ++++++ app/controllers/work_packages_controller.rb | 15 ++++- app/views/work_packages/new.html.erb | 44 ++++++++++++++ config/initializers/permissions.rb | 10 +++- config/routes.rb | 2 +- frontend/src/app/app.module.ts | 2 + .../components/wp-new/wp-create.component.ts | 23 ++++--- .../wp-new/wp-new-full-view.component.ts | 2 +- .../components/wp-new/wp-new-full-view.html | 4 +- .../components/wp-new/wp-new-split-view.html | 2 +- .../openproject-work-packages.module.ts | 4 +- .../routing/work-packages-routes.ts | 20 ------- .../wp-full-create-entry.component.ts | 60 +++++++++++++++++++ .../op-types-context-menu.directive.ts | 5 -- 15 files changed, 168 insertions(+), 49 deletions(-) create mode 100644 app/components/work_packages/full_view/create_component.html.erb create mode 100644 app/components/work_packages/full_view/create_component.rb create mode 100644 app/views/work_packages/new.html.erb create mode 100644 frontend/src/app/features/work-packages/routing/wp-full-create/wp-full-create-entry.component.ts 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/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 4ff6b826a321..85e4414eb4f4 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -38,10 +38,10 @@ class WorkPackagesController < ApplicationController 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 :find_optional_project, only: %i[split_view split_create copy] + before_action :find_optional_project, only: %i[split_view split_create] + before_action :load_and_authorize_in_optional_project, only: %i[index export_dialog new copy] authorization_checked! :index, :show, :copy, :export_dialog, :split_view, :split_create before_action :load_and_validate_query, only: %i[index split_view split_create copy] @@ -104,6 +104,15 @@ def copy 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 export_dialog respond_with_dialog WorkPackages::Exports::ModalDialogComponent.new(query: @query, project: @project, title: params[:title]) end 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/config/initializers/permissions.rb b/config/initializers/permissions.rb index 6a6cd956d972..0248aa9a75ef 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -219,7 +219,7 @@ { versions: %i[index show status_by], journals: %i[index], - work_packages: %i[show index copy], + work_packages: %i[show index], work_packages_api: [:get], "work_packages/reports": %i[report report_details], "work_packages/menus": %i[show], @@ -230,7 +230,9 @@ 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 +254,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 01d31b285e2a..598beb1c7aea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -609,10 +609,10 @@ on: :member get "/copy" => "work_packages#copy", on: :member, as: "copy" + get "/new" => "work_packages#new", on: :collection, as: "new" # states managed by client-side (angular) routing on work_package#show get "/" => "work_packages#index", on: :collection, as: "index" - 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|split_create|copy)).+/ } get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index b632f1400af1..49bc54c83619 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -166,6 +166,7 @@ import { SpotSwitchComponent } from 'core-app/spot/components/switch/switch.comp 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'; export function initializeServices(injector:Injector) { return () => { @@ -359,6 +360,7 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-share-upsale', ShareUpsaleComponent, { injector }); registerCustomElement('opce-wp-split-view', WorkPackageSplitViewEntryComponent, { injector }); registerCustomElement('opce-wp-split-create', WorkPackageSplitCreateEntryComponent, { injector }); + registerCustomElement('opce-wp-full-create', WorkPackageFullCreateEntryComponent, { injector }); registerCustomElement('opce-wp-full-copy', WorkPackageFullCopyEntryComponent, { injector }); registerCustomElement('opce-timer-account-menu', TimerAccountMenuComponent, { injector }); registerCustomElement('opce-remote-field-updater', RemoteFieldUpdaterComponent, { injector }); diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts index cb8af3dbf354..565e0ac0c29c 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts @@ -53,14 +53,10 @@ import * as URI from 'urijs'; import { UntilDestroyedMixin } from 'core-app/shared/helpers/angular/until-destroyed.mixin'; import { splitViewRoute } from 'core-app/features/work-packages/routing/split-view-routes.helper'; import { ApiV3Service } from 'core-app/core/apiv3/api-v3.service'; -import { - HalResource, - HalSource, -} from 'core-app/features/hal/resources/hal-resource'; +import { HalSource } from 'core-app/features/hal/resources/hal-resource'; import { OpTitleService } from 'core-app/core/html/op-title.service'; import { WorkPackageCreateService } from './wp-create.service'; import { HalError } from 'core-app/features/hal/services/hal-error'; -import idFromLink from 'core-app/features/hal/helpers/id-from-link'; @Directive() export class WorkPackageCreateComponent extends UntilDestroyedMixin implements OnInit { @@ -125,13 +121,8 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O } public switchToFullscreen() { - if (this.routedFromAngular) { - const type = idFromLink(this.change.value('type')?.href); - void this.$state.go('work-packages.new', { ...this.$state.params, type }); - } else { - const link = this.stateParams.projectPath ? this.pathHelper.projectWorkPackageNewPath(this.stateParams.projectPath) : this.pathHelper.workPackageNewPath(); - window.location.href = link + window.location.search; - } + 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 }) { @@ -210,13 +201,19 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O } } - public showNewWorkPackage(wp:WorkPackageResource) { + 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() { const defaults:HalSource = (this.stateParams.defaults as HalSource) || {}; defaults._links = defaults._links || {}; 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.html b/frontend/src/app/features/work-packages/components/wp-new/wp-new-split-view.html index 85557588f181..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 @@ -24,7 +24,7 @@
    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 58e51f2fa4a1..b7c3a2ac7dd5 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 @@ -418,6 +418,7 @@ import { } 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'; @NgModule({ imports: [ @@ -611,7 +612,6 @@ import { WorkPackageFullCopyEntryComponent } from 'core-app/features/work-packag WorkPackageSplitViewComponent, WorkPackageSplitViewEntryComponent, WorkPackageSplitCreateEntryComponent, - WorkPackageFullCopyEntryComponent, WorkPackageBreadcrumbComponent, WorkPackageSplitViewToolbarComponent, WorkPackageWatcherButtonComponent, @@ -620,6 +620,8 @@ import { WorkPackageFullCopyEntryComponent } from 'core-app/features/work-packag // Full view WorkPackagesFullViewComponent, + WorkPackageFullCopyEntryComponent, + WorkPackageFullCreateEntryComponent, // Modals WpTableConfigurationModalComponent, 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 7cfca479ddb4..af834e8b867d 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'; @@ -69,25 +68,6 @@ export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [ name: { type: 'string', dynamic: true }, }, }, - { - name: 'work-packages.new', - url: '/new?type&parent_id', - component: WorkPackageNewFullViewComponent, - reloadOnSearch: false, - params: { - defaults: { - value: null, - }, - }, - data: { - baseRoute: 'work-packages', - allowMovingInEditMode: true, - bodyClasses: 'router--work-packages-full-create', - menuItem: menuItemClass, - successState: 'work-packages.show', - sideMenuOptions, - }, - }, { name: 'work-packages.show', url: '/{workPackageId:[0-9]+}', 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/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..e8b4b63e7c54 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 @@ -69,11 +69,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) { From 8a46e334c80f7dc75ca41645049e8edfa4805b5d Mon Sep 17 00:00:00 2001 From: Henriette Darge Date: Wed, 16 Oct 2024 13:13:44 +0200 Subject: [PATCH 15/28] First draft of implementing the FullView route for WorkPackages from rails --- .../full_view/show_component.html.erb | 17 +++ .../work_packages/full_view/show_component.rb | 24 ++++ .../split_view/show_component.html.erb | 2 +- app/controllers/work_packages_controller.rb | 5 +- app/views/work_packages/show.html.erb | 4 + config/routes.rb | 4 +- frontend/src/app/app.module.ts | 2 + .../wp-subject/wp-subject.component.ts | 29 ++--- .../wp-tab-wrapper.component.ts | 19 ++- .../openproject-work-packages.module.ts | 2 + .../routing/work-packages-routes.ts | 122 +++++++++--------- .../wp-full-view-entry.component.ts | 58 +++++++++ .../wp-full-view/wp-full-view.component.ts | 10 +- .../routing/wp-full-view/wp-full-view.html | 10 +- .../wp-split-view/wp-split-view.component.ts | 4 +- .../work-package-single-view.base.ts | 16 ++- 16 files changed, 216 insertions(+), 112 deletions(-) create mode 100644 app/components/work_packages/full_view/show_component.html.erb create mode 100644 app/components/work_packages/full_view/show_component.rb create mode 100644 frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts 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..e417940339d1 --- /dev/null +++ b/app/components/work_packages/full_view/show_component.html.erb @@ -0,0 +1,17 @@ +<%= + if @work_package.nil? + render(Primer::Beta::Blankslate.new(spacious: true)) do |component| + component.with_visual_icon(icon: :inbox) + component.with_heading(tag: :h2).with_content( + I18n.t(:error_work_package_id_not_found) + ) + end + else + #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 + } + 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..14f705c3e495 --- /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: "overview") + 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/split_view/show_component.html.erb b/app/components/work_packages/split_view/show_component.html.erb index 6a5c40d728dc..a23b678ef749 100644 --- a/app/components/work_packages/split_view/show_component.html.erb +++ b/app/components/work_packages/split_view/show_component.html.erb @@ -17,7 +17,7 @@ flex.with_row(flex: 1) do helpers.angular_component_tag "opce-wp-split-view", inputs: { - work_package_id: @id, + workPackageId: @id, resizerClass: helpers.container_class, activeTab: @tab } diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 85e4414eb4f4..69d6ca48e7c6 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -42,7 +42,7 @@ class WorkPackagesController < ApplicationController :protect_from_unauthorized_export, only: %i[index export_dialog] before_action :find_optional_project, only: %i[split_view split_create] before_action :load_and_authorize_in_optional_project, only: %i[index export_dialog new copy] - authorization_checked! :index, :show, :copy, :export_dialog, :split_view, :split_create + authorization_checked! :index, :show, :new, :copy, :export_dialog, :split_view, :split_create before_action :load_and_validate_query, only: %i[index split_view split_create copy] before_action :load_work_packages, only: :index, if: -> { request.format.atom? } @@ -69,8 +69,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 diff --git a/app/views/work_packages/show.html.erb b/app/views/work_packages/show.html.erb index 136812d91752..e9ebadbf0e60 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) %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 598beb1c7aea..0f3d981f2e1b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -347,6 +347,7 @@ end 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 @@ -610,11 +611,12 @@ 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" # 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|split_create|copy)).+/ } + #get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/, state: /(?!(shares|split_view|split_create|copy)).+/ } get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" get "/edit" => "work_packages#show", on: :member, as: "edit" end diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 49bc54c83619..c9e9202560d7 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -167,6 +167,7 @@ import { WorkPackagePrimerizedListViewComponent } from 'core-app/features/work-p 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'; export function initializeServices(injector:Injector) { return () => { @@ -360,6 +361,7 @@ export class OpenProjectModule implements DoBootstrap { 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-timer-account-menu', TimerAccountMenuComponent, { injector }); 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-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..e74db882c608 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,15 +55,12 @@ 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() { this.ndcDynamicInputs$ = this @@ -76,8 +77,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/openproject-work-packages.module.ts b/frontend/src/app/features/work-packages/openproject-work-packages.module.ts index b7c3a2ac7dd5..bf8d29a22184 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 @@ -419,6 +419,7 @@ import { 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'; @NgModule({ imports: [ @@ -620,6 +621,7 @@ import { WorkPackageFullCreateEntryComponent } from 'core-app/features/work-pack // Full view WorkPackagesFullViewComponent, + WorkPackageFullViewEntryComponent, WorkPackageFullCopyEntryComponent, WorkPackageFullCreateEntryComponent, 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 af834e8b867d..be13eabc6c06 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 @@ -68,67 +68,67 @@ export const WORK_PACKAGES_ROUTES:Ng2StateDeclaration[] = [ 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, - ), + // { + // 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', 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..995859d37811 --- /dev/null +++ b/frontend/src/app/features/work-packages/routing/wp-full-view/wp-full-view-entry.component.ts @@ -0,0 +1,58 @@ +//-- 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 dc09674332e1..8d5f8ada3b9f 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 @@ -33,7 +33,7 @@ import { Injector, 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'; @@ -41,12 +41,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: [ @@ -73,17 +72,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); } ngOnInit():void { 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..9e0d18a3dbbd 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,7 +16,7 @@
    - +