diff --git a/Gemfile.lock b/Gemfile.lock index c236730d45d4..67c7526eaf3e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -226,35 +226,35 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (2.0.1) - actioncable (7.1.5) - actionpack (= 7.1.5) - activesupport (= 7.1.5) + actioncable (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.5) - actionpack (= 7.1.5) - activejob (= 7.1.5) - activerecord (= 7.1.5) - activestorage (= 7.1.5) - activesupport (= 7.1.5) + actionmailbox (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.5) - actionpack (= 7.1.5) - actionview (= 7.1.5) - activejob (= 7.1.5) - activesupport (= 7.1.5) + actionmailer (7.1.5.1) + actionpack (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activesupport (= 7.1.5.1) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.5) - actionview (= 7.1.5) - activesupport (= 7.1.5) + actionpack (7.1.5.1) + actionview (= 7.1.5.1) + activesupport (= 7.1.5.1) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -265,31 +265,31 @@ GEM actionpack-xml_parser (2.0.1) actionpack (>= 5.0) railties (>= 5.0) - actiontext (7.1.5) - actionpack (= 7.1.5) - activerecord (= 7.1.5) - activestorage (= 7.1.5) - activesupport (= 7.1.5) + actiontext (7.1.5.1) + actionpack (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.5) - activesupport (= 7.1.5) + actionview (7.1.5.1) + activesupport (= 7.1.5.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.5) - activesupport (= 7.1.5) + activejob (7.1.5.1) + activesupport (= 7.1.5.1) globalid (>= 0.3.6) - activemodel (7.1.5) - activesupport (= 7.1.5) + activemodel (7.1.5.1) + activesupport (= 7.1.5.1) activemodel-serializers-xml (1.0.3) activemodel (>= 5.0.0.a) activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (7.1.5) - activemodel (= 7.1.5) - activesupport (= 7.1.5) + activerecord (7.1.5.1) + activemodel (= 7.1.5.1) + activesupport (= 7.1.5.1) timeout (>= 0.4.0) activerecord-import (1.8.1) activerecord (>= 4.2) @@ -302,13 +302,13 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 4) railties (>= 6.1) - activestorage (7.1.5) - actionpack (= 7.1.5) - activejob (= 7.1.5) - activerecord (= 7.1.5) - activesupport (= 7.1.5) + activestorage (7.1.5.1) + actionpack (= 7.1.5.1) + activejob (= 7.1.5.1) + activerecord (= 7.1.5.1) + activesupport (= 7.1.5.1) marcel (~> 1.0) - activesupport (7.1.5) + activesupport (7.1.5.1) base64 benchmark (>= 0.3) bigdecimal @@ -330,7 +330,7 @@ GEM public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) afm (0.2.2) - airbrake (13.0.4) + airbrake (13.0.5) airbrake-ruby (~> 6.0) airbrake-ruby (6.2.2) rbtree3 (~> 0.6) @@ -344,7 +344,7 @@ GEM awesome_nested_set (3.8.0) activerecord (>= 4.0.0, < 8.1) aws-eventstream (1.3.0) - aws-partitions (1.1017.0) + aws-partitions (1.1022.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -353,7 +353,7 @@ GEM aws-sdk-kms (1.96.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.176.0) + aws-sdk-s3 (1.176.1) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -916,20 +916,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (7.1.5) - actioncable (= 7.1.5) - actionmailbox (= 7.1.5) - actionmailer (= 7.1.5) - actionpack (= 7.1.5) - actiontext (= 7.1.5) - actionview (= 7.1.5) - activejob (= 7.1.5) - activemodel (= 7.1.5) - activerecord (= 7.1.5) - activestorage (= 7.1.5) - activesupport (= 7.1.5) + rails (7.1.5.1) + actioncable (= 7.1.5.1) + actionmailbox (= 7.1.5.1) + actionmailer (= 7.1.5.1) + actionpack (= 7.1.5.1) + actiontext (= 7.1.5.1) + actionview (= 7.1.5.1) + activejob (= 7.1.5.1) + activemodel (= 7.1.5.1) + activerecord (= 7.1.5.1) + activestorage (= 7.1.5.1) + activesupport (= 7.1.5.1) bundler (>= 1.15.0) - railties (= 7.1.5) + railties (= 7.1.5.1) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -944,9 +944,9 @@ GEM rails-i18n (7.0.10) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.5) - actionpack (= 7.1.5) - activesupport (= 7.1.5) + railties (7.1.5.1) + actionpack (= 7.1.5.1) + activesupport (= 7.1.5.1) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -1015,7 +1015,7 @@ GEM rspec-support (3.13.1) rspec-wait (1.0.1) rspec (>= 3.4) - rubocop (1.69.1) + rubocop (1.69.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) diff --git a/app/components/_index.sass b/app/components/_index.sass index aa9da94c01f9..59f6c3cdfa43 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -20,3 +20,4 @@ @import "work_packages/exports/modal_dialog_component" @import "work_package_relations_tab/index_component" @import "users/hover_card_component" +@import "enterprise_edition/banner_component" diff --git a/app/components/enterprise_edition/banner_component.html.erb b/app/components/enterprise_edition/banner_component.html.erb new file mode 100644 index 000000000000..5161729b5d58 --- /dev/null +++ b/app/components/enterprise_edition/banner_component.html.erb @@ -0,0 +1,19 @@ +<%= + grid_layout("op-ee-banner", **@system_arguments) do |grid| + grid.with_area(:'icon-container') do + content_tag :div, class: "op-ee-banner--shield" do + render(Primer::Beta::Octicon.new(icon: 'op-enterprise-addons', + size: :medium, + classes: "op-ee-banner--icon")) + end + end + grid.with_area(:'title-container') { render(Primer::Beta::Text.new) { title } } + grid.with_area(:'description-container') { render(Primer::Beta::Text.new) { description } } + grid.with_area(:'link-container') do + render(Primer::Beta::Link.new(href: href)) do |link| + link.with_trailing_visual_icon(icon: 'link-external') + link_title + end + end + end +%> diff --git a/app/components/enterprise_edition/banner_component.rb b/app/components/enterprise_edition/banner_component.rb new file mode 100644 index 000000000000..f7ac7ed39bab --- /dev/null +++ b/app/components/enterprise_edition/banner_component.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2024 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 EnterpriseEdition + # Add a general description of component here + # Add additional usage considerations or best practices that may aid the user to use the component correctly. + # @accessibility Add any accessibility considerations + class BannerComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + # @param feature_key [Symbol, NilClass] The key of the feature to show the banner for. + # @param title [String] The title of the banner. + # @param description [String] The description of the banner. + # @param href [String] The URL to link to. + # @param skip_render [Boolean] Whether to skip rendering the banner. + # @param system_arguments [Hash] <%= link_to_system_arguments_docs %> + def initialize(feature_key, + title: nil, + description: nil, + link_title: nil, + href: nil, + skip_render: !EnterpriseToken.show_banners?, + **system_arguments) + @system_arguments = system_arguments + @system_arguments[:tag] = "div" + super + + @feature_key = feature_key + @title = title + @description = description + @link_title = link_title + @href = href + @skip_render = skip_render + end + + private + + attr_reader :skip_render, + :feature_key + + def title + @title || I18n.t("ee.upsale.#{feature_key}.title", default: I18n.t("ee.upsale.title")) + end + + def description + @description || begin + I18n.t("ee.upsale.#{feature_key}.description") + rescue StandardError + I18n.t("ee.upsale.#{feature_key}.description_html") + end + rescue I18n::MissingTranslationData => e + raise e.exception( + <<~TEXT.squish + The expected '#{I18n.locale}.ee.upsale.#{feature_key}.description' key does not exist. + Ideally, provide it in the locale file. + If that isn't applicable, a description parameter needs to be provided. + TEXT + ) + end + + def link_title + @link_title || I18n.t("ee.upsale.#{feature_key}.link_title", default: I18n.t("ee.upsale.link_title")) + end + + def href + href_value = @href || OpenProject::Static::Links.links.dig(:enterprise_docs, feature_key, :href) + + unless href_value + raise "Neither a custom href is provided nor is a value set " \ + "in OpenProject::Static::Links.enterprise_docs[#{feature_key}][:href]" + end + + href_value + end + + def render? + !skip_render + end + end +end diff --git a/app/components/enterprise_edition/banner_component.sass b/app/components/enterprise_edition/banner_component.sass new file mode 100644 index 000000000000..d8ca7bc2f193 --- /dev/null +++ b/app/components/enterprise_edition/banner_component.sass @@ -0,0 +1,68 @@ +/*! + / -- copyright + / OpenProject is an open source project management software. + / Copyright (C) 2024 the OpenProject GmbH + / + / This program is free software; you can redistribute it and/or + / modify it under the terms of the GNU General Public License version 3. + / + / OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: + / Copyright (C) 2006-2013 Jean-Philippe Lang + / Copyright (C) 2010-2013 the ChiliProject Team + / + / This program is free software; you can redistribute it and/or + / modify it under the terms of the GNU General Public License + / as published by the Free Software Foundation; either version 2 + / of the License, or (at your option) any later version. + / + / This program is distributed in the hope that it will be useful, + / but WITHOUT ANY WARRANTY; without even the implied warranty of + / MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + / GNU General Public License for more details. + / + / You should have received a copy of the GNU General Public License + / along with this program; if not, write to the Free Software + / Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + / + / See COPYRIGHT and LICENSE files for more details. + / ++ + / + +$op-ee-banner--shield-width: 32px + +// This is not named op-enterprise-banner because as of now, there is still a legacy angular component that uses that block name. +.op-ee-banner + display: grid + grid-template-columns: $op-ee-banner--shield-width auto auto + grid-template-areas: "icon-container title-container" "icon-container description-container" "icon-container link-container" + grid-column-gap: 0.5rem + justify-content: left + @media screen and (min-width: $breakpoint-md) + grid-template-areas: "icon-container title-container title-container" "icon-container description-container link-container" + + &--icon-container + @extend .upsale-colored + align-self: start + justify-self: center + + &--shield + @extend .upsale-border-colored + width: $op-ee-banner--shield-width + height: 42px + border-width: 10px 5px 10px 5px + border-radius: 0 0 10px 10px + border-style: solid + display: flex + align-items: center + justify-content: center + + &--icon + width: $op-ee-banner--shield-width + height: $op-ee-banner--shield-width + + &--title-container + @extend .upsale-colored + font-weight: bold + + &--link-container + align-self: end diff --git a/app/components/shares/project_queries/upsale_component.html.erb b/app/components/shares/project_queries/upsale_component.html.erb index 1f0a77a91888..946bbda996a5 100644 --- a/app/components/shares/project_queries/upsale_component.html.erb +++ b/app/components/shares/project_queries/upsale_component.html.erb @@ -1,7 +1,7 @@ <%= modal_content.with_row(data: { 'test-selector': 'op-share-dialog-upsale-block' }) do render Primer::OpenProject::FeedbackMessage.new(icon_arguments: { icon: :"op-enterprise-addons", classes: "upsale-colored" }, border: true) do |component| - component.with_heading(tag: :h2, classes: 'upsale-colored').with_content(I18n.t(:label_enterprise_addon)) + component.with_heading(tag: :h2, classes: 'upsale-colored').with_content(I18n.t(:"ee.upsale.title")) component.with_description { I18n.t('sharing.project_queries.upsale.message') } href = "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=project-list-sharing-modal" diff --git a/app/components/shares/work_packages/modal_upsale_component.html.erb b/app/components/shares/work_packages/modal_upsale_component.html.erb index f1be3ab6dc33..036bc35a673b 100644 --- a/app/components/shares/work_packages/modal_upsale_component.html.erb +++ b/app/components/shares/work_packages/modal_upsale_component.html.erb @@ -1,7 +1,7 @@ <%= component_wrapper(tag: 'turbo-frame') do render Primer::OpenProject::FeedbackMessage.new(icon_arguments: { icon: :"op-enterprise-addons", classes: "upsale-colored" }, border: true) do |component| - component.with_heading(tag: :h2, classes: 'upsale-colored').with_content(I18n.t(:label_enterprise_addon)) + component.with_heading(tag: :h2, classes: 'upsale-colored').with_content(I18n.t(:"ee.upsale.title")) component.with_description { I18n.t('mail.sharing.work_packages.enterprise_text') } href = "#{OpenProject::Static::Links.links[:upsale][:href]}/?utm_source=unknown&utm_medium=community-edition&utm_campaign=work-package-sharing-modal" diff --git a/app/contracts/api_tokens/create_contract.rb b/app/contracts/api_tokens/create_contract.rb index f9c3d93484f4..b4835e55eb6c 100644 --- a/app/contracts/api_tokens/create_contract.rb +++ b/app/contracts/api_tokens/create_contract.rb @@ -32,7 +32,7 @@ module APITokens class CreateContract < BaseContract attribute :token_name - validates :token_name, presence: { message: I18n.t("my.access_token.errors.token_name_blank") } + validates :token_name, presence: { message: proc { I18n.t("my.access_token.errors.token_name_blank") } } validate :token_name_is_unique, unless: :token_name_is_blank? private diff --git a/app/controllers/concerns/op_turbo/component_stream.rb b/app/controllers/concerns/op_turbo/component_stream.rb index 6d3b3fa27162..6cc60cd36918 100644 --- a/app/controllers/concerns/op_turbo/component_stream.rb +++ b/app/controllers/concerns/op_turbo/component_stream.rb @@ -74,8 +74,8 @@ def add_before_via_turbo_stream(component:, target_component:) turbo_streams << target_component.insert_as_turbo_stream(component:, view_context:, action: :before) end - def render_error_flash_message_via_turbo_stream(**kwargs) - update_flash_message_via_turbo_stream(**kwargs.merge(scheme: :danger, icon: :stop)) + def render_error_flash_message_via_turbo_stream(**) + update_flash_message_via_turbo_stream(**, scheme: :danger, icon: :stop) end def update_flash_message_via_turbo_stream(message:, component: OpPrimer::FlashComponent, **) @@ -89,6 +89,12 @@ def scroll_into_view_via_turbo_stream(target, behavior: :auto, block: :start) .render_in(view_context) end + def add_caption_to_input_element_via_turbo_stream(target, caption:, clean_other_captions: true) + turbo_streams << OpTurbo::StreamComponent + .new(action: :addInputCaption, target:, caption:, clean_other_captions:) + .render_in(view_context) + end + def turbo_streams @turbo_streams ||= [] end diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index 2beb4cfd0ff5..c280ec2dfebb 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -30,14 +30,17 @@ class CustomFieldsController < ApplicationController include CustomFields::SharedActions # share logic with ProjectCustomFieldsControlller layout "admin" + # rubocop:disable Rails/LexicallyScopedActionFilter before_action :require_admin before_action :find_custom_field, only: %i(edit update destroy delete_option reorder_alphabetical) before_action :prepare_custom_option_position, only: %i(update create) before_action :find_custom_option, only: :delete_option + before_action :validate_enterprise_token, only: %i(create) + # rubocop:enable Rails/LexicallyScopedActionFilter def index # loading wp cfs exclicity to allow for eager loading - @custom_fields_by_type = CustomField.all + @custom_fields_by_type = CustomField .where.not(type: ["WorkPackageCustomField", "ProjectCustomField"]) .group_by { |f| f.class.name } @@ -64,6 +67,12 @@ def show_local_breadcrumb false end + def validate_enterprise_token + if params.dig(:custom_field, :field_format) == "hierarchy" && !EnterpriseToken.allows_to?(:custom_field_hierarchies) + render_403 + end + end + def find_custom_field @custom_field = CustomField.find(params[:id]) rescue ActiveRecord::RecordNotFound diff --git a/app/forms/custom_fields/hierarchy/item_form.rb b/app/forms/custom_fields/hierarchy/item_form.rb index 50f9d41e8529..be2e5185feb8 100644 --- a/app/forms/custom_fields/hierarchy/item_form.rb +++ b/app/forms/custom_fields/hierarchy/item_form.rb @@ -56,7 +56,7 @@ class ItemForm < ApplicationForm ) end - item_form.group(layout: :horizontal) do |button_group| + item_form.group(layout: :horizontal, align_self: :end) do |button_group| button_group.button(name: :cancel, tag: :a, label: I18n.t(:button_cancel), diff --git a/app/forms/work_packages/activities_tab/journals/submit.rb b/app/forms/work_packages/activities_tab/journals/submit.rb index 08bce83ed9ea..defd84b26d54 100644 --- a/app/forms/work_packages/activities_tab/journals/submit.rb +++ b/app/forms/work_packages/activities_tab/journals/submit.rb @@ -28,7 +28,7 @@ module WorkPackages::ActivitiesTab::Journals class Submit < ApplicationForm form do |notes_form| - notes_form.submit(name: :submit, label: "Save", scheme: :primary, + notes_form.submit(name: :submit, label: I18n.t("button_save"), scheme: :primary, data: { test_selector: "op-submit-work-package-journal-form" }) end end diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb index f9880b5d14e6..1c81066a9009 100644 --- a/app/helpers/custom_fields_helper.rb +++ b/app/helpers/custom_fields_helper.rb @@ -219,7 +219,7 @@ def label_for_custom_field_format(format_string) label = format.label.is_a?(Proc) ? format.label.call : I18n.t(format.label) show_enterprise_text = format_string == "hierarchy" && !EnterpriseToken.allows_to?(:custom_field_hierarchies) - suffix = show_enterprise_text ? " (#{I18n.t(:label_enterprise_addon)})" : "" + suffix = show_enterprise_text ? " (#{I18n.t(:"ee.upsale.title")})" : "" "#{label}#{suffix}" end diff --git a/app/menus/notifications/menu.rb b/app/menus/notifications/menu.rb index cc6961c823ef..8b03c3864285 100644 --- a/app/menus/notifications/menu.rb +++ b/app/menus/notifications/menu.rb @@ -63,7 +63,7 @@ def inbox_menu end def reason_filters - %w[mentioned assigned responsible watched dateAlert shared].map do |reason| + %w[mentioned assigned responsible watched dateAlert shared reminder].map do |reason| count = unread_by_reason[reason] menu_item(title: I18n.t("notifications.reasons.#{reason}"), icon_key: reason, @@ -128,7 +128,8 @@ def icon_map "responsible" => :"op-person-accountable", "watched" => :eye, "shared" => :"share-android", - "dateAlert" => :"op-calendar-alert" + "dateAlert" => :"op-calendar-alert", + "reminder" => :clock } end diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index f7f148183239..1808c08da8e1 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -326,12 +326,15 @@ def possible_version_values_options(obj) def possible_user_values_options(obj) mapped_with_deduced_project(obj) do |project| - if project&.persisted? - project.principals - else - Principal - .in_visible_project_or_me(User.current) - end + scope = if project&.persisted? + project.principals + else + Principal + .in_visible_project_or_me(User.current) + end + + scope + .select(*(User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s) << "id")) end end diff --git a/app/models/work_package/pdf_export/common/common.rb b/app/models/work_package/pdf_export/common/common.rb index c91d44836ab5..63c055a40044 100644 --- a/app/models/work_package/pdf_export/common/common.rb +++ b/app/models/work_package/pdf_export/common/common.rb @@ -35,7 +35,7 @@ module WorkPackage::PDFExport::Common::Common private - def get_pdf(_language) + def get_pdf ::WorkPackage::PDFExport::Common::View.new(current_language) end @@ -164,6 +164,13 @@ def draw_text_multiline_part(line, text_style, x_position, y_position) measure_text_height(line, text_style) end + def ellipsis_if_longer(text, available_width, text_style) + title_text_width = measure_text_width(text, text_style) + return text if title_text_width < available_width + + truncate_ellipsis(text, available_width, text_style) + end + def truncate_ellipsis(text, available_width, text_style) line = text.dup while line.present? && (measure_text_width("#{line}...", text_style) > available_width) @@ -285,4 +292,12 @@ def footer_date def current_page_nr pdf.page_number + @page_count - (with_cover? ? 1 : 0) end + + def write_horizontal_line(y_position, height, color) + draw_horizontal_line( + y_position, + pdf.bounds.left, pdf.bounds.right, + height, color + ) + end end diff --git a/app/models/work_package/pdf_export/document_generator.rb b/app/models/work_package/pdf_export/document_generator.rb index 37b69c6b4ea0..26a3a46ceb88 100644 --- a/app/models/work_package/pdf_export/document_generator.rb +++ b/app/models/work_package/pdf_export/document_generator.rb @@ -52,7 +52,7 @@ def initialize(work_package, _options = {}) end def setup_page! - self.pdf = get_pdf(current_language) + self.pdf = get_pdf end def export! diff --git a/app/models/work_package/pdf_export/export/cover.rb b/app/models/work_package/pdf_export/export/cover.rb index e50d0c101c30..a91eed6ead2f 100644 --- a/app/models/work_package/pdf_export/export/cover.rb +++ b/app/models/work_package/pdf_export/export/cover.rb @@ -40,17 +40,34 @@ def write_cover_page! def write_cover_hero max_width = pdf.bounds.width - styles.cover_hero_padding[:right_padding] float_top = write_background_image - float_top -= write_hero_title(float_top, max_width) if project + float_top -= write_hero_title(float_top, max_width) float_top -= write_hero_heading(float_top, max_width) - write_hero_subheading(float_top, max_width) unless User.current.nil? + float_top -= write_hero_dates(float_top, max_width) + write_hero_subheading(float_top, max_width) end def available_title_height(current_y) current_y - - styles.cover_hero_title_max_height - - styles.cover_hero_title_spacing - - styles.cover_hero_heading_spacing - - styles.cover_hero_subheading_max_height + cover_hero_title_max_height - + cover_hero_heading_max_height - + cover_hero_dates_max_height - + cover_hero_subheading_max_height + end + + def cover_hero_title_max_height + cover_page_title&.then { styles.cover_hero_title_max_height + styles.cover_hero_title_spacing } || 0 + end + + def cover_hero_heading_max_height + cover_page_heading&.then { styles.cover_hero_heading_spacing } || 0 + end + + def cover_hero_dates_max_height + cover_page_dates&.then { styles.cover_hero_dates_max_height } || 0 + end + + def cover_hero_subheading_max_height + cover_page_title&.then { styles.cover_hero_title_max_height + styles.cover_hero_title_spacing } || 0 end def write_cover_hr @@ -81,27 +98,44 @@ def validate_cover_text_color end def write_hero_title(top, width) + return 0 if cover_page_title.blank? + write_hero_text( top:, width:, - text: project.name, + text: cover_page_title, text_style: styles.cover_hero_title, height: styles.cover_hero_title_max_height ) + styles.cover_hero_title_spacing end def write_hero_heading(top, width) + return 0 if cover_page_heading.blank? + write_hero_text( top:, width:, - text: heading, + text: cover_page_heading, text_style: styles.cover_hero_heading, height: available_title_height(top) ) + styles.cover_hero_heading_spacing end + def write_hero_dates(top, width) + return 0 if cover_page_dates.blank? + + write_hero_text( + top:, width:, + text: cover_page_dates, + text_style: styles.cover_hero_dates, + height: styles.cover_hero_dates_max_height + ) + styles.cover_hero_dates_spacing + end + def write_hero_subheading(top, width) + return 0 if cover_page_subheading.blank? + write_hero_text( top:, width:, - text: User.current.name, + text: cover_page_subheading, text_style: styles.cover_hero_subheading, height: styles.cover_hero_subheading_max_height ) diff --git a/app/models/work_package/pdf_export/export/schema.json b/app/models/work_package/pdf_export/export/schema.json index 50c64700b44a..316f60469667 100644 --- a/app/models/work_package/pdf_export/export/schema.json +++ b/app/models/work_package/pdf_export/export/schema.json @@ -193,6 +193,42 @@ } ] }, + "dates" : { + "title" : "The dates block in the hero", + "type" : "object", + "x-example" : { + "heading" : { + "spacing" : 10, + "max_height" : 20, + "size" : 32, + "color" : "414d5f", + "styles" : [ + "bold" + ] + } + }, + "properties" : { + "max_height" : { + "title" : "Maximum height of the block", + "examples" : [ + 30 + ], + "$ref" : "#/$defs/measurement" + }, + "spacing" : { + "title" : "Minimum spacing between dates and subheading", + "examples" : [ + 10 + ], + "$ref" : "#/$defs/measurement" + } + }, + "allOf" : [ + { + "$ref" : "#/$defs/font" + } + ] + }, "subheading" : { "title" : "The last block in the hero", "type" : "object", diff --git a/app/models/work_package/pdf_export/export/standard.yml b/app/models/work_package/pdf_export/export/standard.yml index 915d6dd37c2e..ecfc8a0a904b 100644 --- a/app/models/work_package/pdf_export/export/standard.yml +++ b/app/models/work_package/pdf_export/export/standard.yml @@ -275,9 +275,14 @@ cover: styles: - bold size: 16 + dates: + spacing: 4 + max_height: 16 + color: '414d5f' + size: 10 + styles: + - bold subheading: max_height: 30 color: '414d5f' - styles: - - italic size: 10 diff --git a/app/models/work_package/pdf_export/export/style.rb b/app/models/work_package/pdf_export/export/style.rb index 4a1a152adf1f..8e4197138903 100644 --- a/app/models/work_package/pdf_export/export/style.rb +++ b/app/models/work_package/pdf_export/export/style.rb @@ -253,6 +253,18 @@ def cover_hero_heading_spacing resolve_pt(@styles.dig(:cover, :hero, :heading, :spacing), 0) end + def cover_hero_dates + resolve_font(@styles.dig(:cover, :hero, :dates)) + end + + def cover_hero_dates_spacing + resolve_pt(@styles.dig(:cover, :hero, :dates, :spacing), 0) + end + + def cover_hero_dates_max_height + resolve_pt(@styles.dig(:cover, :hero, :dates, :max_height), 0) + end + def cover_hero_subheading resolve_font(@styles.dig(:cover, :hero, :subheading)) end diff --git a/app/models/work_package/pdf_export/work_package_list_to_pdf.rb b/app/models/work_package/pdf_export/work_package_list_to_pdf.rb index 9a1ca49c949e..b62855f71c5b 100644 --- a/app/models/work_package/pdf_export/work_package_list_to_pdf.rb +++ b/app/models/work_package/pdf_export/work_package_list_to_pdf.rb @@ -86,7 +86,7 @@ def export! private def setup_page! - self.pdf = get_pdf(current_language) + self.pdf = get_pdf configure_page_size!(wants_report? ? :portrait : :landscape) end @@ -111,6 +111,22 @@ def with_cover? wants_report? end + def cover_page_title + project&.name + end + + def cover_page_heading + heading + end + + def cover_page_subheading + User.current&.name + end + + def cover_page_dates + nil + end + def render_work_packages_pdfs(work_packages, filename) write_cover_page! if with_cover? if wants_gantt? diff --git a/app/models/work_package/pdf_export/work_package_to_pdf.rb b/app/models/work_package/pdf_export/work_package_to_pdf.rb index 5dcfa09dd8c8..2006616c0fc4 100644 --- a/app/models/work_package/pdf_export/work_package_to_pdf.rb +++ b/app/models/work_package/pdf_export/work_package_to_pdf.rb @@ -63,7 +63,7 @@ def export! end def setup_page! - self.pdf = get_pdf(current_language) + self.pdf = get_pdf @page_count = 0 configure_page_size!(:portrait) end diff --git a/app/services/duration_converter.rb b/app/services/duration_converter.rb index e2efe5a771e7..c16c3b58b0bf 100644 --- a/app/services/duration_converter.rb +++ b/app/services/duration_converter.rb @@ -96,7 +96,7 @@ def valid?(duration) false end - def output(duration_in_hours) + def output(duration_in_hours, format: default_format) return duration_in_hours if duration_in_hours.nil? seconds = (duration_in_hours * 3600).to_i @@ -142,7 +142,7 @@ def do_parse(duration_string) **duration_length_options) / 3600.to_f end - def format + def default_format Setting.duration_format == "days_and_hours" ? :days_and_hours : :hours_only end diff --git a/app/views/wiki/export_multiple.html.erb b/app/views/wiki/export_multiple.html.erb index 83462dae26a2..f7f3daa7f2f8 100644 --- a/app/views/wiki/export_multiple.html.erb +++ b/app/views/wiki/export_multiple.html.erb @@ -53,7 +53,7 @@ See COPYRIGHT and LICENSE files for more details. <% @pages.each do |page| %>