diff --git a/app/components/repositories/page_header_component.html.erb b/app/components/repositories/page_header_component.html.erb
new file mode 100644
index 00000000000..f76c1abd195
--- /dev/null
+++ b/app/components/repositories/page_header_component.html.erb
@@ -0,0 +1,35 @@
+<%=
+ render Primer::OpenProject::PageHeader.new do |header|
+ header.with_title { t(
+ "repositories.named_repository",
+ vendor_name: @repository.class.vendor_name
+ ) }
+ header.with_breadcrumbs(breadcrumb_items)
+ if !@empty && User.current.allowed_in_project?(:browse_repository, @project)
+ header.with_action_icon_button(
+ tag: :a,
+ icon: :graph,
+ label: t(:label_statistics),
+ mobile_icon: :graph,
+ mobile_label: t(:label_statistics),
+ size: :medium,
+ href: stats_project_repository_path(@project),
+ aria: { label: t(:label_statistics) },
+ title: t(:label_statistics)
+ )
+ end
+ if User.current.allowed_in_project?(:manage_repository, @project)
+ header.with_action_icon_button(
+ tag: :a,
+ icon: :gear,
+ label: t(:label_setting_plural),
+ mobile_icon: :gear,
+ mobile_label: t(:label_setting_plural),
+ size: :medium,
+ href: project_settings_repository_path(@project),
+ aria: { label: t(:label_setting_plural) },
+ title: t(:label_setting_plural)
+ )
+ end
+ end
+%>
diff --git a/app/components/repositories/page_header_component.rb b/app/components/repositories/page_header_component.rb
new file mode 100644
index 00000000000..1247fe85769
--- /dev/null
+++ b/app/components/repositories/page_header_component.rb
@@ -0,0 +1,89 @@
+# 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 Repositories
+ class PageHeaderComponent < ApplicationComponent
+ include ApplicationHelper
+ include OpPrimer::ComponentHelpers
+
+ def initialize(repository:, empty: false, path: nil, rev: nil, project: nil)
+ super
+ @project = project
+ @repository = repository
+ @path = path
+ @rev = rev
+ @empty = empty
+ end
+
+ def breadcrumb_items
+ [
+ project_breadcrumb,
+ repository_breadcrumb,
+ *path_breadcrumbs
+ ]
+ end
+
+ def project_breadcrumb
+ {
+ href: project_overview_path(@project.id),
+ text: @project.name
+ }
+ end
+
+ def repository_breadcrumb
+ {
+ href: url_for(action: "show", project_id: @project.id, repo_path: nil, rev: @rev),
+ text: t("repositories.named_repository", vendor_name: @repository.class.vendor_name)
+ }
+ end
+
+ def path_breadcrumbs
+ dirs = @path.to_s.split("/").compact_blank
+ link_path = ""
+ dirs.each_with_index.map do |dir, index|
+ link_path = File.join(link_path, dir)
+
+ if index == dirs.size - 1
+ dir
+ else
+ {
+ href: url_for(
+ action: "show",
+ project_id: @project.id,
+ repo_path: to_path_param(link_path),
+ rev: @rev
+ ),
+ text: dir
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/components/repositories/revision/page_header_component.html.erb b/app/components/repositories/revision/page_header_component.html.erb
new file mode 100644
index 00000000000..eaf69532fd0
--- /dev/null
+++ b/app/components/repositories/revision/page_header_component.html.erb
@@ -0,0 +1,57 @@
+<%= render Primer::OpenProject::PageHeader.new do |header| %>
+ <% header.with_title do %>
+ <%= "#{t(:label_revision)} #{helpers.format_revision(@changeset)}" %>
+ <% end %>
+
+ <% header.with_breadcrumbs([{
+ href: project_overview_path(@project.id),
+ text: @project.name
+ },
+ {
+ href: url_for({ action: "show", project_id: @project.id }),
+ text: t(
+ "repositories.named_repository",
+ vendor_name: @repository.class.vendor_name
+ )
+ },
+ {
+ href: revisions_project_repository_path(@project),
+ text: t(:label_revision_plural)
+ },
+ "#{t(:label_revision)} #{helpers.format_revision(@changeset)}"
+ ]) %>
+
+ <%# Previous Revision Button %>
+ <% header.with_action_button(
+ tag: previous_button_tag,
+ icon: "arrow-left",
+ label: t(:label_previous),
+ mobile_icon: "arrow-left",
+ mobile_label: t(:label_previous),
+ href: previous_button_url,
+ disabled: previous_button_disabled?,
+ title: previous_button_title,
+ aria: { label: t(:label_previous) }
+ ) do |button|
+ button.with_leading_visual_icon(icon: "arrow-left")
+ t(:label_previous)
+ end %>
+
+ <%# Next Revision Button %>
+ <% header.with_action_button(
+ tag: next_button_tag,
+ icon: "arrow-right",
+ label: t(:label_next),
+ mobile_icon: "arrow-right",
+ mobile_label: t(:label_next),
+ href: next_button_url,
+ disabled: next_button_disabled?,
+ title: next_button_title,
+ aria: { label: t(:label_next) }
+ ) do |button|
+ button.with_leading_visual_icon(icon: "arrow-right")
+ t(:label_next)
+ end %>
+
+ <%# Revision Search Form Button (if needed) could go here too) %>
+<% end %>
diff --git a/app/components/repositories/revision/page_header_component.rb b/app/components/repositories/revision/page_header_component.rb
new file mode 100644
index 00000000000..61545ded612
--- /dev/null
+++ b/app/components/repositories/revision/page_header_component.rb
@@ -0,0 +1,89 @@
+# 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 Repositories
+ module Revision
+ class PageHeaderComponent < ApplicationComponent
+ include ApplicationHelper
+ include OpPrimer::ComponentHelpers
+
+ def initialize(changeset:, repository:, project: nil)
+ super
+ @project = project
+ @changeset = changeset
+ @repository = repository
+ end
+
+ def previous_button_present?
+ @previous_button_present ||= @changeset.previous.present?
+ end
+
+ def next_button_present?
+ @next_button_present ||= @changeset.next.present?
+ end
+
+ def previous_button_disabled?
+ !previous_button_present?
+ end
+
+ def next_button_disabled?
+ !next_button_present?
+ end
+
+ def previous_button_url
+ return nil unless previous_button_present?
+
+ url_for(controller: "/repositories", action: "revision", project_id: @project, rev: @changeset.previous.identifier)
+ end
+
+ def next_button_url
+ return nil unless next_button_present?
+
+ url_for(controller: "/repositories", action: "revision", project_id: @project, rev: @changeset.next.identifier)
+ end
+
+ def previous_button_tag
+ previous_button_present? ? :a : :button
+ end
+
+ def next_button_tag
+ next_button_present? ? :a : :button
+ end
+
+ def previous_button_title
+ previous_button_present? ? t(:label_revision_id, value: helpers.format_revision(@changeset.previous)) : t(:label_previous)
+ end
+
+ def next_button_title
+ previous_button_present? ? t(:label_revision_id, value: helpers.format_revision(@changeset.next)) : t(:label_next)
+ end
+ end
+ end
+end
diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb
index 154e0588970..7b3d9802693 100644
--- a/app/helpers/repositories_helper.rb
+++ b/app/helpers/repositories_helper.rb
@@ -121,7 +121,7 @@ module RepositoriesHelper
style = +"change"
text = File.basename(file)
if s = tree[file][:s]
- style << " folder"
+ style += " folder"
path_param = without_leading_slash(to_path_param(@repository.relative_path(file)))
text = link_to(h(text),
show_revisions_path_project_repository_path(project_id: @project,
@@ -129,10 +129,10 @@ module RepositoriesHelper
rev: @changeset.identifier),
title: I18n.t(:label_folder))
- output << "
"
+ output += render_changes_tree(s)
elsif c = tree[file][:c]
- style << " change-#{c.action}"
+ style += " change-#{c.action}"
path_param = without_leading_slash(to_path_param(@repository.relative_path(c.path)))
unless c.action == "D"
@@ -156,10 +156,10 @@ module RepositoriesHelper
text << raw(" " + content_tag("span", h(c.from_path), class: "copied-from")) if c.from_path.present?
- output << changes_tree_li_element(c.action, text, style)
+ output += changes_tree_li_element(c.action, text, style)
end
end
- output << ""
+ output += ""
output.html_safe
end
diff --git a/app/views/repositories/_breadcrumbs.html.erb b/app/views/repositories/_breadcrumbs.html.erb
deleted file mode 100644
index f8d417b7187..00000000000
--- a/app/views/repositories/_breadcrumbs.html.erb
+++ /dev/null
@@ -1,60 +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.
-
-++#%>
-
-<%= link_to(
- { action: "show", project_id: @project, repo_path: nil, rev: @rev },
- { title: I18n.t(:label_repository_root) }
- ) do %>
- <%= op_icon("icon-home repository-breadcrumbs--home") %>
-<% end %>
-<%
-dirs = path.split('/')
-link_path = ''
-dirs.each_with_index do |dir, index|
- next if dir.blank?
- link_path << '/' unless link_path.empty?
- link_path << "#{dir}"
-%>
-
- <% if index == dirs.size - 1 %>
- <%= h(dir) %>
- <% else %>
- <%= link_to h(dir), action: "show", project_id: @project,
- repo_path: to_path_param(link_path), rev: @rev %>
- <% end %>
-<% end %>
-<%
- # @rev is revision or git branch or tag.
- rev_text = @changeset.nil? ? @rev : format_revision(@changeset)
-%>
-
- <%= "(#{t('repositories.at_identifier', identifier: rev_text)})" if rev_text.present? %>
-
-
-<% html_title(h(with_leading_slash(path))) -%>
diff --git a/app/views/repositories/_repository_header.html.erb b/app/views/repositories/_repository_header.html.erb
index 878e8f31b64..e319641d733 100644
--- a/app/views/repositories/_repository_header.html.erb
+++ b/app/views/repositories/_repository_header.html.erb
@@ -28,59 +28,55 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-<%= toolbar title: t(
- "repositories.named_repository",
- vendor_name: @repository.class.vendor_name
- ) do %>
- <% if @instructions && @instructions.available? %>
-
<% if authorize_for('repositories', 'revisions') %>
<% if @changesets && !@changesets.empty? %>
diff --git a/app/views/repositories/stats.html.erb b/app/views/repositories/stats.html.erb
index 14f098a8470..e7ac499b313 100644
--- a/app/views/repositories/stats.html.erb
+++ b/app/views/repositories/stats.html.erb
@@ -27,7 +27,16 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-<%= toolbar title: t(:label_statistics) %>
+<%=
+ render Primer::OpenProject::PageHeader.new do |header|
+ header.with_title { t(:label_statistics) }
+ header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name },
+ { href: url_for({ action: "show", project_id: @project }),
+ text: @repository ? t("repositories.named_repository", vendor_name: @repository.class.vendor_name) : t(:label_repository) },
+ t(:label_statistics)
+ ])
+ end
+%>
<%= tag(
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 41cf9ee4ab6..be145a0baf5 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -191,7 +191,6 @@ import { NoResultsComponent } from 'core-app/shared/components/no-results/no-res
import {
OpNonWorkingDaysListComponent,
} from 'core-app/shared/components/op-non-working-days-list/op-non-working-days-list.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 { PersistentToggleComponent } from 'core-app/shared/components/persistent-toggle/persistent-toggle.component';
import { TypeFormConfigurationComponent } from 'core-app/features/admin/types/type-form-configuration.component';
@@ -453,7 +452,6 @@ export class OpenProjectModule implements DoBootstrap {
registerCustomElement('opce-non-working-days-list', OpNonWorkingDaysListComponent, { injector });
registerCustomElement('opce-main-menu-toggle', MainMenuToggleComponent, { injector });
registerCustomElement('opce-main-menu-resizer', MainMenuResizerComponent, { injector });
- registerCustomElement('opce-copy-to-clipboard', CopyToClipboardComponent, { injector });
registerCustomElement('opce-global-search-title', GlobalSearchTitleComponent, { injector });
registerCustomElement('opce-persistent-toggle', PersistentToggleComponent, { injector });
registerCustomElement('opce-admin-type-form-configuration', TypeFormConfigurationComponent, { injector });
diff --git a/frontend/src/app/shared/components/copy-to-clipboard/copy-to-clipboard.component.ts b/frontend/src/app/shared/components/copy-to-clipboard/copy-to-clipboard.component.ts
deleted file mode 100644
index e9bcd516fd3..00000000000
--- a/frontend/src/app/shared/components/copy-to-clipboard/copy-to-clipboard.component.ts
+++ /dev/null
@@ -1,74 +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, ElementRef, OnInit } from '@angular/core';
-import { ToastService } from 'core-app/shared/components/toaster/toast.service';
-import { I18nService } from 'core-app/core/i18n/i18n.service';
-
-import { CopyToClipboardService } from './copy-to-clipboard.service';
-
-@Component({
- template: '',
- selector: 'opce-copy-to-clipboard',
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-
-export class CopyToClipboardComponent implements OnInit {
- public clickTarget:string;
-
- public clipboardTarget:string;
-
- private target:JQuery;
-
- constructor(
- readonly toastService:ToastService,
- readonly elementRef:ElementRef,
- readonly I18n:I18nService,
- protected copyToClipboardService:CopyToClipboardService,
- ) {
- }
-
- ngOnInit() {
- const element = this.elementRef.nativeElement;
- // Get inputs as attributes since this is a bootstrapped directive
- this.clickTarget = element.getAttribute('click-target');
- this.clipboardTarget = element.getAttribute('clipboard-target');
-
- jQuery(this.clickTarget).on('click', (evt:JQuery.TriggeredEvent) => this.onClick(evt));
-
- element.classList.add('copy-to-clipboard');
- this.target = jQuery(this.clipboardTarget ? this.clipboardTarget : element);
- }
-
- onClick($event:JQuery.TriggeredEvent) {
- $event.preventDefault();
- // Select the text in case the clipboard is not supported by the browser
- this.target.select().focus();
- this.copyToClipboardService.copy(String(this.target.val()));
- }
-}
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index a3bc9b9eda5..8c2c46f340d 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -62,7 +62,6 @@ import { HomescreenNewFeaturesBlockComponent } from 'core-app/features/homescree
import { TablePaginationComponent } from 'core-app/shared/components/table-pagination/table-pagination.component';
import { StaticQueriesService } from 'core-app/shared/components/op-view-select/op-static-queries.service';
import { CopyToClipboardService } from './components/copy-to-clipboard/copy-to-clipboard.service';
-import { CopyToClipboardComponent } from './components/copy-to-clipboard/copy-to-clipboard.component';
import { OpDateTimeComponent } from './components/date/op-date-time.component';
import { ToastComponent } from './components/toaster/toast.component';
import { ToastsContainerComponent } from './components/toaster/toasts-container.component';
@@ -197,9 +196,6 @@ export function bootstrapModule(injector:Injector):void {
OPContextMenuComponent,
IconTriggeredContextMenuComponent,
- // Add functionality to rails rendered templates
- CopyToClipboardComponent,
-
ResizerComponent,
TablePaginationComponent,
diff --git a/frontend/src/global_styles/openproject/_scm.sass b/frontend/src/global_styles/openproject/_scm.sass
index db7e6888753..208d17030a2 100644
--- a/frontend/src/global_styles/openproject/_scm.sass
+++ b/frontend/src/global_styles/openproject/_scm.sass
@@ -71,29 +71,41 @@ li.change
margin-right: 1em
.repository--revision-toolbar
+ display: flex
margin-top: 3rem
-.repository--checkout-instructions--url
- margin-top: 1rem
+.repository--checkout-container
+ width: 450px
-.repository-breadcrumbs
- font-size: 0.9rem
- margin: 15px 0
+.repository-input-group
+ display: flex
-.repository-breadcrumbs--home
- font-size: 0.75rem
+.revisions-action-container
+ display: flex
+ gap: 0.5rem
+ width: 50%
+ .button
+ height: 36px
-.repository-bradcrumbs--identifier
- display: inline-block
- margin-left: 10px
+.revisions-title-container
+ flex: 1 1
+ white-space: nowrap
+ max-width: 100%
-.repository-breadcrumbs--sep
- display: inline-block
- margin: 0 2px
+.revisions-items
+ display: flex
+ gap: 0.5rem
- &::before
- content: '▸'
- color: var(--fgColor-muted)
+ .revisions-item
+ display: flex
+ align-items: center
+
+ .revisions-item--input
+ flex: 1
+ flex-basis: 150px
+
+ .revisions-item--label
+ padding: 0 5px
table.filecontent
border: 1px solid var(--borderColor-default)
diff --git a/spec/controllers/repositories_controller_spec.rb b/spec/controllers/repositories_controller_spec.rb
index c1b7fda7f8d..2b6111d848c 100644
--- a/spec/controllers/repositories_controller_spec.rb
+++ b/spec/controllers/repositories_controller_spec.rb
@@ -244,7 +244,7 @@ RSpec.describe RepositoriesController do
shared_examples "renders the repository title" do |active_breadcrumb|
it do
expect(response).to be_successful
- expect(response.body).to have_css(".repository-breadcrumbs", text: active_breadcrumb)
+ expect(response.body).to have_css(".PageHeader-breadcrumbs", text: active_breadcrumb)
end
end