diff --git a/app/components/work_packages/project_attributes_tab_component.html.erb b/app/components/work_packages/project_attributes_tab_component.html.erb
new file mode 100644
index 00000000000..74c9d3ccd99
--- /dev/null
+++ b/app/components/work_packages/project_attributes_tab_component.html.erb
@@ -0,0 +1,19 @@
+
+ <%=
+ component_wrapper do
+ if project_custom_fields_grouped_by_section.any?
+ render(Primer::OpenProject::SidePanel.new(spacious: true)) do |panel|
+ project_custom_fields_grouped_by_section.each do |project_custom_field_section, project_custom_fields|
+ panel.with_section(
+ Overviews::ProjectCustomFields::SectionComponent.new(
+ project: @project,
+ project_custom_field_section:,
+ project_custom_fields:
+ )
+ )
+ end
+ end
+ end
+ end
+ %>
+
diff --git a/app/components/work_packages/project_attributes_tab_component.rb b/app/components/work_packages/project_attributes_tab_component.rb
new file mode 100644
index 00000000000..df6cc9c2af4
--- /dev/null
+++ b/app/components/work_packages/project_attributes_tab_component.rb
@@ -0,0 +1,49 @@
+# 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::ProjectAttributesTabComponent < ApplicationComponent
+ include OpPrimer::ComponentHelpers
+ include OpTurbo::Streamable
+
+ def initialize(work_package:)
+ super
+
+ @work_package = work_package
+ @project = work_package.project
+ end
+
+ private
+
+ def project_custom_fields_grouped_by_section
+ @project_custom_fields_grouped_by_section ||=
+ @project.available_custom_fields
+ .group_by(&:project_custom_field_section)
+ end
+end
diff --git a/app/controllers/work_packages/project_attributes_tab_controller.rb b/app/controllers/work_packages/project_attributes_tab_controller.rb
new file mode 100644
index 00000000000..f0816a1536f
--- /dev/null
+++ b/app/controllers/work_packages/project_attributes_tab_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 the OpenProject GmbH
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License version 3.
+#
+# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
+# Copyright (C) 2006-2013 Jean-Philippe Lang
+# Copyright (C) 2010-2013 the ChiliProject Team
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# See COPYRIGHT and LICENSE files for more details.
+# ++
+
+class WorkPackages::ProjectAttributesTabController < ApplicationController
+ before_action :find_work_package
+ before_action :authorize
+
+ def index
+ render(WorkPackages::ProjectAttributesTabComponent.new(work_package: @work_package))
+ end
+
+ private
+
+ def find_work_package
+ @work_package = WorkPackage.visible.find(params.expect(:id))
+ @project = @work_package.project
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+end
diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb
index 1a24afa7f6b..b556735ae36 100644
--- a/config/initializers/permissions.rb
+++ b/config/initializers/permissions.rb
@@ -314,7 +314,8 @@ Rails.application.reloader.to_prepare do
"work_packages/menus": %i[show],
"work_packages/hover_card": %i[show],
work_package_relations_tab: %i[index],
- "work_packages/reminders": %i[modal_body create update destroy]
+ "work_packages/reminders": %i[modal_body create update destroy],
+ "work_packages/project_attributes_tab": %i[index]
},
permissible_on: %i[work_package project],
contract_actions: { work_packages: %i[read] }
diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml
index 2f1eaf5d883..6b2f7206532 100644
--- a/config/locales/js-en.yml
+++ b/config/locales/js-en.yml
@@ -968,11 +968,12 @@ en:
relation_filters:
filter_work_packages_by_relation_type: "Filter work packages by relation type"
tabs:
- overview: Overview
activity: Activity
+ files: Files
+ project_attributes: Project attributes
+ overview: Overview
relations: Relations
watchers: Watchers
- files: Files
time_relative:
days: "days"
weeks: "weeks"
diff --git a/config/routes.rb b/config/routes.rb
index e12cf76bc09..278c9abb26a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -909,6 +909,7 @@ Rails.application.routes.draw do
concerns :shareable
get "hover_card" => "work_packages/hover_card#show", on: :member
+ get "project_attributes" => "work_packages/project_attributes_tab#index", on: :member
get "generate_pdf_dialog" => "work_packages#generate_pdf_dialog", on: :member
post "generate_pdf" => "work_packages#generate_pdf", on: :member
diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component.html b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component.html
new file mode 100644
index 00000000000..c5adf2f0403
--- /dev/null
+++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component.ts b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component.ts
new file mode 100644
index 00000000000..3a585ec7153
--- /dev/null
+++ b/frontend/src/app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component.ts
@@ -0,0 +1,71 @@
+// -- 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,
+ inject,
+ Input,
+ OnInit,
+} from '@angular/core';
+
+import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource';
+import { I18nService } from 'core-app/core/i18n/i18n.service';
+import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
+import { UIRouterGlobals } from '@uirouter/core';
+
+@Component({
+ selector: 'op-project-attributes-tab',
+ templateUrl: './op-project-attributes-tab.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: false,
+})
+export class WorkPackageProjectAttributesTabComponent implements OnInit {
+ readonly I18n = inject(I18nService);
+ readonly uiRouterGlobals = inject(UIRouterGlobals);
+ readonly pathHelper = inject(PathHelperService);
+
+ public turboFrameSrc:string;
+ public workPackageId:string;
+
+ @Input() public workPackage:WorkPackageResource;
+
+ ngOnInit() {
+ const { workPackageId } = this.uiRouterGlobals.params as unknown as { workPackageId:string };
+ this.workPackageId = (this.workPackage.id!) || workPackageId;
+
+ this.turboFrameSrc = this.buildTurboFrameSrc();
+ }
+
+ protected buildTurboFrameSrc():string {
+ const baseUrl = window.location.origin;
+ const url = new URL(`${this.pathHelper.staticBase}/work_packages/${this.workPackageId}/project_attributes`, baseUrl);
+
+ return url.toString();
+ }
+}
diff --git a/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts b/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts
index 2890616f578..59feeec31be 100644
--- a/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts
+++ b/frontend/src/app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-tabs.service.ts
@@ -59,6 +59,7 @@ import {
import {
workPackageFilesCount,
} from 'core-app/features/work-packages/components/wp-tabs/services/wp-tabs/wp-files-count.function';
+import { WorkPackageProjectAttributesTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component';
@Injectable({
providedIn: 'root',
@@ -146,6 +147,11 @@ export class WorkPackageTabsService {
count: workPackageNotificationsCount,
showCountAsBubble: true,
},
+ {
+ id: 'project_attributes',
+ component: WorkPackageProjectAttributesTabComponent,
+ name: I18n.t('js.work_packages.tabs.project_attributes'),
+ },
{
id: 'files',
component: WorkPackageFilesTabComponent,
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 ab4663dbc5a..291772e54bd 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
@@ -409,6 +409,7 @@ import { WorkPackageFullViewEntryComponent } from 'core-app/features/work-packag
import {
WorkPackageSplitCreateEntryComponent,
} from 'core-app/features/work-packages/routing/wp-split-create/wp-split-create-entry.component';
+import { WorkPackageProjectAttributesTabComponent } from 'core-app/features/work-packages/components/wp-single-view-tabs/project-attributes-tab/op-project-attributes-tab.component';
@NgModule({
imports: [
@@ -589,6 +590,9 @@ import {
// Files tab
WorkPackageFilesTabComponent,
+ // Project attributes tab
+ WorkPackageProjectAttributesTabComponent,
+
// Split view
WorkPackageDetailsViewButtonComponent,
WorkPackageSplitViewComponent,