From 8474cd8751f4158a362ca69e86aaa00deac7febc Mon Sep 17 00:00:00 2001 From: Behrokh Satarnejad <62008897+bsatarnejad@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:14:32 +0200 Subject: [PATCH] [64111] Use Primer PageHeader in repositories (#19001) * create a helper for breadcrumbs in repositories * move breadcrumbs to the header partial * use page header action buttons instead of toolbar items * use page header icon action buttons * add page header to revisions page * add page header to statistics page * remove breadcrumbs in annotate, changes and entry pages * change diff page * change revision page and add page header component * fix rubocup errors * fix failing tests * check breadcrumbs class in test * remove test for checking the title * fix breadcrumbs helper * add checkout instructions toggle * fix test for title check * fix Assignment Branch Condition size error * fix error while lading revision pages * remove toolbar items classes usages * use project id * move page header of revision page into a component * move page header of repository page into a component * remove toolbar classes * fix failing specs * remove toolbar usages * Re-add hook and change breadcrumb slightly * Simplify PageHeader button template * Replace custom input group with Primer component * Cleanup sass code * Correct the method for getting the "next_button" tag --------- Co-authored-by: Henriette Darge --- .../page_header_component.html.erb | 35 ++++++ .../repositories/page_header_component.rb | 89 ++++++++++++++ .../revision/page_header_component.html.erb | 57 +++++++++ .../revision/page_header_component.rb | 89 ++++++++++++++ app/helpers/repositories_helper.rb | 12 +- app/views/repositories/_breadcrumbs.html.erb | 60 ---------- .../repositories/_repository_header.html.erb | 98 ++++++++-------- app/views/repositories/annotate.html.erb | 4 - app/views/repositories/changes.html.erb | 5 - app/views/repositories/diff.html.erb | 49 +++++--- app/views/repositories/entry.html.erb | 5 - app/views/repositories/revision.html.erb | 23 +--- app/views/repositories/revisions.html.erb | 36 ++++-- app/views/repositories/show.html.erb | 109 +++++++++--------- app/views/repositories/stats.html.erb | 11 +- frontend/src/app/app.module.ts | 2 - .../copy-to-clipboard.component.ts | 74 ------------ frontend/src/app/shared/shared.module.ts | 4 - .../src/global_styles/openproject/_scm.sass | 44 ++++--- .../repositories_controller_spec.rb | 2 +- 20 files changed, 480 insertions(+), 328 deletions(-) create mode 100644 app/components/repositories/page_header_component.html.erb create mode 100644 app/components/repositories/page_header_component.rb create mode 100644 app/components/repositories/revision/page_header_component.html.erb create mode 100644 app/components/repositories/revision/page_header_component.rb delete mode 100644 app/views/repositories/_breadcrumbs.html.erb delete mode 100644 frontend/src/app/shared/components/copy-to-clipboard/copy-to-clipboard.component.ts 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 << "
  • #{text}
  • " - output << render_changes_tree(s) + output += "
  • #{text}
  • " + 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? %> -
  • -
    - <%= @instructions.checkout_command %> -
    - - <% csp_onclick("this.focus(); this.select();", "#repository-checkout-url") %> - - - -
  • -
  • - "> - <%= op_icon("button--icon icon-info1") %> - -
  • - <% end %> - <% if !empty && User.current.allowed_in_project?(:browse_repository, @project) %> -
  • - <%= link_to stats_project_repository_path(@project), - class: "button", title: t(:label_statistics) do %> - <%= op_icon("button--icon icon-chart1") %> - <% end %> -
  • - <% end %> - <%= call_hook( - :repositories_navigation_toolbar, - { repository: @repository, project: @project, repository_empty: empty } - ) %> - <% if User.current.allowed_in_project?(:manage_repository, @project) %> -
  • - <%= link_to project_settings_repository_path(@project), - class: "button", title: t(:label_setting_plural) do %> - <%= op_icon("button--icon icon-settings") %> - <% end %> -
  • - <% end %> + +<%= render(Repositories::PageHeaderComponent.new(repository: @repository, empty: empty, path: @path, rev: @rev, project: @project)) %> +<% if @instructions && @instructions.available? %> + <%= + render(Primer::OpenProject::FlexLayout.new(align_items: :flex_end, mb: 2)) do |container| + container.with_column(mr: 2, classes: "repository--checkout-container") do + render(Primer::OpenProject::InputGroup.new(input_width: :large)) do |group| + group.with_text_input( + id: "repository-checkout-url", + name: "repository-checkout-url", + label: @instructions.checkout_command, + value: @instructions.checkout_url(true), + readonly: true + ) + group.with_trailing_action_clipboard_copy_button( + value: @instructions.checkout_url(true), + aria: { label: t(:button_copy_to_clipboard) } + ) + end + end + + container.with_column do + render( + Primer::Beta::IconButton.new( + icon: :info, + tag: :a, + id: "repository--checkout-instructions-toggle", + classes: "persistent-toggle--click-handler", + aria: { label: t("repositories.checkout.show_instructions") }, + title: t("repositories.checkout.show_instructions") + ) + ) + end + end + %> <% end %> +<%= call_hook( + :repositories_navigation_toolbar, + { repository: @repository, project: @project, repository_empty: empty } + ) %> + +<% + # @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? %> + <% if @instructions %> <%= render partial: "checkout_instructions", locals: { repository: @repository, instructions: @instructions } %> diff --git a/app/views/repositories/annotate.html.erb b/app/views/repositories/annotate.html.erb index c743114bf29..91cc2c85e5a 100644 --- a/app/views/repositories/annotate.html.erb +++ b/app/views/repositories/annotate.html.erb @@ -30,10 +30,6 @@ See COPYRIGHT and LICENSE files for more details. <% html_title(t(:button_annotate)) %> <%= render partial: "repository_header", locals: { empty: false } %> -
    - <%= render partial: "breadcrumbs", - locals: { path: @path, revision: @rev }.merge(kind: "file") %> -

    <%= render partial: "link_to_functions" %>

    <% if @annotate.nil? || @annotate.empty? %> diff --git a/app/views/repositories/changes.html.erb b/app/views/repositories/changes.html.erb index 456895382bc..79c5ac7f0db 100644 --- a/app/views/repositories/changes.html.erb +++ b/app/views/repositories/changes.html.erb @@ -30,11 +30,6 @@ See COPYRIGHT and LICENSE files for more details. <%= call_hook(:view_repositories_show_contextual, { repository: @repository, project: @project }) %> <%= render partial: "repository_header", locals: { empty: false } %> -
    - <%= render partial: "breadcrumbs", - locals: { path: @path, revision: @rev }.merge(kind: (@entry ? @entry.kind : nil)) %> -
    -

    <%= render partial: "link_to_functions" %>

    <%= render_properties(@properties) %> diff --git a/app/views/repositories/diff.html.erb b/app/views/repositories/diff.html.erb index ae9b42cc671..3733ddaafa3 100644 --- a/app/views/repositories/diff.html.erb +++ b/app/views/repositories/diff.html.erb @@ -27,23 +27,42 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: "#{t(:label_revision)} #{@diff_format_revisions} #{@path}" do %> -
  • - <%= form_tag({repo_path: to_path_param(@path)}, method: :get) do %> - <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %> - <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %> - <%= styled_select_tag 'type', options_for_select([[t(:label_diff_inline), "inline"], [t(:label_diff_side_by_side), "sbs"]], @diff_type), id: "repository-diff-type-select" %> - <% end %> - <%= - content_for(:additional_js_dom_ready) do - "jQuery('#repository-diff-type-select').change(function() { +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { "#{t(:label_revision)} #{@diff_format_revisions} #{@path}" } + 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)} #{@diff_format_revisions} #{@path}" + ]) + end +%> +
    + <%= form_tag({repo_path: to_path_param(@path)}, method: :get) do %> + <%= hidden_field_tag('rev', params[:rev]) if params[:rev] %> + <%= hidden_field_tag('rev_to', params[:rev_to]) if params[:rev_to] %> + <%= styled_select_tag 'type', options_for_select([[t(:label_diff_inline), "inline"], [t(:label_diff_side_by_side), "sbs"]], @diff_type), id: "repository-diff-type-select" %> + <% end %> + <%= + content_for(:additional_js_dom_ready) do + "jQuery('#repository-diff-type-select').change(function() { if (this.value != '') { this.form.submit() } });".html_safe - end - %> -
  • -<% end %> - + end + %> + <% cache(@cache_key) do -%> <%= render partial: 'common/diff', locals: { diff: @diff, diff_type: @diff_type } %> <% end -%> diff --git a/app/views/repositories/entry.html.erb b/app/views/repositories/entry.html.erb index 2dd2bc3a724..ed1539cdc9c 100644 --- a/app/views/repositories/entry.html.erb +++ b/app/views/repositories/entry.html.erb @@ -31,11 +31,6 @@ See COPYRIGHT and LICENSE files for more details. <%= render partial: "repository_header", locals: { empty: false } %> -
    - <%= render partial: "breadcrumbs", - locals: { path: @path, revision: @rev }.merge(kind: "dir") %> -
    -

    <%= render partial: "link_to_functions" %>

    <%= render partial: "common/file", locals: { filename: @path, content: @content } %> diff --git a/app/views/repositories/revision.html.erb b/app/views/repositories/revision.html.erb index bc572562464..2c50f59912e 100644 --- a/app/views/repositories/revision.html.erb +++ b/app/views/repositories/revision.html.erb @@ -27,26 +27,9 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: "#{t(:label_revision)} #{format_revision(@changeset)}" do %> -
  • - <% if @changeset.previous.nil? %> - - <% else %> - <%= link_to_revision(@changeset.previous, @project, text: t(:label_previous), class: "button") %> - <% end %> -
  • -
  • - <% if @changeset.next.nil? %> - - <% else %> - <%= link_to_revision(@changeset.next, @project, text: t(:label_next), class: "button") %> - <% end %> -
  • -
  • - <%= form_tag({ controller: "/repositories", action: "revision", project_id: @project }, method: :get) do %> - <%= text_field_tag :rev, @rev, placeholder: t(:label_revision) %> - <% end %> -
  • +<%= render(Repositories::Revision::PageHeaderComponent.new(changeset: @changeset, repository: @repository, project: @project)) %> +<%= form_tag({ controller: "/repositories", action: "revision", project_id: @project }, method: :get) do %> + <%= text_field_tag :rev, @rev, placeholder: t(:label_revision) %> <% end %>

    <% if @changeset.scmid %>ID: <%= h(@changeset.scmid) %>
    <% end %> diff --git a/app/views/repositories/revisions.html.erb b/app/views/repositories/revisions.html.erb index e2bed4bd240..4221951615e 100644 --- a/app/views/repositories/revisions.html.erb +++ b/app/views/repositories/revisions.html.erb @@ -26,16 +26,32 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: t(:label_revision_plural) do %> - <%= form_tag({ action: "revision", id: @project }, { method: :get }) do %> -

  • - <%= label_tag :rev, t(:label_revision), class: "hidden-for-sighted" %> - <%= text_field_tag :rev, @rev, size: 8, placeholder: t(:label_revision) %> -
  • -
  • - <%= submit_tag "OK", class: "button -primary" %> -
  • - <% end %> + +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_revision_plural) } + 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 + ) + }, + t(:label_revision_plural) + ]) + end +%> + +<%= form_tag({ action: "revision", id: @project }, { method: :get }) do %> +
    + <%= label_tag :rev, t(:label_revision), class: "hidden-for-sighted" %> + <%= text_field_tag :rev, @rev, size: 8, placeholder: t(:label_revision) %> + <%= submit_tag "OK", class: "button -primary" %> +
    <% end %> <%= render partial: "revisions", locals: { project: @project, path: "", revisions: @changesets, entry: nil } %> <%= pagination_links_full @changesets %> diff --git a/app/views/repositories/show.html.erb b/app/views/repositories/show.html.erb index 4ec0e6575f9..6e0d5bce4cb 100644 --- a/app/views/repositories/show.html.erb +++ b/app/views/repositories/show.html.erb @@ -30,19 +30,15 @@ See COPYRIGHT and LICENSE files for more details. <% content_controller "repository-navigation", dynamic: true %> <%= render partial: "repository_header", locals: { empty: false } %> -
    - <%= render partial: "breadcrumbs", - locals: { path: @path, revision: @rev }.merge(kind: "dir") %> -
    - <% if !@entries.nil? && authorize_for('repositories', 'browse') %> <%= render partial: "dir_list" %> <% end %> <%= render_properties(@properties) %> -<%= toolbar title: t(:label_revision_plural), html: { class: "repository--revision-toolbar" } do %> +
    <%# rev => nil prevents overwriting the rev parameter queried for in the form with the parameter from the request %> +

    <%= t(:label_revision_plural) %>

    <%= form_tag( { action: controller.action_name, project_id: @project, @@ -55,60 +51,65 @@ See COPYRIGHT and LICENSE files for more details. "repository-navigation-target": "form" } ) do %> -
  • -
    - <%= label_tag "rev", I18n.t("repositories.go_to_revision") %> +
    +
    +
    + <%= label_tag "rev", I18n.t("repositories.go_to_revision") %> +
    + <%= text_field_tag :rev, + @rev, + id: "revision-identifier-input", + class: "revisions-item--input", + placeholder: t(:label_revision), + data: { + "repository-navigation-target": "revision", + action: "keydown.enter->repository-navigation#sendForm" + } %>
    - <%= text_field_tag :rev, - @rev, - id: "revision-identifier-input", - placeholder: t(:label_revision), + <% if !@repository.branches.nil? && @repository.branches.length > 0 %> +
    +
    + <%= label_tag "branch", I18n.t(:label_branch) %> +
    + <%= select_tag :branch, + options_for_select(@repository.branches, @rev), + include_blank: "--- #{t(:actionview_instancetag_blank_option)} ---", + id: "revision-branch-select", + class: "revisions-item--input", data: { - "repository-navigation-target": "revision", - action: "keydown.enter->repository-navigation#sendForm" + "repository-navigation-target": "branch", + action: "repository-navigation#applyValue" } %> -
  • - <% if !@repository.branches.nil? && @repository.branches.length > 0 %> -
  • -
    - <%= label_tag "branch", I18n.t(:label_branch) %>
    - <%= select_tag :branch, - options_for_select(@repository.branches, @rev), - include_blank: "--- #{t(:actionview_instancetag_blank_option)} ---", - id: "revision-branch-select", - data: { - "repository-navigation-target": "branch", - action: "repository-navigation#applyValue" - } %> -
  • - <% end %> - <% if !@repository.tags.nil? && @repository.tags.length > 0 %> -
  • -
    - <%= label_tag "tag", I18n.t(:label_tag) %> -
    - <%= select_tag :tag, - options_for_select(@repository.tags, @rev), - include_blank: "--- #{t(:actionview_instancetag_blank_option)} ---", - id: "revision-tag-select", - data: { - "repository-navigation-target": "tag", - action: "repository-navigation#applyValue" - } %> -
  • - <% end %> -
  • - <%= link_to( - { url: { action: "revision#revision-identifier-inputs", project_id: @project, - key: User.current.rss_key } }, - { class: "button", title: t("repositories.atom_revision_feed") } - ) do %> - <%= op_icon("button--icon icon-export-atom") %> <% end %> -
  • + <% if !@repository.tags.nil? && @repository.tags.length > 0 %> +
    +
    + <%= label_tag "tag", I18n.t(:label_tag) %> +
    + <%= select_tag :tag, + options_for_select(@repository.tags, @rev), + include_blank: "--- #{t(:actionview_instancetag_blank_option)} ---", + id: "revision-tag-select", + class: "revisions-item--input", + data: { + "repository-navigation-target": "tag", + action: "repository-navigation#applyValue" + } %> +
    + <% end %> +
    + <%= link_to( + { url: { action: "revision#revision-identifier-inputs", project_id: @project, + key: User.current.rss_key } }, + { class: "button button_no-margin", title: t("repositories.atom_revision_feed") } + ) do %> + <%= op_icon("button--icon icon-export-atom") %> + <% end %> +
    +
    <% end %> -<% end %> + <% 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