diff --git a/Gemfile b/Gemfile index f8e962882816..0e6b318ffe35 100644 --- a/Gemfile +++ b/Gemfile @@ -171,6 +171,9 @@ gem "paper_trail", "~> 15.2.0" gem "op-clamav-client", "~> 3.4", require: "clamav" +# Recurring meeting events definition +gem "ice_cube", "~> 0.17.0" + group :production do # we use dalli as standard memcache client # requires memcached 1.4+ diff --git a/Gemfile.lock b/Gemfile.lock index b58762173e66..8768f635d4e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -620,9 +620,11 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-cloud-env (2.2.1) faraday (>= 1.0, < 3.a) - googleauth (1.11.2) + google-logging-utils (0.1.0) + googleauth (1.12.0) faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -707,7 +709,7 @@ GEM launchy (3.0.1) addressable (~> 2.8) childprocess (~> 5.0) - lefthook (1.8.5) + lefthook (1.9.0) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) @@ -963,7 +965,7 @@ GEM rbtree3 (0.7.1) rdoc (6.8.1) psych (>= 4.0.0) - recaptcha (5.17.1) + recaptcha (5.18.0) redcarpet (3.6.0) redis (5.3.0) redis-client (>= 0.22.0) @@ -1270,6 +1272,7 @@ DEPENDENCIES httpx i18n-js (~> 4.2.3) i18n-tasks (~> 1.0.13) + ice_cube (~> 0.17.0) json_schemer (~> 2.3.0) json_spec (~> 1.1.4) ladle diff --git a/app/components/messages/show_page_header_component.html.erb b/app/components/messages/show_page_header_component.html.erb new file mode 100644 index 000000000000..7b7ce34f5323 --- /dev/null +++ b/app/components/messages/show_page_header_component.html.erb @@ -0,0 +1,60 @@ +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { @topic.subject } + header.with_breadcrumbs(breadcrumb_items) + + watcher_button_args = watcher_button_arguments(@topic, User.current) + header.with_action_button(**watcher_button_args) do |button| + button.with_leading_visual_icon(icon: watcher_button_args[:mobile_icon]) + watcher_button_args[:mobile_label] + end + + if !@topic.locked? && authorize_for('messages', 'reply') + header.with_action_button(tag: :a, + scheme: :default, + mobile_icon: :quote, + mobile_label: t(:button_quote), + size: :medium, + href: url_for({ action: 'quote', id: @topic }), + aria: { label: I18n.t(:button_delete) }, + data: { 'action': 'forum-messages#quote', test_selector: "message-quote-button" }, + title: t(:button_quote)) do |button| + button.with_leading_visual_icon(icon: :quote) + t(:button_quote) + end + end + + if @message.editable_by?(User.current) + header.with_action_button(tag: :a, + scheme: :default, + mobile_icon: :pencil, + mobile_label: t(:button_edit), + size: :medium, + href: edit_topic_path(@topic), + aria: { label: t(:button_edit) }, + data: { test_selector: "message-edit-button" }, + title: t(:button_edit)) do |button| + button.with_leading_visual_icon(icon: :pencil) + t(:button_edit) + end + end + + if @message.destroyable_by?(User.current) + header.with_action_button(tag: :a, + scheme: :danger, + mobile_icon: :trash, + mobile_label: t(:button_delete), + size: :medium, + href: topic_path(@topic), + aria: { label: I18n.t(:button_delete) }, + data: { + confirm: I18n.t(:text_are_you_sure), + method: :delete + }, + title: I18n.t(:button_delete)) do |button| + button.with_leading_visual_icon(icon: :trash) + t(:button_delete) + end + end + end +%> diff --git a/app/components/messages/show_page_header_component.rb b/app/components/messages/show_page_header_component.rb new file mode 100644 index 000000000000..23a606034599 --- /dev/null +++ b/app/components/messages/show_page_header_component.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module Messages + class ShowPageHeaderComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + include ApplicationHelper + include WatchersHelper + + def initialize(topic:, message:, forum:, project:) + super + @topic = topic + @message = message + @forum = forum + @project = project + end + + def breadcrumb_items + [ + { href: project_overview_path(@project.id), text: @project.name }, + { href: project_forums_path(@project), text: t(:label_forum_plural) }, + { href: project_forum_path(@project, @forum), text: @forum.name }, + @topic.subject + ] + end + end +end diff --git a/app/components/op_primer/border_box_table_component.html.erb b/app/components/op_primer/border_box_table_component.html.erb index 4c96232aa95f..a026da0f9aa1 100644 --- a/app/components/op_primer/border_box_table_component.html.erb +++ b/app/components/op_primer/border_box_table_component.html.erb @@ -58,6 +58,12 @@ See COPYRIGHT and LICENSE files for more details. end end end + + if has_footer? + component.with_footer(classes: grid_class, color: :muted) do + footer + end + end end %> diff --git a/app/components/op_primer/border_box_table_component.rb b/app/components/op_primer/border_box_table_component.rb index b005fe4be6d0..1e1160790966 100644 --- a/app/components/op_primer/border_box_table_component.rb +++ b/app/components/op_primer/border_box_table_component.rb @@ -43,7 +43,7 @@ class << self # # This results in the description columns to be hidden on mobile def mobile_columns(*names) - return @mobile_columns || columns if names.empty? + return Array(@mobile_columns || columns) if names.empty? @mobile_columns = names.map(&:to_sym) end @@ -54,7 +54,7 @@ def mobile_columns(*names) # # This results in the description columns to be hidden on mobile def mobile_labels(*names) - return @mobile_labels if names.empty? + return Array(@mobile_labels) if names.empty? @mobile_labels = names.map(&:to_sym) end @@ -106,6 +106,10 @@ def has_actions? false end + def has_footer? + false + end + def sortable? false end @@ -133,5 +137,9 @@ def blank_description def blank_icon nil end + + def footer + raise ArgumentError, "Need to provide footer content" + end end end diff --git a/app/components/projects/settings/index_page_header_component.html.erb b/app/components/projects/settings/index_page_header_component.html.erb new file mode 100644 index 000000000000..06c1f59002e5 --- /dev/null +++ b/app/components/projects/settings/index_page_header_component.html.erb @@ -0,0 +1,100 @@ +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_information_plural) } + header.with_breadcrumbs( [ + { href: project_overview_path(@project.id), text: @project.name }, + { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + t(:label_information_plural) + ]) + + if User.current.allowed_in_project?(:add_subprojects, @project) + header.with_action_button(scheme: :primary, + mobile_icon: :plus, + mobile_label: t(:label_subproject_new), + aria: { label: t(:label_subproject_new) }, + title: t(:label_subproject_new), + tag: :a, + href: new_project_path(parent_id: @project.id)) do |button| + button.with_leading_visual_icon(icon: :plus) + t(:label_subproject) + end + end + + header.with_action_button(tag: :a, + mobile_icon: :pencil, + mobile_label: t('projects.settings.change_identifier'), + size: :medium, + href: project_identifier_path(@project), + aria: { label: t('projects.settings.change_identifier') }, + title: t('projects.settings.change_identifier')) do |button| + button.with_leading_visual_icon(icon: :pencil) + t('projects.settings.change_identifier') + end + + header.with_action_menu( + menu_arguments: { + anchor_align: :end + }, + button_arguments: { + icon: "op-kebab-vertical", + "aria-label": t(:label_more), + test_selector: "project-settings-more-menu" + } + ) do |menu| + if @project.copy_allowed? + menu.with_item( + label:t(:button_copy), + href: copy_project_path(@project), + content_arguments: { + data: { turbo: false }, + test_selector: "project-settings--copy" + }, + accesskey: helpers.accesskey(:copy), + ) do |item| + item.with_leading_visual_icon(icon: :copy) + end + end + + if User.current.allowed_in_project?(:archive_project, @project) + menu.with_item( + tag: :a, + label: t(:button_archive), + href: project_archive_path(@project, status: '', name: @project.name), + content_arguments: { + data: { confirm: t('project.archive.are_you_sure', name: @project.name), method: :post, }, + test_selector: "project-settings--archive" + } + ) do |item| + item.with_leading_visual_icon(icon: 'lock') + end + end + if User.current.admin? + label = @project.templated ? 'remove_from_templates' : 'make_template' + menu.with_item( + tag: :a, + label: t("project.template.#{label}"), + href: project_templated_path(@project), + content_arguments: { + data: { method: @project.templated ? :delete : :post }, + test_selector: "project-settings--mark-template" + } + ) do |item| + item.with_leading_visual_icon(icon: @project.templated ? :"file-removed" : :"file-added") + end + + menu.with_item( + tag: :a, + scheme: :danger, + label: t(:button_delete), + href: confirm_destroy_project_path(@project), + content_arguments: { + data: { turbo: false }, + test_selector: "project-settings--delete" + } + ) do |item| + item.with_leading_visual_icon(icon: :trash) + end + end + end + end +%> diff --git a/app/components/projects/settings/index_page_header_component.rb b/app/components/projects/settings/index_page_header_component.rb new file mode 100644 index 000000000000..a57128ce4f05 --- /dev/null +++ b/app/components/projects/settings/index_page_header_component.rb @@ -0,0 +1,39 @@ +# 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 Projects::Settings::IndexPageHeaderComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + def initialize(project:) + super + + @project = project + end +end diff --git a/app/components/users/hover_card_component.sass b/app/components/users/hover_card_component.sass index ca6fa63034aa..5f8fc8e212b6 100644 --- a/app/components/users/hover_card_component.sass +++ b/app/components/users/hover_card_component.sass @@ -6,7 +6,7 @@ // modal overlay will darken the background while the hover card is active, since its semi-transparent bg shading is // added on top of the other dialog background shaders. We don't want an additional spot modal background here, // so we disable it for this edge case. -.controller-projects.action-index +.controller-projects.action-index, .controller-meetings.action-show .spot-modal-overlay:not(:has(.op-user-hover-card)) background: transparent diff --git a/app/components/users/show_page_header_component.html.erb b/app/components/users/show_page_header_component.html.erb index 9a8ac99ceed6..73a8d63c799f 100644 --- a/app/components/users/show_page_header_component.html.erb +++ b/app/components/users/show_page_header_component.html.erb @@ -28,7 +28,9 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title { "#{avatar @user} #{h(@user.name)}".html_safe } + header.with_title do + "#{avatar(@user, hover_card: { active: false })} #{h(@user.name)}".html_safe + end header.with_breadcrumbs(breadcrumb_items) if @current_user.allowed_globally?(:manage_user) diff --git a/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb b/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb index d068472fdcff..1aedf05ea037 100644 --- a/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb +++ b/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb @@ -3,7 +3,7 @@ size: :large, id: DIALOG_ID)) do |d| d.with_header(variant: :large) - d.with_body(classes: "Overlay-body_autocomplete_height") do + d.with_body(classes: body_classes) do render(WorkPackageRelationsTab::AddWorkPackageChildFormComponent.new( work_package: @work_package )) diff --git a/app/components/work_package_relations_tab/add_work_package_child_dialog_component.rb b/app/components/work_package_relations_tab/add_work_package_child_dialog_component.rb index 8f3ae85e92d2..fc7e09dee8df 100644 --- a/app/components/work_package_relations_tab/add_work_package_child_dialog_component.rb +++ b/app/components/work_package_relations_tab/add_work_package_child_dialog_component.rb @@ -51,4 +51,8 @@ def dialog_title child_label = t("#{I18N_NAMESPACE}.relations.label_child_singular") t("#{I18N_NAMESPACE}.label_add_x", x: child_label) end + + def body_classes + "Overlay-body_autocomplete_height" + end end diff --git a/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb b/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb index d9cf73a50d36..2a804ff50d0b 100644 --- a/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb +++ b/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb @@ -3,7 +3,7 @@ size: :large, id: DIALOG_ID)) do |d| d.with_header(variant: :large) - d.with_body(classes: "Overlay-body_autocomplete_height") do + d.with_body(classes: body_classes) do render(WorkPackageRelationsTab::WorkPackageRelationFormComponent.new( work_package: @work_package, relation: @relation diff --git a/app/components/work_package_relations_tab/work_package_relation_dialog_component.rb b/app/components/work_package_relations_tab/work_package_relation_dialog_component.rb index c56edcd2ba98..02fd5f33dd76 100644 --- a/app/components/work_package_relations_tab/work_package_relation_dialog_component.rb +++ b/app/components/work_package_relations_tab/work_package_relation_dialog_component.rb @@ -58,4 +58,8 @@ def dialog_title t("#{I18N_NAMESPACE}.label_add_x", x: relation_label) end end + + def body_classes + @relation.persisted? ? nil : "Overlay-body_autocomplete_height" + end end diff --git a/app/contracts/custom_fields/hierarchy/update_item_contract.rb b/app/contracts/custom_fields/hierarchy/update_item_contract.rb index 01c12c873401..614cd121ab7e 100644 --- a/app/contracts/custom_fields/hierarchy/update_item_contract.rb +++ b/app/contracts/custom_fields/hierarchy/update_item_contract.rb @@ -52,6 +52,7 @@ class UpdateItemContract < Dry::Validation::Contract rule(:short) do next if schema_error?(:item) + next unless key? key.failure(:not_unique) if values[:item].siblings.exists?(short: value) end diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index 434b7686ffd9..414248f8161a 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -35,7 +35,6 @@ class ForumsController < ApplicationController accept_key_auth :show include SortHelper - include WatchersHelper include PaginationHelper def index diff --git a/app/controllers/sys_controller.rb b/app/controllers/sys_controller.rb index b12087027fee..95e9132540a5 100644 --- a/app/controllers/sys_controller.rb +++ b/app/controllers/sys_controller.rb @@ -43,6 +43,24 @@ def repo_auth end end + def fetch_changesets + projects = [] + if params[:id] + projects << Project.active.has_module(:repository).find_by!(identifier: params[:id]) + else + projects = Project.active.has_module(:repository) + .includes(:repository).references(:repositories) + end + projects.each do |project| + if project.repository + project.repository.fetch_changesets + end + end + head :ok + rescue ActiveRecord::RecordNotFound + head :not_found + end + private def authorized?(project, user) diff --git a/app/controllers/types_controller.rb b/app/controllers/types_controller.rb index 8534e3843904..c46d8cb09eee 100644 --- a/app/controllers/types_controller.rb +++ b/app/controllers/types_controller.rb @@ -88,7 +88,7 @@ def update call.on_failure do |result| flash[:error] = result.errors.full_messages.join("\n") - render_edit_tab(@type) + render_edit_tab(@type, status: :unprocessable_entity) end end end @@ -139,12 +139,12 @@ def redirect_to_type_tab_path(type, notice) notice:) end - def render_edit_tab(type) + def render_edit_tab(type, status: :ok) @tab = params[:tab] @projects = Project.all @type = type - render action: :edit, status: :unprocessable_entity + render action: :edit, status: end def show_local_breadcrumb diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb index 85217057380e..821798a5cec2 100644 --- a/app/helpers/journals_helper.rb +++ b/app/helpers/journals_helper.rb @@ -38,7 +38,8 @@ def back_to_activity_page_url(activity_page) in ["users", user_id] user_url(user_id) in ["work_packages", work_package_id] - work_package_url(work_package_id) + # Sometimes the parameter provided is erroneous (having an extra ') for unknown reasons. + work_package_url(work_package_id.chomp("'")) else nil end diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb index 22e7fb2743be..27f883a66b93 100644 --- a/app/helpers/watchers_helper.rb +++ b/app/helpers/watchers_helper.rb @@ -30,25 +30,75 @@ module WatchersHelper # Create a link to watch/unwatch object # # * :replace - a string or array of strings with css selectors that will be updated, whenever the watcher status is changed - def watcher_link(object, user, options = { replace: ".watcher_link", class: "watcher_link" }) - options = options.with_indifferent_access - raise ArgumentError, "Missing :replace option in options hash" if options["replace"].blank? + def watcher_link(object, user, options = {}) + options = { replace: ".watcher_link", class: "watcher_link" }.merge(options) - return "" unless user&.logged? && object.respond_to?(:watched_by?) + return "" unless valid_watcher_conditions?(object, user, options) watched = object.watched_by?(user) - html_options = options - path = send(:"#{(watched ? 'unwatch' : 'watch')}_path", object_type: object.class.to_s.underscore.pluralize, - object_id: object.id, - replace: options.delete("replace")) - html_options[:class] = html_options[:class].to_s + " button" + path = watcher_path(object, watched, options) + + html_options = prepare_html_options(watched, options) + + link_to_watcher_button(watched, path, html_options) + end + + def watcher_button_arguments(object, user) + return nil unless user&.logged? && object.respond_to?(:watched_by?) + + watched = object.watched_by?(user) + + path = send(:"#{(watched ? 'unwatch' : 'watch')}_path", + object_type: object.class.to_s.underscore.pluralize, + object_id: object.id) method = watched ? :delete : :post label = watched ? I18n.t(:button_unwatch) : I18n.t(:button_watch) - link_to(content_tag(:i, "", class: watched ? "button--icon icon-watched" : " button--icon icon-unwatched") + " " + - content_tag(:span, label, class: "button--text"), path, html_options.merge(method:)) + { + tag: :a, + href: path, + scheme: :default, + aria: { label: label }, + data: { + method: + }, + mobile_icon: watched ? "eye-closed" : "eye", + mobile_label: label + } + end + + private + + def valid_watcher_conditions?(object, user, options) + raise ArgumentError, "Missing :replace option in options hash" if options[:replace].blank? + + user&.logged? && object.respond_to?(:watched_by?) + end + + def watcher_path(object, watched, options) + action = watched ? "unwatch" : "watch" + send(:"#{action}_path", object_type: object.class.to_s.underscore.pluralize, object_id: object.id, replace: options[:replace]) + end + + def prepare_html_options(watched, options) + options.merge( + class: "#{options[:class]} button", + method: watched ? :delete : :post + ) + end + + def link_to_watcher_button(watched, path, html_options) + label = watched ? I18n.t(:button_unwatch) : I18n.t(:button_watch) + icon_class = watched ? "icon-watched" : "icon-unwatched" + + link_to( + content_tag(:i, "", class: "button--icon #{icon_class}") + + content_tag(:span, label, class: "button--text"), + path, + html_options + ) end end diff --git a/app/menus/submenu.rb b/app/menus/submenu.rb index 8909c15f32fa..6bd27e89a4a5 100644 --- a/app/menus/submenu.rb +++ b/app/menus/submenu.rb @@ -29,7 +29,7 @@ class Submenu include Rails.application.routes.url_helpers attr_reader :view_type, :project, :params - def initialize(view_type:, project: nil, params: nil) + def initialize(view_type:, params:, project: nil) @view_type = view_type @project = project @params = params @@ -108,12 +108,13 @@ def query_params(id) { query_id: id } end - def menu_item(title:, icon_key: nil, count: nil, show_enterprise_icon: false, query_params: {}) + def menu_item(title:, icon_key: nil, count: nil, show_enterprise_icon: false, + query_params: {}, selected: selected?(query_params)) OpenProject::Menu::MenuItem.new(title:, href: query_path(query_params), icon: icon_map.fetch(icon_key, icon_key), count:, - selected: selected?(query_params), + selected:, favored: favored?(query_params), show_enterprise_icon:) end @@ -144,4 +145,8 @@ def icon_map def query_path(query_params) raise NotImplementedError end + + def url_helpers + @url_helpers ||= OpenProject::StaticRouting::StaticRouter.new.url_helpers + end end diff --git a/app/models/custom_field/hierarchy/hierarchy_item_adapter.rb b/app/models/custom_field/hierarchy/hierarchy_item_adapter.rb new file mode 100644 index 000000000000..14cd4053bc73 --- /dev/null +++ b/app/models/custom_field/hierarchy/hierarchy_item_adapter.rb @@ -0,0 +1,41 @@ +# 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 CustomField::Hierarchy::HierarchyItemAdapter + delegate :id, :label, :short, :to_s, to: :item + + def initialize(item:) = @item = item + + def name = @item.label + + private + + attr_accessor :item +end diff --git a/app/models/custom_field/hierarchy/item.rb b/app/models/custom_field/hierarchy/item.rb index dcb12cc27597..7bfd41af2159 100644 --- a/app/models/custom_field/hierarchy/item.rb +++ b/app/models/custom_field/hierarchy/item.rb @@ -35,4 +35,6 @@ class CustomField::Hierarchy::Item < ApplicationRecord has_closure_tree order: "sort_order", numeric_order: true, dont_order_roots: true, dependent: :destroy scope :including_children, -> { includes(children: :children) } + + def to_s = short.nil? ? label : "#{label} (#{short})" end diff --git a/app/models/custom_field/order_statements.rb b/app/models/custom_field/order_statements.rb index 0e4da57f8f52..3f3153db0613 100644 --- a/app/models/custom_field/order_statements.rb +++ b/app/models/custom_field/order_statements.rb @@ -92,7 +92,7 @@ def group_by_join_statement private - def can_be_used_for_grouping? = field_format.in?(%w[list date bool int float string link]) + def can_be_used_for_grouping? = field_format.in?(%w[list date bool int float string link hierarchy]) # Template for all the join statements. # diff --git a/app/models/queries/filters/shared/custom_fields/hierarchy.rb b/app/models/queries/filters/shared/custom_fields/hierarchy.rb index 81538bc4bdb4..c29e545b9bed 100644 --- a/app/models/queries/filters/shared/custom_fields/hierarchy.rb +++ b/app/models/queries/filters/shared/custom_fields/hierarchy.rb @@ -40,24 +40,9 @@ def ar_object_filter? def value_objects CustomField::Hierarchy::Item .where(id: @values) - .map { |item| HierarchyItemFilterAdapter.new(item:) } + .map { |item| CustomField::Hierarchy::HierarchyItemAdapter.new(item:) } end end - - class HierarchyItemFilterAdapter - attr_reader :name - - delegate :id, to: :item - - def initialize(item:) - @item = item - @name = item.label - end - - private - - attr_accessor :item - end end end end diff --git a/app/models/queries/filters/strategies/integer_list_optional.rb b/app/models/queries/filters/strategies/integer_list_optional.rb index df55d434c6c3..f7a0ff61c3d1 100644 --- a/app/models/queries/filters/strategies/integer_list_optional.rb +++ b/app/models/queries/filters/strategies/integer_list_optional.rb @@ -33,6 +33,7 @@ class IntegerListOptional < ::Queries::Filters::Strategies::Integer def operator_map super_value = super.dup super_value["="] = ::Queries::Operators::EqualsOr + super_value["*"] = ::Queries::Operators::All super_value end diff --git a/app/models/query/results/group_by.rb b/app/models/query/results/group_by.rb index c3097fdb03e7..efe8d4f18112 100644 --- a/app/models/query/results/group_by.rb +++ b/app/models/query/results/group_by.rb @@ -89,21 +89,55 @@ def transform_custom_field_keys(groups) if custom_field.list? transform_list_custom_field_keys(custom_field, groups) + elsif custom_field.field_format_hierarchy? + transform_hierarchy_custom_field_keys(custom_field, groups) else transform_single_custom_field_keys(custom_field, groups) end end + def transform_hierarchy_custom_field_keys(custom_field, groups) + items = hierarchy_items_for_keys(custom_field, groups) + + groups.transform_keys do |key| + if custom_field.multi_value? + Array(key&.split(".")).map { |subkey| items[subkey] } + else + items[key] || nil + end + end + end + + # rubocop:disable Metrics/AbcSize + def hierarchy_items_for_keys(custom_field, groups) + keys = groups.keys.map { |k| k ? k.split(".") : [] }.flatten.uniq + + CustomFields::Hierarchy::HierarchicalItemService + .new + .get_descendants(item: custom_field.hierarchy_root, include_self: false) + .fmap do |list| + list.filter_map { |item| CustomField::Hierarchy::HierarchyItemAdapter.new(item:) if keys.include?(item.label) } + .index_by(&:label) + end + .either( + ->(list) { list }, + ->(error) do + msg = "#{I18n.t('api_v3.errors.code_500')} #{error}" + raise ::API::Errors::InternalError.new(msg) + end + ) + end + + # rubocop:enable Metrics/AbcSize + def transform_list_custom_field_keys(custom_field, groups) options = custom_options_for_keys(custom_field, groups) groups.transform_keys do |key| if custom_field.multi_value? - (key ? key.split(".") : []).map do |subkey| - options[subkey].first - end + Array(key&.split(".")).map.map { |subkey| options[subkey].first } else - options[key] ? options[key].first : nil + options[key]&.first end end end @@ -111,7 +145,7 @@ def transform_list_custom_field_keys(custom_field, groups) def custom_options_for_keys(custom_field, groups) keys = groups.keys.map { |k| k ? k.split(".") : [] } # Because of multi select cfs we might end up having overlapping groups - # (e.g group "1" and group "1.3" and group "3" which represent concatenated ids). + # (e.g. group "1" and group "1.3" and group "3" which represent concatenated ids). # This can result in us having ids in the keys array multiple times (e.g. ["1", "1", "3", "3"]). # If we were to use the keys array with duplicates to find the actual custom options, # AR would throw an error as the number of records returned does not match the number diff --git a/app/models/setting/aliases.rb b/app/models/setting/aliases.rb index 98aa5bbbf4fd..154aabab71db 100644 --- a/app/models/setting/aliases.rb +++ b/app/models/setting/aliases.rb @@ -51,5 +51,13 @@ def host_without_protocol def optional_port_from_host_name Setting.host_name&.split(":")&.[](1) end + + ## + # Get the names of working days + # @return [Array] the names of the working days + def working_day_names + weekdays = %i[monday tuesday wednesday thursday friday saturday sunday] + Setting.working_days.map { |day| weekdays[day - 1] } + end end end diff --git a/app/services/sessions/clear_old_sessions_service.rb b/app/services/sessions/clear_old_sessions_service.rb new file mode 100644 index 000000000000..95b7a8846c5d --- /dev/null +++ b/app/services/sessions/clear_old_sessions_service.rb @@ -0,0 +1,39 @@ +#-- 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 Sessions + class ClearOldSessionsService + class << self + ## + # Drop all sessions for the given user + def call!(days_ago: 30) + # sessions expire after 30 days of inactivity by default + ActiveRecord::SessionStore::Session.where("updated_at < ?", days_ago.days.ago).delete_all + end + end + end +end diff --git a/app/views/account/lost_password.html.erb b/app/views/account/lost_password.html.erb index 5f2859f265c4..1a5fced1d7c6 100644 --- a/app/views/account/lost_password.html.erb +++ b/app/views/account/lost_password.html.erb @@ -27,7 +27,15 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_password_lost) %> -<%= toolbar title: t(:label_password_lost) %> + +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_password_lost) } + header.with_breadcrumbs([{ href: home_path, text: organization_name }, + t(:label_password_lost)]) + end +%> +
<%= styled_form_tag({action: "lost_password"}) do %>
diff --git a/app/views/account/password_recovery.html.erb b/app/views/account/password_recovery.html.erb index 47b1f76db29f..3e33e162f621 100644 --- a/app/views/account/password_recovery.html.erb +++ b/app/views/account/password_recovery.html.erb @@ -27,7 +27,15 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_password_lost) %> -<%= toolbar title: t(:label_password_lost) %> + +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_password_lost) } + header.with_breadcrumbs([{ href: home_path, text: organization_name }, + t(:label_password_lost)]) + end +%> + <%= error_messages_for 'user' %> <%= styled_form_tag({token: @token.value}, autocomplete: 'off') do %>
diff --git a/app/views/admin/settings/project_custom_fields/edit.html.erb b/app/views/admin/settings/project_custom_fields/edit.html.erb index 9651e2fb7db1..4b28742f4731 100644 --- a/app/views/admin/settings/project_custom_fields/edit.html.erb +++ b/app/views/admin/settings/project_custom_fields/edit.html.erb @@ -28,7 +28,6 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_administration), t("settings.project_attributes.heading"), @custom_field.name %> -<% local_assigns[:additional_breadcrumb] = @custom_field.name %> <%= render(Settings::ProjectCustomFields::EditFormHeaderComponent.new( diff --git a/app/views/admin/settings/project_custom_fields/new.html.erb b/app/views/admin/settings/project_custom_fields/new.html.erb index f95642bc2048..0384cb23b787 100644 --- a/app/views/admin/settings/project_custom_fields/new.html.erb +++ b/app/views/admin/settings/project_custom_fields/new.html.erb @@ -28,7 +28,6 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_administration), t("settings.project_attributes.heading"), t('settings.project_attributes.new.heading') %> -<% local_assigns[:additional_breadcrumb] = t('settings.project_attributes.new.heading') %> <%= render(Settings::ProjectCustomFields::NewFormHeaderComponent.new) %> diff --git a/app/views/categories/destroy.html.erb b/app/views/categories/destroy.html.erb index ea75334d6a64..430fd0185378 100644 --- a/app/views/categories/destroy.html.erb +++ b/app/views/categories/destroy.html.erb @@ -26,7 +26,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: "#{Category.model_name.human} #{@category.name}" %> + +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { @category.name } + header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + { href: project_settings_categories_path(@project.id), text: t(:label_work_package_category_plural) }, + @category.name]) + end %> + <%= form_tag({}, {method: :delete, class: 'form'}) do %>

<%= t(:text_work_package_category_destroy_question, count: @issue_count) %>

diff --git a/app/views/categories/edit.html.erb b/app/views/categories/edit.html.erb index cd0a4711ef27..ea7ef00d6d33 100644 --- a/app/views/categories/edit.html.erb +++ b/app/views/categories/edit.html.erb @@ -26,7 +26,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: Category.model_name.human %> + +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { @category.name } + header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + { href: project_settings_categories_path(@project.id), text: t(:label_work_package_category_plural) }, + @category.name]) + end +%> + <%= labelled_tabular_form_for @category, as: :category do |f| %> <%= render partial: 'categories/form', locals: { f: f } %> <%= f.button t(:button_save), class: 'button -primary -with-icon icon-checkmark' %> diff --git a/app/views/categories/new.html.erb b/app/views/categories/new.html.erb index f93042e60ecc..bcb04b11371c 100644 --- a/app/views/categories/new.html.erb +++ b/app/views/categories/new.html.erb @@ -26,7 +26,17 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: t(:label_work_package_category_new) %> + +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_work_package_category_new) } + header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") }, + { href: project_settings_categories_path(@project.id), text: t(:label_work_package_category_plural) }, + t(:label_work_package_category_new) + ]) + end %> + <%= labelled_tabular_form_for [@project, @category], as: :category do |f| %> <%= render partial: 'categories/form', locals: { f: f } %> <%= f.button t(:button_create), class: 'button -primary -with-icon icon-checkmark' %> diff --git a/app/views/colors/confirm_destroy.html.erb b/app/views/colors/confirm_destroy.html.erb index 878cb53b1785..a8bd724e9195 100644 --- a/app/views/colors/confirm_destroy.html.erb +++ b/app/views/colors/confirm_destroy.html.erb @@ -26,7 +26,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: @color.name %> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { @color.name } + header.with_breadcrumbs([{ href: admin_index_path, text: t(:label_administration) }, + { href: colors_path, text: t(:label_color_plural) }, + @color.name]) + end +%> + <%= labelled_tabular_form_for @color, url: color_url(@color), html: {method: 'delete'}, diff --git a/app/views/forums/edit.html.erb b/app/views/forums/edit.html.erb index 55eecad24ebf..db71092e464e 100644 --- a/app/views/forums/edit.html.erb +++ b/app/views/forums/edit.html.erb @@ -26,7 +26,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: Forum.model_name.human %> +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { @forum.name } + header.with_breadcrumbs( [{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_forums_path(@project.id), text: t(:label_forum_plural) }, + @forum.name + ]) end %> <%= labelled_tabular_form_for [@project, @forum] do |f| %> <%= render partial: 'form', locals: { f: f } %> diff --git a/app/views/forums/index.html.erb b/app/views/forums/index.html.erb index 73a2c166a9a7..b228b5921e34 100644 --- a/app/views/forums/index.html.erb +++ b/app/views/forums/index.html.erb @@ -27,20 +27,28 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_forum_plural) %> -<%= toolbar title: t(:label_forum_plural) do %> - <% if User.current.allowed_in_project?(:manage_forums, @project) %> -
  • - <%= link_to(new_project_forum_path(@project), - { aria: { label: t(:label_forum_new) }, - title: t(:label_forum_new), - class: 'button -primary' }) do %> - <%= op_icon('button--icon icon-add') %> - <%= t('activerecord.models.forum') %> - <% end %> -
  • - <% end %> -<% end %> +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_forum_plural) } + header.with_breadcrumbs( [{ href: project_overview_path(@project.id), text: @project.name }, + t(:label_forum_plural) + ]) end %> + +<% if User.current.allowed_in_project?(:manage_forums, @project) %> + <%= + render(Primer::OpenProject::SubHeader.new) do |subheader| + subheader.with_action_button(scheme: :primary, + aria: { label: t(:label_forum_new) }, + title: t(:label_forum_new), + tag: :a, + href: new_project_forum_path(@project)) do |button| + button.with_leading_visual_icon(icon: :plus) + t('activerecord.models.forum') + end + end + %> +<% end %> <% if @forums.empty? %> <%= no_results_box(action_url: new_project_forum_path(@project)) %> <% else %> diff --git a/app/views/forums/new.html.erb b/app/views/forums/new.html.erb index 47c22c0fd526..97f41c0d1be0 100644 --- a/app/views/forums/new.html.erb +++ b/app/views/forums/new.html.erb @@ -27,8 +27,14 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: t(:label_forum_new) %> - +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_forum_new) } + header.with_breadcrumbs( [{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_forums_path(@project.id), text: t(:label_forum_plural) }, + t(:label_forum_new) + ]) + end %> <%= labelled_tabular_form_for [@project, @forum] do |f| %> <%= render partial: 'form', locals: {f: f} %> <%= f.button t(:button_create), class: 'button -primary -with-icon icon-checkmark' %> diff --git a/app/views/forums/show.html.erb b/app/views/forums/show.html.erb index 576da3808a90..0d7e4a48b977 100644 --- a/app/views/forums/show.html.erb +++ b/app/views/forums/show.html.erb @@ -49,24 +49,39 @@ See COPYRIGHT and LICENSE files for more details. <% end %>
    -<%= toolbar title: @forum.name, subtitle: format_text(@forum.description) do %> - <% if authorize_for(:messages, :new) %> -
  • - <%= link_to({ controller: '/messages', action: 'new', forum_id: @forum }, - { class: 'add-message-button button -primary', - aria: { label: t(:label_message_new) }, - title: t(:label_message_new) }) do %> - <%= op_icon('button--icon icon-add') %> - <%= t(:label_message) %> - <% end %> - <% csp_onclick('jQuery("#add-message").show(); jQuery("#message_subject").focus();', '.add-message-button') %> -
  • - <% end %> - <% unless User.current.anonymous? %> -
  • - <%= watcher_link(@forum, User.current) %> -
  • - <% end %> + +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { @forum.name } + header.with_description { @forum.description } + header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_forums_path(@project), text: t(:label_forum_plural) }, + @forum.name]) + unless User.current.anonymous? + watcher_button_args = watcher_button_arguments(@forum, User.current) + header.with_action_button(**watcher_button_args) do |button| + button.with_leading_visual_icon(icon: watcher_button_args[:mobile_icon]) + watcher_button_args[:mobile_label] + end + end + end +%> +<% if authorize_for(:messages, :new) %> + <%= + render(Primer::OpenProject::SubHeader.new) do |subheader| + if authorize_for(:messages, :new) + subheader.with_action_button(scheme: :primary, + aria: { label: t(:label_message_new) }, + title: t(:label_message_new), + tag: :a, + class: 'add-message-button', + href: url_for({controller: '/messages', action: 'new', forum_id: @forum})) do |button| + button.with_leading_visual_icon(icon: :plus) + t(:label_message) + end + end + end + %> <% end %> <% if @topics.any? %> diff --git a/app/views/groups/_memberships.html.erb b/app/views/groups/_memberships.html.erb index a7d554fff612..8426d22ee27b 100644 --- a/app/views/groups/_memberships.html.erb +++ b/app/views/groups/_memberships.html.erb @@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> -<% roles = Role.givable %> +<% roles = ProjectRole.givable %> <% projects = Project.active.order(Arel.sql('lft')) %> <% memberships = @group.memberships %> diff --git a/app/views/messages/edit.html.erb b/app/views/messages/edit.html.erb index 503d85c27e51..f906ee6ed300 100644 --- a/app/views/messages/edit.html.erb +++ b/app/views/messages/edit.html.erb @@ -27,7 +27,14 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -

    <%= link_to h(@forum.name), controller: '/forums', action: 'show', project_id: @project, id: @forum %> » <%= h @message.subject %>

    +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { @message.subject } + header.with_breadcrumbs( [{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_forums_path(@project), text: t(:label_forum_plural) }, + { href: project_forum_path(@project, @forum), text: @forum.name }, + @message.subject]) + end %> <%= labelled_tabular_form_for @message, url: topic_path(@message), diff --git a/app/views/messages/new.html.erb b/app/views/messages/new.html.erb index 58fed5b40082..3b6e0879f527 100644 --- a/app/views/messages/new.html.erb +++ b/app/views/messages/new.html.erb @@ -27,8 +27,15 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -

    <%= link_to h(@forum.name), controller: '/forums', action: 'show', project_id: @project, id: @forum %> » <%= t(:label_message_new) %>

    - +<%= + render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title { t(:label_message_new) } + header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_forums_path(@project), text: t(:label_forum_plural) }, + { href: url_for(controller: '/forums', action: 'show', project_id: @project, id: @forum), text: @forum.name }, + t(:label_message_new)]) + end +%> <%= labelled_tabular_form_for @message, url: forum_topics_path(@forum), html: { diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb index f294bd74e145..5ec9cbdda7d5 100644 --- a/app/views/messages/show.html.erb +++ b/app/views/messages/show.html.erb @@ -27,48 +27,12 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<% breadcrumb_paths( - link_to(t(:label_forum_plural), project_forums_path(@project)), - link_to(h(@forum.name), project_forum_path(@project, @forum))) -%> +<%= render Messages::ShowPageHeaderComponent.new(topic: @topic, message: @message, forum: @forum, project:@project) %> <% content_controller 'forum-messages', dynamic: true %> <% title avatar(@topic.author) + h(@topic.subject) %> -<%= toolbar title: title do %> -
  • - <%= watcher_link(@topic, User.current) %> -
  • -
  • - <% if !@topic.locked? && authorize_for('messages', 'reply') %> - <%= link_to( - { action: 'quote', id: @topic }, - data: { 'action': 'forum-messages#quote' }, - class: 'boards--quote-button button' - ) do %> - <%= op_icon('button--icon icon-quote') %> - <%= t(:button_quote) %> - <% end %> - <% end %> -
  • -
  • - <% if @message.editable_by?(User.current) %> - <%= link_to(edit_topic_path(@topic), accesskey: accesskey(:edit), class: 'button') do %> - <%= op_icon('button--icon icon-edit') %> - <%= t(:button_edit) %> - <% end %> - <% end %> -
  • -
  • - <% if @message.destroyable_by?(User.current) %> - <%= link_to(topic_path(@topic), method: :delete, data: { confirm: t(:text_are_you_sure) }, class: 'button') do %> - <%= op_icon('button--icon icon-delete') %> - <%= t(:button_delete) %> - <% end %> - <% end %> -
  • -<% end %>

    <%= authoring @topic.created_at, @topic.author %>

    diff --git a/app/views/news/edit.html.erb b/app/views/news/edit.html.erb index dd8cadc3a921..0509feb45196 100644 --- a/app/views/news/edit.html.erb +++ b/app/views/news/edit.html.erb @@ -28,7 +28,15 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_news_edit) %> -<%= toolbar title: News.model_name.human %> +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { @news.title } + header.with_breadcrumbs([*([ href: home_path, text: organization_name ] unless @project), + *([ href: project_overview_path(@project.id), text: @project.name ] if @project), + *([ href: project_news_index_path(@project.id), text: t(:label_news_plural) ] if @project), + @news.title]) + end +%> <%= labelled_tabular_form_for @news, html: { id: 'news-form' } do |f| %> <%= render partial: 'form', locals: { f: f } %> diff --git a/app/views/news/new.html.erb b/app/views/news/new.html.erb index f031848d2b34..383750570f35 100644 --- a/app/views/news/new.html.erb +++ b/app/views/news/new.html.erb @@ -26,10 +26,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. See COPYRIGHT and LICENSE files for more details. ++#%> - <% html_title t(:label_news_new) %> -<%= toolbar title: t(:label_news_new) %> +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { t(:label_news_new) } + header.with_breadcrumbs([{ href: project_overview_path(@project.id), text: @project.name }, + { href: project_news_index_path(@project), text: t(:label_news_plural)}, + t(:label_news_new)]) + end +%> <%= labelled_tabular_form_for [@project, @news], html: { id: 'news-form' } do |f| %> <%= render partial: 'news/form', locals: { f: f } %> diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb index 6e6820b3def3..317fc5b8a42e 100644 --- a/app/views/news/show.html.erb +++ b/app/views/news/show.html.erb @@ -27,33 +27,46 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= toolbar title: "#{avatar(@news.author)} #{h @news.title}".html_safe do %> - <% if User.current.allowed_in_project?(:manage_news, @project) %> -
  • - <%= link_to(edit_news_path(@news), - accesskey: accesskey(:edit), - class: 'button edit-news-button') do %> - <%= op_icon('button--icon icon-edit') %> - <%= t(:button_edit) %> - <% end %> - <% csp_onclick('jQuery("#edit-news").show()', '.edit-news-button') %> -
  • - <% end %> -
  • - <%= watcher_link(@news, User.current) %> -
  • - <% if User.current.allowed_in_project?(:manage_news, @project) %> -
  • - <%= link_to(news_path(@news), - data: { confirm: t(:text_are_you_sure) }, - method: :delete, - class: 'button') do %> - <%= op_icon('button--icon icon-delete') %> - <%= t(:button_delete) %> - <% end %> -
  • - <% end %> -<% end %> +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { "#{avatar(@news.author)} #{h @news.title}".html_safe } + header.with_breadcrumbs([*([ href: home_path, text: organization_name ] unless @project), + *([ href: project_overview_path(@project.id), text: @project.name ] if @project), + *([ href: project_news_index_path(@project.id), text: t(:label_news_plural) ] if @project), + @news.title]) + if User.current.allowed_in_project?(:manage_news, @project) + header.with_action_button(tag: :a, + mobile_icon: :pencil, + mobile_label: t(:button_edit), + size: :medium, + href: edit_news_path(@news), + aria: { label: I18n.t(:button_edit) }, + title: I18n.t(:button_edit)) do |button| + csp_onclick('jQuery("#edit-news").show()', '.edit-news-button') + button.with_leading_visual_icon(icon: :pencil) + t(:button_edit) + end + end + watcher_button_args = watcher_button_arguments(@news, User.current) + header.with_action_button(**watcher_button_args) do |button| + button.with_leading_visual_icon(icon: watcher_button_args[:mobile_icon]) + watcher_button_args[:mobile_label] + end + if User.current.allowed_in_project?(:manage_news, @project) + header.with_action_button(scheme: :danger, + tag: :a, + href: news_path(@news), + mobile_icon: :trash, + mobile_label: I18n.t("button_delete"), + data: { confirm: t(:text_are_you_sure), method: :delete, }, + aria: { label: I18n.t("button_delete") }, + ) do |button| + button.with_leading_visual_icon(icon: :trash) + I18n.t("button_delete") + end + end + end +%> <% if authorize_for('news', 'edit') %>