diff --git a/.github/workflows/pullpreview.yml b/.github/workflows/pullpreview.yml index cd0d8e3680f..aea28fec491 100644 --- a/.github/workflows/pullpreview.yml +++ b/.github/workflows/pullpreview.yml @@ -35,7 +35,8 @@ jobs: echo "OPENPROJECT_HSTS=false" >> .env.pullpreview echo "OPENPROJECT_NOTIFICATIONS_POLLING_INTERVAL=10000" >> .env.pullpreview echo "OPENPROJECT_ENTERPRISE__CHARGEBEE__SITE=openproject-enterprise-test" >> .env.pullpreview - echo "OPENPROJECT_ENTERPRISE__TRIAL__CREATION__HOST=start.openproject-edge.com" >> .env.pullpreview + echo "OPENPROJECT_ENTERPRISE__TRIAL__CREATION__HOST=https://start.openproject-edge.com" >> .env.pullpreview + echo "OPENPROJECT_FEATURE_BLOCK_NOTE_EDITOR=true" >> .env.pullpreview - name: Boot as BIM edition if: contains(github.ref, 'bim/') || contains(github.head_ref, 'bim/') run: | diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index cb177f708ab..38665570c93 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -25,17 +25,15 @@ jobs: name: Units + Features if: github.repository == 'opf/openproject' runs-on: - labels: - - runs-on - - runner=32cpu-linux-x64 - - family=m7+c7+r7+i7 - - ram=128 - - run-id=${{ github.run_id }} + labels: "runs-on=${{ github.run_id }}/runner=32cpu-linux-x64/family=m7+c7+r7+i7/ram=128" timeout-minutes: 40 env: DOCKER_BUILDKIT: 1 CI_RETRY_COUNT: 3 steps: + - uses: runs-on/action@v2 + with: + metrics: cpu,memory,disk,io,network - uses: actions/checkout@v4 - name: Cache DOCKER id: cache_docker diff --git a/Gemfile b/Gemfile index 8f3a976b5a3..c5e1479530d 100644 --- a/Gemfile +++ b/Gemfile @@ -160,7 +160,7 @@ gem "structured_warnings", "~> 0.5.0" gem "airbrake", "~> 13.0.0", require: false gem "markly", "~> 0.13" # another markdown parser like commonmarker, but with AST support used in PDF export -gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "be1ab00e2c49ce2e5e08a4edb4733438081c53ab" +gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "9961752e4d1e990ec1d4bf48436de9277838763f" gem "prawn", "~> 2.4" gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved. @@ -419,4 +419,4 @@ end gem "openproject-octicons", "~>19.25.0" gem "openproject-octicons_helper", "~>19.25.0" -gem "openproject-primer_view_components", "~>0.70.1" +gem "openproject-primer_view_components", "~>0.70.2" diff --git a/Gemfile.lock b/Gemfile.lock index 0f78d0bd173..5de60b89442 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,10 +8,10 @@ GIT GIT remote: https://github.com/opf/md-to-pdf - revision: be1ab00e2c49ce2e5e08a4edb4733438081c53ab - ref: be1ab00e2c49ce2e5e08a4edb4733438081c53ab + revision: 9961752e4d1e990ec1d4bf48436de9277838763f + ref: 9961752e4d1e990ec1d4bf48436de9277838763f specs: - md_to_pdf (0.2.3) + md_to_pdf (0.2.4) base64 (~> 0.2) bigdecimal (~> 3.1) color_conversion (~> 0.1) @@ -344,8 +344,8 @@ GEM awesome_nested_set (3.8.0) activerecord (>= 4.0.0, < 8.1) aws-eventstream (1.4.0) - aws-partitions (1.1118.0) - aws-sdk-core (3.226.0) + aws-partitions (1.1120.0) + aws-sdk-core (3.226.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -355,7 +355,7 @@ GEM aws-sdk-kms (1.105.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.190.0) + aws-sdk-s3 (1.191.0) aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -429,14 +429,14 @@ GEM descendants_tracker (~> 0.0.1) color_conversion (0.1.2) colored2 (4.0.3) - commonmarker (2.3.0) + commonmarker (2.3.1) rb_sys (~> 0.9) - commonmarker (2.3.0-aarch64-linux) - commonmarker (2.3.0-aarch64-linux-musl) - commonmarker (2.3.0-arm64-darwin) - commonmarker (2.3.0-x86_64-darwin) - commonmarker (2.3.0-x86_64-linux) - commonmarker (2.3.0-x86_64-linux-musl) + commonmarker (2.3.1-aarch64-linux) + commonmarker (2.3.1-aarch64-linux-musl) + commonmarker (2.3.1-arm64-darwin) + commonmarker (2.3.1-x86_64-darwin) + commonmarker (2.3.1-x86_64-linux) + commonmarker (2.3.1-x86_64-linux-musl) compare-xml (0.66) nokogiri (~> 1.8) concurrent-ruby (1.3.5) @@ -496,7 +496,7 @@ GEM concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-monads (1.8.3) + dry-monads (1.9.0) concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) @@ -857,7 +857,7 @@ GEM actionview openproject-octicons (= 19.25.0) railties - openproject-primer_view_components (0.70.1) + openproject-primer_view_components (0.70.2) actionview (>= 7.1.0) activesupport (>= 7.1.0) openproject-octicons (>= 19.25.0) @@ -1077,7 +1077,7 @@ GEM rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.4) + rspec-core (3.13.5) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) @@ -1096,7 +1096,7 @@ GEM rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.13.4) - rspec-wait (1.0.1) + rspec-wait (1.0.2) rspec (>= 3.4) rubocop (1.77.0) json (~> 2.3) @@ -1165,7 +1165,7 @@ GEM securerandom (0.4.1) selenium-devtools (0.137.0) selenium-webdriver (~> 4.2) - selenium-webdriver (4.33.0) + selenium-webdriver (4.34.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -1448,7 +1448,7 @@ DEPENDENCIES openproject-octicons (~> 19.25.0) openproject-octicons_helper (~> 19.25.0) openproject-openid_connect! - openproject-primer_view_components (~> 0.70.1) + openproject-primer_view_components (~> 0.70.2) openproject-recaptcha! openproject-reporting! openproject-storages! @@ -1582,10 +1582,10 @@ CHECKSUMS auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0 awesome_nested_set (3.8.0) sha256=469daff411d80291dbb80d1973133e498048a7afc2519c545f62d2cdebc60eda aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1118.0) sha256=1af3c8814245ed0571012f67f8a23e3feea357291471727a8f4107aea68aad20 - aws-sdk-core (3.226.0) sha256=6b2dca6576d965b8c7ddf393fe49dac6aea2b6f4a07ead0c35306bba2b0c90f5 + aws-partitions (1.1120.0) sha256=9dd2c4de7780ad0b51f6314a9cefb405e7e5cb678c67c5fecdc8b60367226d6b + aws-sdk-core (3.226.1) sha256=6e0fcee9d5fb8792c785ce5ae6a7af266e16e09e24bc623f13db7e91047ff5eb aws-sdk-kms (1.105.0) sha256=d7756c1c2e3a79afaceb7c590f3846d2814cf217e28971c243608c41d65bae4f - aws-sdk-s3 (1.190.0) sha256=75c9e609796713b28c6428bd1d1be1c3f65b1dc5a541590c9b515183c1d85a2b + aws-sdk-s3 (1.191.0) sha256=62b541dbaee2925cbc9d21d2003cd4270f778ddbdb7a7dd58a5343514004dfd3 aws-sdk-sns (1.100.0) sha256=706bb13c5da789a5b9c6219b397980645536dd7f7301c09fbb8d3e8613f65a96 aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 axe-core-api (4.10.3) sha256=6e10f3ed1c031804f16e8154d9d5dc658564d10850cee860e125fe665c3f0148 @@ -1617,13 +1617,13 @@ CHECKSUMS coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc color_conversion (0.1.2) sha256=99bea5fa412e1527a11389975aa6ad445ff8528ebae202c11d08c45ea2b94c96 colored2 (4.0.3) sha256=63e1038183976287efc43034f5cca17fb180b4deef207da8ba78d051cbce2b37 - commonmarker (2.3.0) sha256=74fb85e4ae59a9fc166dd1813ad791805d09dec1a4e79c81a7be0a6a8dc5dc63 - commonmarker (2.3.0-aarch64-linux) sha256=63e6c599ed2b1a01c012e5524ed25f60ab1132c6b582bcef0aa542770d3087ee - commonmarker (2.3.0-aarch64-linux-musl) sha256=2be68fc9106fe5b0a92928cf2903c0924ee2ef862fefd2b5c7943292902e79ab - commonmarker (2.3.0-arm64-darwin) sha256=fb5a6416395cbac63bb7741887c74e17141be75f407a292ab98cd6c8ab4894a2 - commonmarker (2.3.0-x86_64-darwin) sha256=65919fe63bc1e01319d958351ae611cc913bd2a8aa3f1a4f3fb3d12e82fbc568 - commonmarker (2.3.0-x86_64-linux) sha256=a6f2b523be0c3e4752aaf55cc5026239080b81f91bd2f72226b6e71919d204d0 - commonmarker (2.3.0-x86_64-linux-musl) sha256=af895be1dfe723a1ae2671fd147d082e1be107be732b7a52e2e0d5daa1ae687a + commonmarker (2.3.1) sha256=8943ef0731a4205765b1ab8f25a7a9b9f62acb28b0054c7d60f06720a23cadc7 + commonmarker (2.3.1-aarch64-linux) sha256=43b940cb5a4d59378c68d1dcf4480b4223817aaf53acebe262467eca8dd14807 + commonmarker (2.3.1-aarch64-linux-musl) sha256=57971a5429973823f315a861210883640c864182cd622e3691b3d71321c9b3aa + commonmarker (2.3.1-arm64-darwin) sha256=e1c8991b92ea971b8933621124f6461ef06ea64c031429d8b8ebd297dab790dc + commonmarker (2.3.1-x86_64-darwin) sha256=75475606be508f0e3dbae03612516e89e8cbf8d0913c172729b53d0c30853d5e + commonmarker (2.3.1-x86_64-linux) sha256=afa0df3f64076f0fe996120783db6af28b6d634019ff3a954155884d409caf2a + commonmarker (2.3.1-x86_64-linux-musl) sha256=70556fce0bc3f67026e5a20ea9881080755cae80d165374c88498402ba9536a5 compare-xml (0.66) sha256=e21aa5c0f69ef1177eced997c688fd4df989084e74a1b612257af32e1dd05319 concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6 connection_pool (2.5.3) sha256=cfd74a82b9b094d1ce30c4f1a346da23ee19dc8a062a16a85f58eab1ced4305b @@ -1656,7 +1656,7 @@ CHECKSUMS dry-inflector (1.2.0) sha256=22f5d0b50fd57074ae57e2ca17e3b300e57564c218269dcf82ff3e42d3f38f2e dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3 dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2 - dry-monads (1.8.3) sha256=5fbc06ae4ff76ae081922a902be998673703304d10b46b08931696f2c8decc06 + dry-monads (1.9.0) sha256=9348a67b5c862c7a876342dbd94737fdf3fb3c17978382cf6801a85b27215816 dry-schema (1.14.1) sha256=2fcd7539a7099cacae6a22f6a3a2c1846fe5afeb1c841cde432c89c6cb9b9ff1 dry-types (1.8.2) sha256=c84e9ada69419c727c3b12e191e0ed7d2c6d58d040d55e79ea16e0ebf8b3ec0f dry-validation (1.11.1) sha256=70900bb5a2d911c8aab566d3e360c6bff389b8bf92ea8e04885ce51c41ff8085 @@ -1763,7 +1763,7 @@ CHECKSUMS marcel (1.0.4) sha256=0d5649feb64b8f19f3d3468b96c680bae9746335d02194270287868a661516a4 markly (0.13.0) sha256=326a232c876cd95093d110b8d184be8effeadd776c3ffcefd59bdc8de7f59e0d matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b - md_to_pdf (0.2.3) + md_to_pdf (0.2.4) messagebird-rest (1.4.2) sha256=1501e16ad76c87558f20f3b26cb39d05f587b349b44b8bf8056b040e9b495301 meta-tags (2.22.1) sha256=e5ae1febbd320d396c7226d7edb868e5d63466c14b9c8b06622a1a74e6dce354 method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5 @@ -1821,7 +1821,7 @@ CHECKSUMS openproject-octicons (19.25.0) sha256=16fc221375e693f0e893b1c208286f2d7719ae4dfe080c5415642b221f51f550 openproject-octicons_helper (19.25.0) sha256=9b1778a67b0015ebe84ca0471f74e31004b985a8dcaaa443f7a2ac365b0a4e2d openproject-openid_connect (1.0.0) - openproject-primer_view_components (0.70.1) sha256=6ba2eee9308d15e45b8fa5aac5082bad8da78f38ce948eedb5feb3ea48841705 + openproject-primer_view_components (0.70.2) sha256=b08bc35f1edb00c583544e8757badf8914ba7e32c88a2eb187de53a6d688ba0a openproject-recaptcha (1.0.0) openproject-reporting (1.0.0) openproject-storages (1.0.0) @@ -1911,13 +1911,13 @@ CHECKSUMS rotp (6.3.0) sha256=75d40087e65ed0d8022c33055a6306c1c400d1c12261932533b5d6cbcd868854 rouge (4.5.2) sha256=034233fb8a69d0ad0e0476943184e04cb971b68e3c2239724e02f428878b68a3 rspec (3.13.1) sha256=b9f9a58fa915b8d94a1d6b3195fe6dd28c4c34836a6097015142c4a9ace72140 - rspec-core (3.13.4) sha256=f9da156b7b775c82610a7b580624df51a55102f8c8e4a103b98f5d7a9fa23958 + rspec-core (3.13.5) sha256=ab3f682897c6131c67f9a17cfee5022a597f283aebe654d329a565f9937a4fa3 rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 rspec-mocks (3.13.5) sha256=e4338a6f285ada9fe56f5893f5457783af8194f5d08884d17a87321d5195ea81 rspec-rails (8.0.1) sha256=0c3700b10ab6d7c648c4cd554023d8c2b5b07e7f01205f7608f0c511cf686505 rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858 rspec-support (3.13.4) sha256=184b1814f6a968102b57df631892c7f1990a91c9a3b9e80ef892a0fc2a71a3f7 - rspec-wait (1.0.1) sha256=50ce9cc7cd20b8bb67b95a4ed56b59a16d4af7e86ae91b28dbf20eeaa2481d1f + rspec-wait (1.0.2) sha256=865f921239325d3d26fc10ded4bdd485d8b58bcaaad1a28dd85ed15266b5a912 rubocop (1.77.0) sha256=1f360b4575ef7a124be27b0dfffa227a2b2d9420d22d4fd8bf179d702bcc88c0 rubocop-ast (1.45.1) sha256=94042e49adc17f187ba037b33f941ba7398fede77cdf4bffafba95190a473a3e rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c @@ -1943,7 +1943,7 @@ CHECKSUMS secure_headers (7.1.0) sha256=6b1f9d5f9507af2948f4636452c41c09371927836396c2185438ffdf0a731124 securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 selenium-devtools (0.137.0) sha256=ea29581fe0502f10eb1e45492a62e2228398822180655ea94d146050fc44fa3c - selenium-webdriver (4.33.0) sha256=720cad35cb7c2fdd87706940ed107bf2ec6873e50fa45b26f497fa28a8f97e1d + selenium-webdriver (4.34.0) sha256=ec7bb718cbe66fe2b247d8ca5e6ba26caed0976d76579d7cb2fadd8dae8b271e semantic (1.6.1) sha256=3cdbb48f59198ebb782a3fdfb87b559e0822a311610db153bae22777a7d0c163 shoulda-context (2.0.0) sha256=7adf45342cd800f507d2a053658cb1cce2884b616b26004d39684b912ea32c34 shoulda-matchers (6.5.0) sha256=ef6b572b2bed1ac4aba6ab2c5ff345a24b6d055a93a3d1c3bfc86d9d499e3f44 diff --git a/app/components/_index.sass b/app/components/_index.sass index c8a5624de14..8ad4c13cffb 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -6,6 +6,7 @@ @import "open_project/common/attribute_help_text_component" @import "open_project/common/attribute_label_component" @import "open_project/common/submenu_component" +@import "open_project/common/main_menu_toggle_component" @import "projects/row_component" @import "projects/phases/hover_card_component" @import "shares/invite_user_form_component" @@ -13,6 +14,7 @@ @import "users/hover_card_component" @import "work_package_relations_tab/index_component" @import "work_package_relations_tab/relation_component" +@import "work_package_types/pattern_input" @import "work_packages/activities_tab/index_component" @import "work_packages/activities_tab/journals/new_component" @import "work_packages/activities_tab/journals/index_component" @@ -29,5 +31,4 @@ @import "work_packages/hover_card_component" @import "work_packages/progress/modal_body_component" @import "work_packages/reminder/modal_body_component" -@import "work_packages/types/pattern_input" @import "work_packages/split_view_component" diff --git a/app/components/enterprise_edition/banner_component.html.erb b/app/components/enterprise_edition/banner_component.html.erb index 623fe25cc4b..35af39eaddd 100644 --- a/app/components/enterprise_edition/banner_component.html.erb +++ b/app/components/enterprise_edition/banner_component.html.erb @@ -12,7 +12,7 @@ end grid.with_area(:content) do - flex_layout do |flex| + flex_layout(h: :full) do |flex| flex.with_row do if medium? || large? flex_layout( diff --git a/app/components/enterprise_edition/banner_component.sass b/app/components/enterprise_edition/banner_component.sass index 33a4f3825a6..030922aee91 100644 --- a/app/components/enterprise_edition/banner_component.sass +++ b/app/components/enterprise_edition/banner_component.sass @@ -73,6 +73,7 @@ $op-enterprise-banner-fixed-height-large: 320px grid-template-columns: 1fr 1fr &--content + height: $op-enterprise-banner-fixed-height-medium z-index: 1 align-self: center padding: var(--base-size-16) @@ -127,6 +128,8 @@ $op-enterprise-banner-fixed-height-large: 320px top: var(--base-size-24) right: var(--base-size-24) + &--description + flex-grow: 1 &--content padding: var(--base-size-6) var(--base-size-8) diff --git a/app/components/enterprise_edition/buy_now_button_component.rb b/app/components/enterprise_edition/buy_now_button_component.rb new file mode 100644 index 00000000000..49cf12bf830 --- /dev/null +++ b/app/components/enterprise_edition/buy_now_button_component.rb @@ -0,0 +1,48 @@ +# 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 EnterpriseEdition + class BuyNowButtonComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + + def call + render(Primer::Beta::Button.new( + classes: "upsell-colored-background hidden-for-mobile", + tag: :a, + href: OpenProject::Enterprise.upgrade_path, + align_self: :center + )) do + I18n.t(:button_buy_now) + end + end + end +end diff --git a/app/components/enterprise_edition/trial_teaser_component.html.erb b/app/components/enterprise_edition/trial_teaser_component.html.erb new file mode 100644 index 00000000000..e6f765eb068 --- /dev/null +++ b/app/components/enterprise_edition/trial_teaser_component.html.erb @@ -0,0 +1,31 @@ +<%= + render(Primer::BaseComponent.new(tag: :div, classes: "op-enterprise-banner-container")) do + grid_layout( + "op-enterprise-banner", + **@system_arguments + ) do |grid| + grid.with_area(:visual) do + render(Primer::Beta::Octicon.new(icon: :"op-enterprise-addons", classes: "op-enterprise-banner--icon")) + end + + grid.with_area(:content) do + flex_layout do |flex| + flex.with_row do + render(Primer::Beta::Text.new(font_weight: :bold)) { title } + end + + flex.with_row(classes: "op-enterprise-banner--description") do + concat render(Primer::Beta::Text.new(tag: :p)) { description } + end + + flex.with_row(mt: 1) do + render EnterpriseEdition::UpsellButtonsComponent.new( + :teaser, + justify_content: :flex_start + ) + end + end + end + end + end +%> diff --git a/app/components/enterprise_edition/trial_teaser_component.rb b/app/components/enterprise_edition/trial_teaser_component.rb new file mode 100644 index 00000000000..933374769ed --- /dev/null +++ b/app/components/enterprise_edition/trial_teaser_component.rb @@ -0,0 +1,79 @@ +# 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 EnterpriseEdition + class TrialTeaserComponent < ApplicationComponent + include OpPrimer::ComponentHelpers + + def initialize(**system_arguments) + set_system_arguments(system_arguments) + + super + end + + private + + def set_system_arguments(system_arguments) + @system_arguments = system_arguments + @system_arguments[:tag] = :div + @system_arguments[:mx] = 2 + @system_arguments[:mt] = 2 + @system_arguments[:mb] = 3 + @system_arguments[:id] = "op-enterprise-banner-teaser" + @system_arguments[:test_selector] = "op-enterprise-banner" + @system_arguments[:classes] = class_names( + @system_arguments[:classes], + "op-enterprise-banner" + ) + end + + def render? + User.current.admin? && EnterpriseToken.trial_only? + end + + def token + @token ||= EnterpriseToken.active_trial_token + end + + def title + I18n.t("ee.teaser.title", count: token.days_left, trial_plan: token.plan) + end + + def description + I18n.t("ee.teaser.description", trial_plan: plan_name).html_safe + end + + def plan_name + render(Primer::Beta::Text.new(font_weight: :bold, classes: "upsell-colored")) do + I18n.t("ee.upsell.plan_name", plan: token.plan.capitalize) + end + end + end +end diff --git a/app/components/enterprise_edition/upsell_buttons_component.rb b/app/components/enterprise_edition/upsell_buttons_component.rb index aec2538fd2b..601ee298573 100644 --- a/app/components/enterprise_edition/upsell_buttons_component.rb +++ b/app/components/enterprise_edition/upsell_buttons_component.rb @@ -75,14 +75,7 @@ module EnterpriseEdition return unless EnterpriseToken.active? return unless User.current.admin? - render(Primer::Beta::Button.new( - classes: "upsell-colored-background", - tag: :a, - href: OpenProject::Enterprise.upgrade_path, - align_self: :center - )) do - I18n.t("ee.upsell.buy_now_button") - end + render(EnterpriseEdition::BuyNowButtonComponent.new) end # Allow providing a custom upgrade now button @@ -108,6 +101,8 @@ module EnterpriseEdition end def more_info_button + return if @feature_key == :teaser + render(Primer::Beta::Link.new(href: enterprise_link)) do |link| link.with_trailing_visual_icon(icon: "link-external") link_title diff --git a/app/components/enterprise_trials/banner_component.html.erb b/app/components/enterprise_trials/banner_component.html.erb index fea750b1116..c2ec915dadc 100644 --- a/app/components/enterprise_trials/banner_component.html.erb +++ b/app/components/enterprise_trials/banner_component.html.erb @@ -25,22 +25,26 @@ end %> -
- + <%= render EnterpriseEdition::UpsellButtonsComponent.new(nil, show_buy_now: true) %> diff --git a/app/components/open_project/common/main_menu_toggle_component.rb b/app/components/open_project/common/main_menu_toggle_component.rb new file mode 100644 index 00000000000..f70760d2d8a --- /dev/null +++ b/app/components/open_project/common/main_menu_toggle_component.rb @@ -0,0 +1,78 @@ +# 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 OpenProject + module Common + class MainMenuToggleComponent < ApplicationComponent + def initialize(expanded:) + super() + + @expanded = expanded + end + + def call + render(Primer::Beta::IconButton.new(icon:, + id:, + aria: { label: aria_label }, + scheme:, + size:, + data:)) + end + + private + + def icon + @expanded ? "sidebar-expand" : :"sidebar-collapse" + end + + def id + "menu-toggle--#{@expanded ? 'collapse-button' : 'expand-button'}" + end + + def aria_label + @expanded ? I18n.t("js.label_hide_project_menu") : I18n.t("js.label_expand_project_menu") + end + + def scheme + :invisible + end + + def size + @expanded ? :medium : :small + end + + def data + { + action: "click->menus--main-toggle#toggleNavigation" + } + end + end + end +end diff --git a/app/components/open_project/common/main_menu_toggle_component.sass b/app/components/open_project/common/main_menu_toggle_component.sass new file mode 100644 index 00000000000..19b9d65e633 --- /dev/null +++ b/app/components/open_project/common/main_menu_toggle_component.sass @@ -0,0 +1,50 @@ +//-- 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. +//++ + +#menu-toggle--expand-button + position: absolute + left: 11px + z-index: 1 + + @at-root .nosidebar & + display: none + +#menu-toggle--collapse-button + .Button-visual + color: var(--main-menu-font-color) + +#wrapper + .hidden-navigation + // Push the breadcrumb to the right when the sidebar is collapsed + .PageHeader-contextBar, + .op-breadcrumbs + margin-left: 30px + + &:not(.hidden-navigation) + #menu-toggle--expand-button + display: none diff --git a/app/components/open_project/common/submenu_component.sass b/app/components/open_project/common/submenu_component.sass index bba8cd5d7cf..d88e60ad846 100644 --- a/app/components/open_project/common/submenu_component.sass +++ b/app/components/open_project/common/submenu_component.sass @@ -2,10 +2,13 @@ height: 100% display: flex flex-direction: column - overflow: hidden &--search - margin: 12px var(--main-menu-x-spacing) + position: sticky + top: 0 + z-index: 1 + background: var(--main-menu-bg-color) + padding: 12px var(--main-menu-x-spacing) color: var(--main-menu-font-color) display: flex flex-direction: column @@ -41,17 +44,18 @@ background: transparent color: var(--main-menu-fieldset-header-color) border: 1px solid transparent + border-radius: var(--borderRadius-medium) text-transform: uppercase padding: 8px var(--main-menu-x-spacing) 8px var(--main-menu-x-spacing) margin-top: 12px + margin-bottom: 2px font-size: 12px cursor: pointer width: 100% - &:hover - background: var(--main-menu-bg-hover-background) - color: var(--main-menu-hover-font-color) + background: var(--control-transparent-bgColor-hover) + color: var(--main-menu-font-color) border-color: var(--main-menu-hover-border-color) &--items @@ -60,6 +64,9 @@ &_collapsed display: none + &--item + margin-bottom: 2px !important + &--item-action display: flex align-items: center @@ -67,8 +74,8 @@ padding: 8px 12px 8px 32px &:hover - background: var(--main-menu-bg-hover-background) - color: var(--main-menu-hover-font-color) + background: var(--control-transparent-bgColor-hover) + color: var(--main-menu-font-color) &_active background: var(--main-menu-bg-selected-background) diff --git a/app/components/settings/project_custom_fields/header_component.html.erb b/app/components/settings/project_custom_fields/header_component.html.erb index cd7fd782258..0358afd3eed 100644 --- a/app/components/settings/project_custom_fields/header_component.html.erb +++ b/app/components/settings/project_custom_fields/header_component.html.erb @@ -25,20 +25,25 @@ id: "dialog-show-project-custom-field-section-dialog", tag: :a, href: new_link_admin_settings_project_custom_field_sections_path, - content_arguments: { data: { controller: "async-dialog" } } + content_arguments: { data: { controller: "async-dialog", test_selector: "add-project-custom-field-section" } } ) - menu.with_sub_menu_item(label: t("settings.project_attributes.label_new_attribute")) do |sub_menu| - OpenProject::CustomFieldFormat.available_for_class_name("Project") - .sort_by(&:name) - .map do |format| - sub_menu.with_item( - label: helpers.label_for_custom_field_format(format.name), - tag: :a, - href: new_admin_settings_project_custom_field_path(field_format: format.name), - content_arguments: { data: { turbo: "false", - test_selector: "new-project-custom-field-button" } } - ) + if allow_custom_field_creation? + menu.with_sub_menu_item( + label: t("settings.project_attributes.label_new_attribute"), + content_arguments: { data: { test_selector: "add-project-custom-field-attribute" } } + ) do |sub_menu| + OpenProject::CustomFieldFormat.available_for_class_name("Project") + .sort_by(&:name) + .map do |format| + sub_menu.with_item( + label: helpers.label_for_custom_field_format(format.name), + tag: :a, + href: new_admin_settings_project_custom_field_path(field_format: format.name), + content_arguments: { data: { turbo: "false", + test_selector: "new-project-custom-field-button" } } + ) + end end end end diff --git a/app/components/settings/project_custom_fields/header_component.rb b/app/components/settings/project_custom_fields/header_component.rb index 317c6fa5555..b46b6e36220 100644 --- a/app/components/settings/project_custom_fields/header_component.rb +++ b/app/components/settings/project_custom_fields/header_component.rb @@ -34,11 +34,21 @@ module Settings include OpTurbo::Streamable include CustomFieldsHelper + def initialize(allow_custom_field_creation:) + super + + @allow_custom_field_creation = allow_custom_field_creation + end + def breadcrumbs_items [{ href: admin_index_path, text: t("label_administration") }, { href: admin_settings_project_custom_fields_path, text: t("label_project_plural") }, t("settings.project_attributes.heading")] end + + def allow_custom_field_creation? + @allow_custom_field_creation + end end end end diff --git a/app/components/shares/manage_shares_component.rb b/app/components/shares/manage_shares_component.rb index fa61c6afdd5..f4cf92d172c 100644 --- a/app/components/shares/manage_shares_component.rb +++ b/app/components/shares/manage_shares_component.rb @@ -27,7 +27,7 @@ #++ module Shares - class ManageSharesComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + class ManageSharesComponent < ApplicationComponent include ApplicationHelper include MemberHelper include OpPrimer::ComponentHelpers @@ -106,7 +106,7 @@ module Shares return false if role_filter_value.nil? - selected_role = strategy.available_roles.find { _1[:value] == option[:value] } + selected_role = strategy.available_roles.find { it[:value] == option[:value] } selected_role[:value] == role_filter_value.to_i end diff --git a/app/components/users/profile/attributes_component.rb b/app/components/users/profile/attributes_component.rb index a0bbde12307..0283e9454af 100644 --- a/app/components/users/profile/attributes_component.rb +++ b/app/components/users/profile/attributes_component.rb @@ -28,7 +28,7 @@ module Users module Profile - class AttributesComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent + class AttributesComponent < ApplicationComponent include ApplicationHelper include OpTurbo::Streamable include OpPrimer::ComponentHelpers @@ -40,7 +40,7 @@ module Users end def render? - user_is_allowed_to_see_email || @user.visible_custom_field_values.any? { _1.value.present? } + user_is_allowed_to_see_email || @user.visible_custom_field_values.any? { it.value.present? } end def visible_custom_fields diff --git a/app/components/work_package_relations_tab/index_component.rb b/app/components/work_package_relations_tab/index_component.rb index 55617522bd9..0fe0e94e031 100644 --- a/app/components/work_package_relations_tab/index_component.rb +++ b/app/components/work_package_relations_tab/index_component.rb @@ -19,22 +19,22 @@ class WorkPackageRelationsTab::IndexComponent < ApplicationComponent ].freeze FIRST_LEVEL_RELATION_MENU_TYPES = [ - *ADD_CHILD_MENU_TYPES, + Relation::TYPE_RELATES, Relation::TYPE_FOLLOWS, Relation::TYPE_PRECEDES, - Relation::TYPE_PARENT, - Relation::TYPE_RELATES + *ADD_CHILD_MENU_TYPES, + Relation::TYPE_PARENT ].freeze SECOND_LEVEL_RELATION_MENU_TYPES = [ + Relation::TYPE_DUPLICATES, + Relation::TYPE_DUPLICATED, Relation::TYPE_BLOCKS, Relation::TYPE_BLOCKED, Relation::TYPE_INCLUDES, Relation::TYPE_PARTOF, Relation::TYPE_REQUIRES, - Relation::TYPE_REQUIRED, - Relation::TYPE_DUPLICATES, - Relation::TYPE_DUPLICATED + Relation::TYPE_REQUIRED ].freeze include ApplicationHelper diff --git a/app/components/work_packages/types/export_configuration_component.html.erb b/app/components/work_package_types/export_configuration_component.html.erb similarity index 93% rename from app/components/work_packages/types/export_configuration_component.html.erb rename to app/components/work_package_types/export_configuration_component.html.erb index 657a294e510..36a18817b90 100644 --- a/app/components/work_packages/types/export_configuration_component.html.erb +++ b/app/components/work_package_types/export_configuration_component.html.erb @@ -28,4 +28,4 @@ See COPYRIGHT and LICENSE files for more details. ++#%><%= I18n.t("types.edit.export_configuration.intro") %>
-<%= render(WorkPackages::Types::ExportTemplateListComponent.new(type: model)) %> +<%= render(WorkPackageTypes::ExportTemplateListComponent.new(type: model)) %> diff --git a/app/components/work_packages/types/export_configuration_component.rb b/app/components/work_package_types/export_configuration_component.rb similarity index 92% rename from app/components/work_packages/types/export_configuration_component.rb rename to app/components/work_package_types/export_configuration_component.rb index 17f5ff5c67a..da4839c1178 100644 --- a/app/components/work_packages/types/export_configuration_component.rb +++ b/app/components/work_package_types/export_configuration_component.rb @@ -28,9 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackages - module Types - class ExportConfigurationComponent < ApplicationComponent - end +module WorkPackageTypes + class ExportConfigurationComponent < ApplicationComponent end end diff --git a/app/components/work_packages/types/export_template_list_component.html.erb b/app/components/work_package_types/export_template_list_component.html.erb similarity index 98% rename from app/components/work_packages/types/export_template_list_component.html.erb rename to app/components/work_package_types/export_template_list_component.html.erb index 75c03fd5e32..fb4af1456a5 100644 --- a/app/components/work_packages/types/export_template_list_component.html.erb +++ b/app/components/work_package_types/export_template_list_component.html.erb @@ -76,7 +76,7 @@ See COPYRIGHT and LICENSE files for more details. @type.pdf_export_templates.list.each do |template| component.with_row(data: draggable_item_config(template)) do render( - WorkPackages::Types::ExportTemplateRowComponent.new( + WorkPackageTypes::ExportTemplateRowComponent.new( type: @type, template: ) diff --git a/app/components/work_package_types/export_template_list_component.rb b/app/components/work_package_types/export_template_list_component.rb new file mode 100644 index 00000000000..ace1a352c41 --- /dev/null +++ b/app/components/work_package_types/export_template_list_component.rb @@ -0,0 +1,67 @@ +# 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 WorkPackageTypes + class ExportTemplateListComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(type:) + super + + @type = type + end + + def wrapper_data_attributes + { + controller: "generic-drag-and-drop" + } + end + + def drag_and_drop_target_config + { + "is-drag-and-drop-target": true, + "target-container-accessor": "& > ul", + "target-allowed-drag-type": "template", + test_selector: "pdf-export-template-rows" + } + end + + def draggable_item_config(template) + { + "draggable-id": template.id, + "draggable-type": "template", + "drop-url": drop_type_pdf_export_template_path(type_id: @type.id, id: template.id), + test_selector: "pdf-export-template-row-#{template.id}" + } + end + end +end diff --git a/app/components/work_packages/types/export_template_row_component.html.erb b/app/components/work_package_types/export_template_row_component.html.erb similarity index 76% rename from app/components/work_packages/types/export_template_row_component.html.erb rename to app/components/work_package_types/export_template_row_component.html.erb index 89278f32145..de7dac1c92f 100644 --- a/app/components/work_packages/types/export_template_row_component.html.erb +++ b/app/components/work_package_types/export_template_row_component.html.erb @@ -45,16 +45,18 @@ See COPYRIGHT and LICENSE files for more details. end end item_information.with_column(py: 1, mr: 2) do - render(Primer::Alpha::ToggleSwitch.new( - src: toggle_type_pdf_export_template_path(type_id: @type.id, id: @template.id), - csrf_token: form_authenticity_token, - data: { "turbo-method": :post, "turbo-stream": true }, - checked: @template.enabled, - size: :small, - status_label_position: :start, - test_selector: "toggle-pdf-export-template-row-#{@template.id}", - classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator" - )) + render( + Primer::Alpha::ToggleSwitch.new( + src: toggle_type_pdf_export_template_path(type_id: @type.id, id: @template.id), + csrf_token: form_authenticity_token, + data: { "turbo-method": :post, "turbo-stream": true }, + checked: @template.enabled, + size: :small, + status_label_position: :start, + test_selector: "toggle-pdf-export-template-row-#{@template.id}", + classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator" + ) + ) end end end diff --git a/app/components/work_packages/types/export_template_row_component.rb b/app/components/work_package_types/export_template_row_component.rb similarity index 79% rename from app/components/work_packages/types/export_template_row_component.rb rename to app/components/work_package_types/export_template_row_component.rb index 07d9a9aaff2..dd2796f2eef 100644 --- a/app/components/work_packages/types/export_template_row_component.rb +++ b/app/components/work_package_types/export_template_row_component.rb @@ -28,18 +28,17 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackages - module Types - class ExportTemplateRowComponent < ApplicationComponent - include ApplicationHelper - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable - def initialize(type:, template:) - super +module WorkPackageTypes + class ExportTemplateRowComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable - @template = template - @type = type - end + def initialize(type:, template:) + super + + @template = template + @type = type end end end diff --git a/app/components/work_packages/types/pattern_input.html.erb b/app/components/work_package_types/pattern_input.html.erb similarity index 100% rename from app/components/work_packages/types/pattern_input.html.erb rename to app/components/work_package_types/pattern_input.html.erb diff --git a/app/components/work_package_types/pattern_input.rb b/app/components/work_package_types/pattern_input.rb new file mode 100644 index 00000000000..8915573a26d --- /dev/null +++ b/app/components/work_package_types/pattern_input.rb @@ -0,0 +1,57 @@ +# 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 WorkPackageTypes + class PatternInput < Primer::Forms::BaseComponent + prepend Primer::OpenProject::Forms::WrappedInput + + delegate :name, to: :@input + + def initialize(input:, value:, suggestions:) + super() + @input = input + @value = value + @suggestions = suggestions + end + + def suggestions_for_stimulus + @suggestions_for_stimulus ||= @suggestions.to_json + end + + def suggestions_list_component + @suggestions_list_component ||= Primer::Alpha::ActionList.new( + role: :list, + scheme: :inset, + ml: 0, + "data-pattern-input-target": "suggestions" + ) + end + end +end diff --git a/app/components/work_packages/types/pattern_input.sass b/app/components/work_package_types/pattern_input.sass similarity index 100% rename from app/components/work_packages/types/pattern_input.sass rename to app/components/work_package_types/pattern_input.sass diff --git a/app/components/work_package_types/projects_component.html.erb b/app/components/work_package_types/projects_component.html.erb new file mode 100644 index 00000000000..75fb3eee67e --- /dev/null +++ b/app/components/work_package_types/projects_component.html.erb @@ -0,0 +1,61 @@ +<%#-- 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. + +++#%> + +<%= + settings_primer_form_with(**form_options) do |form| + component_wrapper do + flex_layout do |container| + container.with_row(mb: 3) do + render( + Primer::OpenProject::TreeView.new( + expanded: true, + data: { + "admin--work-package-type-projects-target": "treeView", + action: "click->admin--work-package-type-projects#updateSelectedProjects" + }, + select_variant: :multiple + ) + ) do |tree_view| + build_project_tree(tree_view) + end + end + + container.with_row do + form.hidden_field :project_ids, + value: "[]", + data: { "admin--work-package-type-projects-target": "selectedProjects" } + end + + container.with_row do + render(Primer::Beta::Button.new(scheme: :primary, type: :submit)) { t(:button_save) } + end + end + end + end +%> diff --git a/app/components/work_package_types/projects_component.rb b/app/components/work_package_types/projects_component.rb new file mode 100644 index 00000000000..91994a746d1 --- /dev/null +++ b/app/components/work_package_types/projects_component.rb @@ -0,0 +1,82 @@ +# 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 WorkPackageTypes + class ProjectsComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def form_options + { + url: type_projects_path(type_id: model.id), + method: :put, + model:, + data: { + controller: "admin--work-package-type-projects", + "admin--work-package-type-projects-initially-selected-projects-value": model.projects.pluck(:id).join(",") + } + } + end + + def projects = options[:projects] + + def build_project_tree(tree) + nested_project_list = Project.build_projects_hierarchy(projects) + + add_sub_tree(tree, nested_project_list) + end + + private + + def add_sub_tree(tree, project_list) + project_list.each do |project_hash| + if project_hash[:children].empty? + tree.with_leaf(**item_options(project_hash[:project])) + else + tree.with_sub_tree(expanded: true, + select_strategy: :self, + **item_options(project_hash[:project])) do |sub_tree| + add_sub_tree(sub_tree, project_hash[:children]) + end + end + end + end + + def item_options(item) + { + select_variant: :multiple, + label: item.name, + data: { project_id: item.id }, + checked: model.projects.include?(item) + } + end + end +end diff --git a/app/components/work_packages/types/settings_component.html.erb b/app/components/work_package_types/settings_component.html.erb similarity index 95% rename from app/components/work_packages/types/settings_component.html.erb rename to app/components/work_package_types/settings_component.html.erb index bae0a30f35e..f8a398fc0b9 100644 --- a/app/components/work_packages/types/settings_component.html.erb +++ b/app/components/work_package_types/settings_component.html.erb @@ -29,6 +29,6 @@ See COPYRIGHT and LICENSE files for more details. <%= settings_primer_form_with(**form_options) do |f| - render(WorkPackages::Types::SettingsForm.new(f)) + render(WorkPackageTypes::SettingsForm.new(f)) end %> diff --git a/app/components/work_package_types/settings_component.rb b/app/components/work_package_types/settings_component.rb new file mode 100644 index 00000000000..a92c9fc00f8 --- /dev/null +++ b/app/components/work_package_types/settings_component.rb @@ -0,0 +1,62 @@ +# 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 WorkPackageTypes + class SettingsComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(model, copy_workflow_from: nil, **) + @copy_workflow_from = copy_workflow_from + super(model, **) + end + + def form_options + if model.new_record? + create_form_options + else + update_form_options + end + end + + private + + attr_reader :copy_workflow_from + + def create_form_options + { url: types_path, method: :post, model:, copy_workflow_from: } + end + + def update_form_options + { url: update_tab_type_path(id: model.id, tab: :settings), method: :patch, model: } + end + end +end diff --git a/app/components/work_packages/types/subject_configuration_component.html.erb b/app/components/work_package_types/subject_configuration_component.html.erb similarity index 95% rename from app/components/work_packages/types/subject_configuration_component.html.erb rename to app/components/work_package_types/subject_configuration_component.html.erb index 4c1550b80f4..6fff3153b36 100644 --- a/app/components/work_packages/types/subject_configuration_component.html.erb +++ b/app/components/work_package_types/subject_configuration_component.html.erb @@ -33,6 +33,6 @@ See COPYRIGHT and LICENSE files for more details. <%= settings_primer_form_with(**form_options) do |f| - render(WorkPackages::Types::SubjectConfigurationForm.new(f)) + render(WorkPackageTypes::SubjectConfigurationForm.new(f)) end %> diff --git a/app/components/work_package_types/subject_configuration_component.rb b/app/components/work_package_types/subject_configuration_component.rb new file mode 100644 index 00000000000..a407c2b5b94 --- /dev/null +++ b/app/components/work_package_types/subject_configuration_component.rb @@ -0,0 +1,110 @@ +# 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 WorkPackageTypes + class SubjectConfigurationComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(model, subject_configuration_form_data: nil, **) + @subject_configuration_form_data = subject_configuration_form_data + super(model, **) + end + + def form_options + form_model = subject_form_object + + { + url: subject_configuration_type_path(id: model.id), + method: :put, + model: form_model, + data: { + controller: "admin--subject-configuration", + admin__subject_configuration_hide_pattern_input_value: form_model.subject_configuration == :manual + } + } + end + + private + + def subject_form_object + values = subject_configuration_form_values + + Forms::SubjectConfigurationFormModel.new( + subject_configuration: values[:subject_configuration], + pattern: values[:pattern], + suggestions: sort_attributes(supported_attributes), + validation_errors: model.errors + ) + end + + def supported_attributes + attribute_tokens = Patterns::TokenPropertyMapper.new.tokens_for_type(model) + + result = { + work_package: { + title: I18n.t("types.edit.subject_configuration.token.context.work_package"), + tokens: [] + }, + parent: { + title: I18n.t("types.edit.subject_configuration.token.context.parent"), + tokens: [] + }, + project: { + title: I18n.t("types.edit.subject_configuration.token.context.project"), + tokens: [] + } + } + + attribute_tokens.each_with_object(result) do |token, obj| + token_hash = { key: token.key, label: token.label, label_with_context: token.label_with_context } + obj[token.context][:tokens] << token_hash + end + end + + def sort_attributes(attributes) + attributes.each_value { |group| group[:tokens] = group[:tokens].sort_by { |a| a[:label] } } + attributes + end + + def subject_configuration_form_values + if @subject_configuration_form_data.present? + @subject_configuration_form_data + else + persisted_subject_pattern = model.patterns.subject || Pattern.new(blueprint: "", enabled: false) + subject_configuration = persisted_subject_pattern.enabled ? :generated : :manual + pattern = persisted_subject_pattern.blueprint + + { subject_configuration:, pattern: } + end + end + end +end diff --git a/app/components/work_packages/types/subject_configuration_component.rb b/app/components/work_packages/types/subject_configuration_component.rb deleted file mode 100644 index 7d2ecbb53fe..00000000000 --- a/app/components/work_packages/types/subject_configuration_component.rb +++ /dev/null @@ -1,112 +0,0 @@ -# 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 WorkPackages - module Types - class SubjectConfigurationComponent < ApplicationComponent - include ApplicationHelper - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable - - def initialize(model, subject_configuration_form_data: nil, **) - @subject_configuration_form_data = subject_configuration_form_data - super(model, **) - end - - def form_options - form_model = subject_form_object - - { - url: subject_configuration_type_path(id: model.id), - method: :put, - model: form_model, - data: { - controller: "admin--subject-configuration", - admin__subject_configuration_hide_pattern_input_value: form_model.subject_configuration == :manual - } - } - end - - private - - def subject_form_object - values = subject_configuration_form_values - - ::Types::Forms::SubjectConfigurationFormModel.new( - subject_configuration: values[:subject_configuration], - pattern: values[:pattern], - suggestions: sort_attributes(supported_attributes), - validation_errors: model.errors - ) - end - - def supported_attributes - attribute_tokens = ::Types::Patterns::TokenPropertyMapper.new.tokens_for_type(model) - - result = { - work_package: { - title: I18n.t("types.edit.subject_configuration.token.context.work_package"), - tokens: [] - }, - parent: { - title: I18n.t("types.edit.subject_configuration.token.context.parent"), - tokens: [] - }, - project: { - title: I18n.t("types.edit.subject_configuration.token.context.project"), - tokens: [] - } - } - - attribute_tokens.each_with_object(result) do |token, obj| - token_hash = { key: token.key, label: token.label, label_with_context: token.label_with_context } - obj[token.context][:tokens] << token_hash - end - end - - def sort_attributes(attributes) - attributes.each_value { |group| group[:tokens] = group[:tokens].sort_by { |a| a[:label] } } - attributes - end - - def subject_configuration_form_values - if @subject_configuration_form_data.present? - @subject_configuration_form_data - else - persisted_subject_pattern = model.patterns.subject || ::Types::Pattern.new(blueprint: "", enabled: false) - subject_configuration = persisted_subject_pattern.enabled ? :generated : :manual - pattern = persisted_subject_pattern.blueprint - - { subject_configuration:, pattern: } - end - end - end - end -end diff --git a/app/contracts/attachments/create_contract.rb b/app/contracts/attachments/create_contract.rb index 2ec0d41d3fe..a3be099f6bc 100644 --- a/app/contracts/attachments/create_contract.rb +++ b/app/contracts/attachments/create_contract.rb @@ -68,29 +68,29 @@ module Attachments end ## - # Validates the content type, if a whitelist is set + # Validates the content type, if a allowlist is set def validate_content_type - # If the whitelist is empty, assume all files are allowed + # If the allowlist is empty, assume all files are allowed # as before - unless matches_whitelist?(attachment_whitelist) - Rails.logger.info { "Uploaded file #{model.filename} with type #{model.content_type} does not match whitelist" } - errors.add :content_type, :not_whitelisted, value: model.content_type + unless matches_allowlist?(attachment_allowlist) + Rails.logger.info { "Uploaded file #{model.filename} with type #{model.content_type} does not match allowlist" } + errors.add :content_type, :not_allowlisted, value: model.content_type end end ## - # Get the user-defined whitelist or a custom whitelist + # Get the user-defined allowlist or a custom allowlist # defined for this invocation - def attachment_whitelist - Array(options.fetch(:whitelist, Setting.attachment_whitelist)) + def attachment_allowlist + Array(options.fetch(:allowlist, Setting.attachment_whitelist)) end ## - # Returns whether the attachment matches the whitelist - def matches_whitelist?(whitelist) - return true if whitelist.empty? + # Returns whether the attachment matches the allowlist + def matches_allowlist?(allowlist) + return true if allowlist.empty? - whitelist.include?(model.content_type) || whitelist.include?("*#{model.extension}") + allowlist.include?(model.content_type) || allowlist.include?("*#{model.extension}") end end end diff --git a/app/contracts/shares/base_contract.rb b/app/contracts/shares/base_contract.rb index 5bd00481f50..6e5742d807c 100644 --- a/app/contracts/shares/base_contract.rb +++ b/app/contracts/shares/base_contract.rb @@ -54,7 +54,7 @@ module Shares end def role_grantable - errors.add(:roles, :ungrantable) unless active_roles.all? { _1.is_a?(assignable_role_class) } + errors.add(:roles, :ungrantable) unless active_roles.all? { it.is_a?(assignable_role_class) } end def active_roles diff --git a/app/contracts/types/base_contract.rb b/app/contracts/types/base_contract.rb index 8a1920e93e0..af2fbd1b02e 100644 --- a/app/contracts/types/base_contract.rb +++ b/app/contracts/types/base_contract.rb @@ -112,9 +112,9 @@ module Types return if blueprint.nil? valid_tokens = flat_valid_token_list - invalid_tokens = blueprint.scan(PatternResolver::TOKEN_REGEX) + invalid_tokens = blueprint.scan(::WorkPackageTypes::PatternResolver::TOKEN_REGEX) .reduce([]) do |acc, match| - token = Patterns::PatternToken.build(match).key + token = ::WorkPackageTypes::Patterns::PatternToken.build(match).key valid_tokens.include?(token) ? acc : acc << token end @@ -123,6 +123,6 @@ module Types end end - def flat_valid_token_list = Patterns::TokenPropertyMapper.new.tokens_for_type(model).map(&:key) + def flat_valid_token_list = ::WorkPackageTypes::Patterns::TokenPropertyMapper.new.tokens_for_type(model).map(&:key) end end diff --git a/app/contracts/work_package_types/copy_workflow_attribute_contract.rb b/app/contracts/work_package_types/copy_workflow_attribute_contract.rb new file mode 100644 index 00000000000..515d7e1fa04 --- /dev/null +++ b/app/contracts/work_package_types/copy_workflow_attribute_contract.rb @@ -0,0 +1,51 @@ +# 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 WorkPackageTypes + class CopyWorkflowAttributeContract < Dry::Validation::Contract + config.messages.backend = :i18n + + params do + optional(:copy_workflow_from).filled(:string) + end + + rule(:copy_workflow_from) do + next unless key? + + type = Type.find_by(id: value) + if type.nil? + key.failure(:not_found) + next + end + + key.failure(:workflow_missing) if type.workflows.empty? + end + end +end diff --git a/app/contracts/work_package_types/update_form_configuration_contract.rb b/app/contracts/work_package_types/update_form_configuration_contract.rb new file mode 100644 index 00000000000..021e8538e5b --- /dev/null +++ b/app/contracts/work_package_types/update_form_configuration_contract.rb @@ -0,0 +1,87 @@ +# 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 WorkPackageTypes + class UpdateFormConfigurationContract < BaseContract + attribute :attribute_groups + + validate :validate_attribute_group_names + validate :validate_attribute_groups + + private + + def validate_attribute_group_names + return unless model.attribute_groups_changed? + + seen = Set.new + model.attribute_groups.each do |group| + errors.add(:attribute_groups, :group_without_name) if group.key.blank? + errors.add(:attribute_groups, :duplicate_group, group: group.key) if seen.add?(group.key).nil? + end + end + + def validate_attribute_groups + return unless model.attribute_groups_changed? + + model.attribute_groups_objects.each do |group| + if group.is_a?(Type::QueryGroup) + validate_query_group(group) + else + validate_attribute_group(group) + end + end + end + + def validate_query_group(group) + query = group.query + + contract_class = query.persisted? ? Queries::UpdateContract : Queries::CreateContract + contract = contract_class.new(query, user) + + unless contract.validate + errors.add(:attribute_groups, :query_invalid, group: group.key, details: contract.errors.full_messages.join) + end + end + + def validate_attribute_group(group) + valid_attributes = model.work_package_attributes.keys + + group.attributes.each do |key| + if key.is_a?(String) && valid_attributes.exclude?(key) + errors.add( + :attribute_groups, + I18n.t("activerecord.errors.models.type.attributes.attribute_groups.attribute_unknown_name", + attribute: key) + ) + end + end + end + end +end diff --git a/app/contracts/work_package_types/update_projects_contract.rb b/app/contracts/work_package_types/update_projects_contract.rb new file mode 100644 index 00000000000..8af223f7ecb --- /dev/null +++ b/app/contracts/work_package_types/update_projects_contract.rb @@ -0,0 +1,35 @@ +# 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 WorkPackageTypes + class UpdateProjectsContract < BaseContract + attribute :projects + end +end diff --git a/app/contracts/work_package_types/update_subject_pattern_contract.rb b/app/contracts/work_package_types/update_subject_pattern_contract.rb index 521fc240957..00d7a6a6fb5 100644 --- a/app/contracts/work_package_types/update_subject_pattern_contract.rb +++ b/app/contracts/work_package_types/update_subject_pattern_contract.rb @@ -41,9 +41,9 @@ module WorkPackageTypes return if blueprint.nil? valid_tokens = flat_valid_token_list - invalid_tokens = blueprint.scan(Types::PatternResolver::TOKEN_REGEX) + invalid_tokens = blueprint.scan(WorkPackageTypes::PatternResolver::TOKEN_REGEX) .reduce([]) do |acc, match| - token = Types::Patterns::PatternToken.build(match).key + token = WorkPackageTypes::Patterns::PatternToken.build(match).key valid_tokens.include?(token) ? acc : acc << token end @@ -52,6 +52,6 @@ module WorkPackageTypes end end - def flat_valid_token_list = Types::Patterns::TokenPropertyMapper.new.tokens_for_type(model).map(&:key) + def flat_valid_token_list = WorkPackageTypes::Patterns::TokenPropertyMapper.new.tokens_for_type(model).map(&:key) end end diff --git a/app/contracts/work_packages/base_contract.rb b/app/contracts/work_packages/base_contract.rb index 819fccea612..ff86b68d0be 100644 --- a/app/contracts/work_packages/base_contract.rb +++ b/app/contracts/work_packages/base_contract.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -286,6 +288,7 @@ module WorkPackages def validate_parent_not_self if model.parent == model + errors.delete(:parent_id) # remove the error added by closure_tree's cycle detection errors.add :parent, :cannot_be_self_assigned end end @@ -301,11 +304,22 @@ module WorkPackages if model.parent_id_changed? && model.parent_id && errors.exclude?(:parent) && - WorkPackage.relatable(model, Relation::TYPE_PARENT).where(id: model.parent_id).empty? + current_parent_unrelatable? + # closure_tree adds an error on :parent_id because of the cycle + # detection, and active_record sees the error when saving the children + # association and adds an error on :children as well. We need to remove + # them. + errors.delete(:parent_id) # remove the error added by closure_tree + errors.delete(:children) # remove the error added by active_record + # add our own error errors.add :parent, :cant_link_a_work_package_with_a_descendant end end + def current_parent_unrelatable? + WorkPackage.relatable(model, Relation::TYPE_PARENT).where(id: model.parent_id).empty? + end + def validate_status_exists errors.add :status, :does_not_exist if model.status && !status_exists? end diff --git a/app/controllers/admin/settings/project_custom_field_sections_controller.rb b/app/controllers/admin/settings/project_custom_field_sections_controller.rb index 35646229f79..5865f99dbf3 100644 --- a/app/controllers/admin/settings/project_custom_field_sections_controller.rb +++ b/app/controllers/admin/settings/project_custom_field_sections_controller.rb @@ -40,7 +40,7 @@ module Admin::Settings ) if call.success? - update_header_via_turbo_stream # required to closed the dialog + update_header_via_turbo_stream(allow_custom_field_creation: allow_custom_field_creation?) update_sections_via_turbo_stream(project_custom_field_sections: ProjectCustomFieldSection.all) else update_section_dialog_body_form_via_turbo_stream(project_custom_field_section: call.result) @@ -67,6 +67,7 @@ module Admin::Settings call = ::ProjectCustomFieldSections::DeleteService.new(user: current_user, model: @project_custom_field_section).call if call.success? + update_header_via_turbo_stream(allow_custom_field_creation: allow_custom_field_creation?) update_sections_via_turbo_stream(project_custom_field_sections: ProjectCustomFieldSection.all) else # TODO: show error message @@ -95,6 +96,7 @@ module Admin::Settings ) if call.success? + update_header_via_turbo_stream(allow_custom_field_creation: allow_custom_field_creation?) update_sections_via_turbo_stream(project_custom_field_sections: ProjectCustomFieldSection.all) else # TODO: show error message @@ -112,6 +114,10 @@ module Admin::Settings @project_custom_field_section = ProjectCustomFieldSection.find(params[:id]) end + def allow_custom_field_creation? + ProjectCustomFieldSection.any? + end + def project_custom_field_section_params params.require(:project_custom_field_section).permit(:name) end diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index a3e2330eb93..a841c4110b7 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -48,6 +48,8 @@ module Admin::Settings # rubocop:enable Rails/LexicallyScopedActionFilter def index + @allow_custom_field_creation = @project_custom_field_sections.any? + respond_to :html end diff --git a/app/controllers/concerns/admin/settings/project_custom_fields/component_streams.rb b/app/controllers/concerns/admin/settings/project_custom_fields/component_streams.rb index b36b9b49c9e..37950b649e0 100644 --- a/app/controllers/concerns/admin/settings/project_custom_fields/component_streams.rb +++ b/app/controllers/concerns/admin/settings/project_custom_fields/component_streams.rb @@ -33,9 +33,11 @@ module Admin extend ActiveSupport::Concern included do - def update_header_via_turbo_stream + def update_header_via_turbo_stream(allow_custom_field_creation:) update_via_turbo_stream( - component: ::Settings::ProjectCustomFields::HeaderComponent.new + component: ::Settings::ProjectCustomFields::HeaderComponent.new( + allow_custom_field_creation: + ) ) end diff --git a/app/controllers/types_controller.rb b/app/controllers/types_controller.rb index 5480472dae7..315d65c75c4 100644 --- a/app/controllers/types_controller.rb +++ b/app/controllers/types_controller.rb @@ -59,7 +59,11 @@ class TypesController < ApplicationController end def create - additional_params = { copy_workflow_from: params.dig(:type, :copy_workflow_from) } + additional_params = {} + if (value = params.dig(:type, :copy_workflow_from)) + additional_params[:copy_workflow_from] = value + end + service_call = WorkPackageTypes::CreateService .new(user: current_user) .call(permitted_type_params.merge(additional_params)) diff --git a/app/controllers/work_package_types/pdf_export_template_controller.rb b/app/controllers/work_package_types/pdf_export_template_controller.rb new file mode 100644 index 00000000000..f4870cff90f --- /dev/null +++ b/app/controllers/work_package_types/pdf_export_template_controller.rb @@ -0,0 +1,91 @@ +# 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 WorkPackageTypes + class PdfExportTemplateController < ApplicationController + include OpTurbo::ComponentStream + before_action :require_admin + before_action :find_type, only: %i[toggle drop enable_all disable_all] + before_action :find_template, only: %i[toggle drop] + + def enable_all + return render_404_turbo_stream if @type.nil? + + @type.pdf_export_templates.enable_all + @type.save! + respond_section_with_turbo_streams + end + + def disable_all + return render_404_turbo_stream if @type.nil? + + @type.pdf_export_templates.disable_all + @type.save! + respond_section_with_turbo_streams + end + + def toggle + return render_404_turbo_stream if @template.nil? + + @type.pdf_export_templates.toggle(@template.id) + @type.save! + respond_with_turbo_streams + end + + def drop + return render_404_turbo_stream if @template.nil? + + @type.pdf_export_templates.move(@template.id, params[:position].to_i - 1) # drop index starts at 1 + @type.save! + respond_to_with_turbo_streams + end + + protected + + def respond_section_with_turbo_streams + replace_via_turbo_stream( + component: ::WorkPackageTypes::ExportTemplateListComponent.new(type: @type) + ) + respond_to_with_turbo_streams + end + + def render_404_turbo_stream + render_error_flash_message_via_turbo_stream(message: t(:notice_file_not_found)) + end + + def find_type + @type = ::Type.find(params[:type_id]) + end + + def find_template + @template = @type.pdf_export_templates.find(params[:id]) + end + end +end diff --git a/app/controllers/work_package_types/projects_tab_controller.rb b/app/controllers/work_package_types/projects_tab_controller.rb new file mode 100644 index 00000000000..510badf78e6 --- /dev/null +++ b/app/controllers/work_package_types/projects_tab_controller.rb @@ -0,0 +1,78 @@ +# 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 WorkPackageTypes + class ProjectsTabController < ApplicationController + layout "admin" + + before_action :require_admin + before_action :find_type + before_action :load_projects, only: :edit + + current_menu_item [:edit] do + :types + end + + def edit; end + + def update + result = UpdateService.new(user: current_user, model: @type, contract_class: UpdateProjectsContract) + .call(permitted_project_params) + + if result.success? + redirect_to edit_type_projects_path(type_id: @type.id), notice: I18n.t(:notice_successful_update) + else + flash_error(result) + load_projects + render :edit, status: :unprocessable_entity + end + end + + private + + def flash_error(result) + flash.now[:error] = result.errors.messages_for(:project_ids).to_sentence + end + + def load_projects + @projects = Project.all + end + + def find_type + @type = ::Type.find(params[:type_id]) + end + + def permitted_project_params + # TODO: once the input is correctly delivered just return: params.expect(type: [:project_ids]) + + { project_ids: JSON.parse(params.expect(type: [:project_ids])[:project_ids]) } + end + end +end diff --git a/app/controllers/work_package_types/subject_configuration_tab_controller.rb b/app/controllers/work_package_types/subject_configuration_tab_controller.rb new file mode 100644 index 00000000000..c2c269f82e4 --- /dev/null +++ b/app/controllers/work_package_types/subject_configuration_tab_controller.rb @@ -0,0 +1,91 @@ +# 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 WorkPackageTypes + class SubjectConfigurationTabController < ApplicationController + layout "admin" + + before_action :require_admin + before_action :find_type, only: %i[update_subject_configuration] + + def update_subject_configuration + form_params = params.expect( + work_package_types_forms_subject_configuration_form_model: %i[subject_configuration pattern] + ).to_h + + UpdateTypeService.new(@type, current_user) + .call({ patterns: pattern_collection_update(form_params) }) do |call| + call.on_success do + redirect_to tab_path, notice: I18n.t(:notice_successful_update) + end + + call.on_failure do + params[:tab] = "subject_configuration" + render template: "types/edit", status: :unprocessable_entity + end + end + end + + private + + def find_type + @type = ::Type.find(params[:id]) + end + + def tab_path = edit_tab_type_path(id: @type.id, tab: :subject_configuration) + + def pattern_collection_update(form_params) + patterns = @type.patterns.to_h.symbolize_keys + + subject_pattern = + case form_params + in { subject_configuration: "generated", pattern: String => blueprint } + { subject: { blueprint:, enabled: true } } + in { subject_configuration: "manual", pattern: String => blueprint } + if blueprint.empty? + # Submitting the form with an empty blueprint and manual subject configuration will + # remove the subject pattern from the collection + nil + else + { subject: { blueprint:, enabled: false } } + end + else + nil + end + + if subject_pattern.nil? + patterns.delete(:subject) + patterns + else + patterns.merge(subject_pattern) + end + end + end +end diff --git a/app/controllers/work_packages/activities_tab_controller.rb b/app/controllers/work_packages/activities_tab_controller.rb index ea6b2a35321..5a37b3f36b6 100644 --- a/app/controllers/work_packages/activities_tab_controller.rb +++ b/app/controllers/work_packages/activities_tab_controller.rb @@ -111,7 +111,6 @@ class WorkPackages::ActivitiesTabController < ApplicationController call = create_journal_service_call if call.success? && call.result - claim_journal_attachments_for(call.result) set_last_server_timestamp_to_headers handle_successful_create_call(call) else @@ -129,7 +128,6 @@ class WorkPackages::ActivitiesTabController < ApplicationController call = update_journal_service_call if call.success? && call.result - claim_journal_attachments_for(call.result) update_item_show_component(journal: call.result, grouped_emoji_reactions: grouped_emoji_reactions_for_journal) else handle_failed_create_or_update_call(call) @@ -324,12 +322,6 @@ class WorkPackages::ActivitiesTabController < ApplicationController Journals::UpdateService.new(model: @journal, user: User.current).call(notes:) end - def claim_journal_attachments_for(journal) - WorkPackages::ActivitiesTab::CommentAttachmentsClaims::ClaimsService - .new(user: User.current, model: journal) - .call - end - def generate_time_based_update_streams(last_update_timestamp) journals = @work_package .journals diff --git a/app/controllers/work_packages/moves_controller.rb b/app/controllers/work_packages/moves_controller.rb index ef2141b28a5..862e596aa3f 100644 --- a/app/controllers/work_packages/moves_controller.rb +++ b/app/controllers/work_packages/moves_controller.rb @@ -126,7 +126,7 @@ class WorkPackages::MovesController < ApplicationController hierarchies = WorkPackageHierarchy .includes(:ancestor) .where(ancestor_id: @work_packages.select(:id)) - Type.where(id: hierarchies.map { _1.ancestor.type_id }) + Type.where(id: hierarchies.map { it.ancestor.type_id }) .select("distinct id") .pluck(:id) .difference(@types.pluck(:id)) diff --git a/app/controllers/work_packages/progress_controller.rb b/app/controllers/work_packages/progress_controller.rb index 7a4b702b254..676fd79e928 100644 --- a/app/controllers/work_packages/progress_controller.rb +++ b/app/controllers/work_packages/progress_controller.rb @@ -142,7 +142,7 @@ class WorkPackages::ProgressController < ApplicationController "remaining_hours_touched", "done_ratio_touched", "status_id_touched") - .transform_values { _1 == "true" } + .transform_values { it == "true" } .permit! end @@ -153,7 +153,7 @@ class WorkPackages::ProgressController < ApplicationController end def allowed_touched_params - allowed_params.filter { touched?(_1) } + allowed_params.filter { touched?(it) } end def allowed_params diff --git a/app/controllers/work_packages/reminders_controller.rb b/app/controllers/work_packages/reminders_controller.rb index d47d0e9bedf..8289236e51a 100644 --- a/app/controllers/work_packages/reminders_controller.rb +++ b/app/controllers/work_packages/reminders_controller.rb @@ -109,7 +109,8 @@ class WorkPackages::RemindersController < ApplicationController def reminder_chosen_time(reminder) OpPrimer::RelativeTimeComponent.new( - datetime: in_user_zone(reminder.remind_at) + datetime: in_user_zone(reminder.remind_at), + month: :long ).render_in(view_context) end diff --git a/app/controllers/work_packages/types/pdf_export_template_controller.rb b/app/controllers/work_packages/types/pdf_export_template_controller.rb deleted file mode 100644 index f39133f8456..00000000000 --- a/app/controllers/work_packages/types/pdf_export_template_controller.rb +++ /dev/null @@ -1,93 +0,0 @@ -# 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 WorkPackages - module Types - class PdfExportTemplateController < ApplicationController - include OpTurbo::ComponentStream - before_action :require_admin - before_action :find_type, only: %i[toggle drop enable_all disable_all] - before_action :find_template, only: %i[toggle drop] - - def enable_all - return render_404_turbo_stream if @type.nil? - - @type.pdf_export_templates.enable_all - @type.save! - respond_section_with_turbo_streams - end - - def disable_all - return render_404_turbo_stream if @type.nil? - - @type.pdf_export_templates.disable_all - @type.save! - respond_section_with_turbo_streams - end - - def toggle - return render_404_turbo_stream if @template.nil? - - @type.pdf_export_templates.toggle(@template.id) - @type.save! - respond_with_turbo_streams - end - - def drop - return render_404_turbo_stream if @template.nil? - - @type.pdf_export_templates.move(@template.id, params[:position].to_i - 1) # drop index starts at 1 - @type.save! - respond_to_with_turbo_streams - end - - protected - - def respond_section_with_turbo_streams - replace_via_turbo_stream( - component: ::WorkPackages::Types::ExportTemplateListComponent.new(type: @type) - ) - respond_to_with_turbo_streams - end - - def render_404_turbo_stream - render_error_flash_message_via_turbo_stream(message: t(:notice_file_not_found)) - end - - def find_type - @type = ::Type.find(params[:type_id]) - end - - def find_template - @template = @type.pdf_export_templates.find(params[:id]) - end - end - end -end diff --git a/app/controllers/work_packages/types/subject_configuration_tab_controller.rb b/app/controllers/work_packages/types/subject_configuration_tab_controller.rb deleted file mode 100644 index f39837b1c27..00000000000 --- a/app/controllers/work_packages/types/subject_configuration_tab_controller.rb +++ /dev/null @@ -1,93 +0,0 @@ -# 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 WorkPackages - module Types - class SubjectConfigurationTabController < ApplicationController - layout "admin" - - before_action :require_admin - before_action :find_type, only: %i[update_subject_configuration] - - def update_subject_configuration - form_params = params.require(:types_forms_subject_configuration_form_model) - .permit(:subject_configuration, :pattern) - .to_h - - UpdateTypeService.new(@type, current_user) - .call({ patterns: pattern_collection_update(form_params) }) do |call| - call.on_success do - redirect_to tab_path, notice: I18n.t(:notice_successful_update) - end - - call.on_failure do - params[:tab] = "subject_configuration" - render template: "types/edit", status: :unprocessable_entity - end - end - end - - private - - def find_type - @type = ::Type.find(params[:id]) - end - - def tab_path = edit_tab_type_path(id: @type.id, tab: :subject_configuration) - - def pattern_collection_update(form_params) - patterns = @type.patterns.to_h.symbolize_keys - - subject_pattern = - case form_params - in { subject_configuration: "generated", pattern: String => blueprint } - { subject: { blueprint:, enabled: true } } - in { subject_configuration: "manual", pattern: String => blueprint } - if blueprint.empty? - # Submitting the form with an empty blueprint and manual subject configuration will - # remove the subject pattern from the collection - nil - else - { subject: { blueprint:, enabled: false } } - end - else - nil - end - - if subject_pattern.nil? - patterns.delete(:subject) - patterns - else - patterns.merge(subject_pattern) - end - end - end - end -end diff --git a/app/forms/work_package_types/settings_form.rb b/app/forms/work_package_types/settings_form.rb new file mode 100644 index 00000000000..7ae69313334 --- /dev/null +++ b/app/forms/work_package_types/settings_form.rb @@ -0,0 +1,117 @@ +# 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 WorkPackageTypes + class SettingsForm < ApplicationForm + form do |settings_form| + settings_form.text_field( + name: :name, + label: label(:name), + placeholder: I18n.t(:label_name), + required: true, + disabled: model.is_standard? + ) + + settings_form.color_select_list( + name: :color_id, + label: Color.model_name.human, + input_width: :medium, + caption: I18n.t("types.edit.settings.type_color_text") + ) + + if show_work_flow_copy? + settings_form.select_list( + name: :copy_workflow_from, + input_width: :medium, + label: I18n.t(:label_copy_workflow_from), + include_blank: true, + validation_message: validation_message_for(:copy_workflow_from) + ) do |other_types| + work_package_types.each do |type| + other_types.option( + value: type.id, + label: type.name, + selected: type.id == prefilled_copy_workflow_from + ) + end + end + end + + settings_form.rich_text_area( + name: :description, + label: label(:description), + rich_text_options: { showAttachments: false } + ) + + settings_form.check_box( + name: :is_milestone, + label: label(:is_milestone) + ) + + settings_form.check_box( + name: :is_in_roadmap, + label: label(:is_in_roadmap) + ) + + settings_form.check_box( + name: :is_default, + label: label(:is_default) + ) + + settings_form.submit( + name: :submit, + label: I18n.t(:button_save), + scheme: :primary + ) + end + + private + + def label(attribute) + model.class.human_attribute_name(attribute) + end + + def show_work_flow_copy? + model.new_record? + end + + def work_package_types + Type.all + end + + def validation_message_for(attribute) + model.errors.messages_for(attribute).to_sentence.presence + end + + def prefilled_copy_workflow_from + @builder.options[:copy_workflow_from] + end + end +end diff --git a/app/forms/work_package_types/subject_configuration_form.rb b/app/forms/work_package_types/subject_configuration_form.rb new file mode 100644 index 00000000000..046c0d39524 --- /dev/null +++ b/app/forms/work_package_types/subject_configuration_form.rb @@ -0,0 +1,99 @@ +# 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 WorkPackageTypes + class SubjectConfigurationForm < ApplicationForm + form do |subject_form| + subject_form.radio_button_group(name: :subject_configuration) do |group| + group.radio_button( + value: :manual, + checked: subject_configuration_manual?, + label: I18n.t("types.edit.subject_configuration.manually_editable_subjects.label"), + caption: I18n.t("types.edit.subject_configuration.manually_editable_subjects.caption"), + data: { action: "admin--subject-configuration#hidePatternInput" } + ) + group.radio_button( + value: :generated, + checked: !subject_configuration_manual?, + label: I18n.t("types.edit.subject_configuration.automatically_generated_subjects.label"), + caption: I18n.t("types.edit.subject_configuration.automatically_generated_subjects.caption"), + disabled: !enterprise? && subject_configuration_manual?, + data: { action: "admin--subject-configuration#showPatternInput" } + ) + end + + subject_form.group(data: { "admin--subject-configuration-target": "patternInput" }) do |toggleable_group| + toggleable_group.pattern_input( + name: :pattern, + value: model.pattern, + suggestions: model.suggestions, + label: I18n.t("types.edit.subject_configuration.pattern.label"), + caption: pattern_input_caption, + required: true, + validation_message: validation_message_for(:patterns) + ) + end + + subject_form.submit( + name: :submit, + label: I18n.t(:button_save), + scheme: :primary + ) + end + + private + + def subject_configuration_manual? + model.subject_configuration == :manual + end + + def enterprise? + EnterpriseToken.allows_to?(:work_package_subject_generation) + end + + def validation_message_for(attribute) + model.validation_errors.messages_for(attribute).to_sentence.presence + end + + def pattern_input_caption + I18n.t( + "types.edit.subject_configuration.pattern.caption_with_supported_attributes_link", + link: make_link( + ::OpenProject::Static::Links.url_for(:enterprise_features, :work_package_subject_generation), + I18n.t("types.edit.subject_configuration.pattern.supported_attributes_link") + ) + ).html_safe + end + + def make_link(href, link_text) + render(Primer::Beta::Link.new(href:, target: "_blank")) { link_text } + end + end +end diff --git a/app/forms/work_packages/progress_form.rb b/app/forms/work_packages/progress_form.rb index 3a7ab982802..f90b5064afb 100644 --- a/app/forms/work_packages/progress_form.rb +++ b/app/forms/work_packages/progress_form.rb @@ -173,7 +173,7 @@ class WorkPackages::ProgressForm < ApplicationForm def field_value(name) errors = @work_package.errors.where(name) - if (user_value = errors.map { |error| error.options[:value] }.find { !_1.nil? }) + if (user_value = errors.map { |error| error.options[:value] }.find { !it.nil? }) user_value elsif name == :done_ratio as_percent(@work_package.public_send(name)) diff --git a/app/forms/work_packages/types/settings_form.rb b/app/forms/work_packages/types/settings_form.rb deleted file mode 100644 index c7ab04ee73f..00000000000 --- a/app/forms/work_packages/types/settings_form.rb +++ /dev/null @@ -1,114 +0,0 @@ -# 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 WorkPackages - module Types - class SettingsForm < ApplicationForm - form do |settings_form| - settings_form.text_field( - name: :name, - label: label(:name), - placeholder: I18n.t(:label_name), - required: true, - disabled: model.is_standard? - ) - - settings_form.color_select_list( - name: :color_id, - label: Color.model_name.human, - input_width: :medium, - caption: I18n.t("types.edit.settings.type_color_text") - ) - - if show_work_flow_copy? - settings_form.select_list( - name: :copy_workflow_from, - input_width: :medium, - label: I18n.t(:label_copy_workflow_from), - include_blank: true - ) do |other_types| - work_package_types.each do |type| - other_types.option( - value: type.id, - label: type.name, - selected: type.id == prefilled_copy_workflow_from - ) - end - end - end - - settings_form.rich_text_area( - name: :description, - label: label(:description), - rich_text_options: { showAttachments: false } - ) - - settings_form.check_box( - name: :is_milestone, - label: label(:is_milestone) - ) - - settings_form.check_box( - name: :is_in_roadmap, - label: label(:is_in_roadmap) - ) - - settings_form.check_box( - name: :is_default, - label: label(:is_default) - ) - - settings_form.submit( - name: :submit, - label: I18n.t(:button_save), - scheme: :primary - ) - end - - private - - def label(attribute) - model.class.human_attribute_name(attribute) - end - - def show_work_flow_copy? - model.new_record? - end - - def work_package_types - Type.all - end - - def prefilled_copy_workflow_from - @builder.options[:copy_workflow_from] - end - end - end -end diff --git a/app/forms/work_packages/types/subject_configuration_form.rb b/app/forms/work_packages/types/subject_configuration_form.rb deleted file mode 100644 index 9a6897c98e6..00000000000 --- a/app/forms/work_packages/types/subject_configuration_form.rb +++ /dev/null @@ -1,101 +0,0 @@ -# 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 WorkPackages - module Types - class SubjectConfigurationForm < ApplicationForm - form do |subject_form| - subject_form.radio_button_group(name: :subject_configuration) do |group| - group.radio_button( - value: :manual, - checked: subject_configuration_manual?, - label: I18n.t("types.edit.subject_configuration.manually_editable_subjects.label"), - caption: I18n.t("types.edit.subject_configuration.manually_editable_subjects.caption"), - data: { action: "admin--subject-configuration#hidePatternInput" } - ) - group.radio_button( - value: :generated, - checked: !subject_configuration_manual?, - label: I18n.t("types.edit.subject_configuration.automatically_generated_subjects.label"), - caption: I18n.t("types.edit.subject_configuration.automatically_generated_subjects.caption"), - disabled: !enterprise? && subject_configuration_manual?, - data: { action: "admin--subject-configuration#showPatternInput" } - ) - end - - subject_form.group(data: { "admin--subject-configuration-target": "patternInput" }) do |toggleable_group| - toggleable_group.pattern_input( - name: :pattern, - value: model.pattern, - suggestions: model.suggestions, - label: I18n.t("types.edit.subject_configuration.pattern.label"), - caption: pattern_input_caption, - required: true, - validation_message: validation_message_for(:patterns) - ) - end - - subject_form.submit( - name: :submit, - label: I18n.t(:button_save), - scheme: :primary - ) - end - - private - - def subject_configuration_manual? - model.subject_configuration == :manual - end - - def enterprise? - EnterpriseToken.allows_to?(:work_package_subject_generation) - end - - def validation_message_for(attribute) - model.validation_errors.messages_for(attribute).to_sentence.presence - end - - def pattern_input_caption - I18n.t( - "types.edit.subject_configuration.pattern.caption_with_supported_attributes_link", - link: make_link( - ::OpenProject::Static::Links.url_for(:enterprise_features, :work_package_subject_generation), - I18n.t("types.edit.subject_configuration.pattern.supported_attributes_link") - ) - ).html_safe - end - - def make_link(href, link_text) - render(Primer::Beta::Link.new(href:, target: "_blank")) { link_text } - end - end - end -end diff --git a/app/helpers/breadcrumb_helper.rb b/app/helpers/breadcrumb_helper.rb index 5e61831cb0e..c4098190806 100644 --- a/app/helpers/breadcrumb_helper.rb +++ b/app/helpers/breadcrumb_helper.rb @@ -30,7 +30,7 @@ module BreadcrumbHelper def nested_breadcrumb_element(section_header, title) output = "".html_safe output << "#{section_header}: " - output << content_tag(:strong, title) + output << content_tag(:b, title) output end diff --git a/app/helpers/types_helper.rb b/app/helpers/types_helper.rb index 16513a1a6bb..5ab45d2d8ee 100644 --- a/app/helpers/types_helper.rb +++ b/app/helpers/types_helper.rb @@ -36,7 +36,7 @@ module ::TypesHelper name: "settings", path: edit_tab_type_path(id: @type.id, tab: :settings), label: I18n.t("types.edit.settings.tab"), - view_component: WorkPackages::Types::SettingsComponent + view_component: WorkPackageTypes::SettingsComponent }, { name: "form_configuration", @@ -48,20 +48,20 @@ module ::TypesHelper name: "subject_configuration", path: edit_tab_type_path(id: @type.id, tab: :subject_configuration), label: I18n.t("types.edit.subject_configuration.tab"), - view_component: WorkPackages::Types::SubjectConfigurationComponent, + view_component: WorkPackageTypes::SubjectConfigurationComponent, enterprise_feature: :work_package_subject_generation }, { name: "projects", - partial: "types/form/projects", - path: edit_tab_type_path(id: @type.id, tab: :projects), + path: edit_type_projects_path(type_id: @type.id), + view_component: WorkPackageTypes::ProjectsComponent, label: I18n.t("types.edit.projects.tab") }, { name: "export_configuration", path: edit_tab_type_path(id: @type.id, tab: :export_configuration), label: I18n.t("types.edit.export_configuration.tab"), - view_component: WorkPackages::Types::ExportConfigurationComponent + view_component: WorkPackageTypes::ExportConfigurationComponent } ] end diff --git a/app/models/attribute_help_text.rb b/app/models/attribute_help_text.rb index fb0f2f30aa2..f9797bfa4d3 100644 --- a/app/models/attribute_help_text.rb +++ b/app/models/attribute_help_text.rb @@ -30,7 +30,9 @@ class AttributeHelpText < ApplicationRecord acts_as_attachable viewable_by_all_users: true def self.cached(user) - OpenProject::Cache.fetch([name, user]) { visible(user).select(:id, :attribute_name).index_by(&:attribute_name) } + RequestStore.fetch(name) do + visible(user).select(:id, :attribute_name).index_by(&:attribute_name) + end end def self.for(model) diff --git a/app/models/custom_actions/actions/base.rb b/app/models/custom_actions/actions/base.rb index 4995d3c9371..c499282473d 100644 --- a/app/models/custom_actions/actions/base.rb +++ b/app/models/custom_actions/actions/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -75,9 +77,7 @@ class CustomActions::Actions::Base end end - def key - self.class.key - end + delegate :key, to: :class def required? false @@ -98,6 +98,10 @@ class CustomActions::Actions::Base private + def deconstruct_keys(*) + { type:, custom_field_based: respond_to?(:custom_field) } + end + def validate_value_required(errors) if required? && values.empty? errors.add :actions, diff --git a/app/models/custom_value/date_strategy.rb b/app/models/custom_value/date_strategy.rb index d355b0187db..523aab51d84 100644 --- a/app/models/custom_value/date_strategy.rb +++ b/app/models/custom_value/date_strategy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -30,9 +32,11 @@ class CustomValue::DateStrategy < CustomValue::FormatStrategy include Redmine::I18n def typed_value - if value.present? - Date.iso8601(value) - end + return if value.blank? + + Date.iso8601(value) + rescue Date::Error + nil end def formatted_value diff --git a/app/models/custom_value/link_strategy.rb b/app/models/custom_value/link_strategy.rb index 091c01de5d6..4c552e06e49 100644 --- a/app/models/custom_value/link_strategy.rb +++ b/app/models/custom_value/link_strategy.rb @@ -45,5 +45,7 @@ class CustomValue::LinkStrategy < CustomValue::FormatStrategy def parsed_url(val) Addressable::URI.heuristic_parse(val, scheme: "http") + rescue Addressable::URI::InvalidURIError + nil end end diff --git a/app/models/enterprise_token.rb b/app/models/enterprise_token.rb index b6a159fdbe4..809cbc579ab 100644 --- a/app/models/enterprise_token.rb +++ b/app/models/enterprise_token.rb @@ -45,8 +45,8 @@ class EnterpriseToken < ApplicationRecord active_tokens.reject(&:trial?) end - def active_trial_tokens - active_tokens.select(&:trial?) + def active_trial_token + active_tokens.find(&:trial?) end def table_exists? @@ -61,6 +61,10 @@ class EnterpriseToken < ApplicationRecord active_tokens.any? end + def trial_only? + active_non_trial_tokens.empty? && active_trial_token.present? + end + def available_features active_tokens.map(&:available_features).flatten.uniq end @@ -84,8 +88,8 @@ class EnterpriseToken < ApplicationRecord def user_limit if active_non_trial_tokens.any? get_user_limit_of(active_non_trial_tokens) - else - get_user_limit_of(active_trial_tokens) + elsif active_trial_token + get_user_limit_of([active_trial_token]) end end @@ -118,6 +122,7 @@ class EnterpriseToken < ApplicationRecord uniqueness: { message: I18n.t("activerecord.errors.models.enterprise_token.already_added") } validate :valid_token_object validate :valid_domain + validate :one_trial_token before_validation :strip_encoded_token before_save :extract_validity_from_token @@ -218,6 +223,10 @@ class EnterpriseToken < ApplicationRecord [expires_at || FAR_FUTURE_DATE, starts_at || FAR_FUTURE_DATE] end + def days_left + (expires_at.to_date - Time.zone.today).to_i + end + private def strip_encoded_token @@ -239,6 +248,12 @@ class EnterpriseToken < ApplicationRecord errors.add :domain, :invalid if invalid_domain? end + def one_trial_token + if self.class.active_trial_token.present? + errors.add :base, :only_one_trial + end + end + def extract_validity_from_token return unless token_object diff --git a/app/models/exports/formatters/custom_field_pdf.rb b/app/models/exports/formatters/custom_field_pdf.rb index 351e72e23f2..5d3c709d633 100644 --- a/app/models/exports/formatters/custom_field_pdf.rb +++ b/app/models/exports/formatters/custom_field_pdf.rb @@ -35,6 +35,13 @@ module Exports export_format == :pdf && attribute.start_with?("cf_") end + def format_value(value, options) + # avoid the value transformed in to a string by the super method + return value if value.is_a?(::Exports::Formatters::LinkFormatter) + + super + end + ## # Print the value meant for PDF export. # @@ -44,6 +51,8 @@ module Exports if custom_field.field_format == "bool" value = object.typed_custom_value_for(custom_field) value ? I18n.t(:general_text_Yes) : I18n.t(:general_text_No) + elsif custom_field.field_format == "link" + LinkFormatter.new(object, custom_field) else super end diff --git a/app/models/exports/formatters/hierarchy_formatter.rb b/app/models/exports/formatters/hierarchy_formatter.rb index fec60692aaf..06aee69b495 100644 --- a/app/models/exports/formatters/hierarchy_formatter.rb +++ b/app/models/exports/formatters/hierarchy_formatter.rb @@ -38,14 +38,10 @@ module Exports def format_hierarchy_item_for_export(item_value) item = ::CustomField::Hierarchy::Item.find_by(id: item_value.to_s) - return "#{item_value} #{I18n.t(:label_not_found)}" if item.nil? + return "#{item_value} #{I18n.t(:label_not_found)}" unless item item.ancestry_path end - - def hierarchy_item_service - ::CustomFields::Hierarchy::HierarchicalItemService.new - end end end end diff --git a/app/components/work_packages/types/pattern_input.rb b/app/models/exports/formatters/link_formatter.rb similarity index 65% rename from app/components/work_packages/types/pattern_input.rb rename to app/models/exports/formatters/link_formatter.rb index aec45fc3e06..d8e30880d6f 100644 --- a/app/components/work_packages/types/pattern_input.rb +++ b/app/models/exports/formatters/link_formatter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# -- copyright +#-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH # @@ -26,33 +26,21 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -# ++ +#++ -module WorkPackages - module Types - class PatternInput < Primer::Forms::BaseComponent - prepend Primer::OpenProject::Forms::WrappedInput - - delegate :name, to: :@input - - def initialize(input:, value:, suggestions:) - super() - @input = input - @value = value - @suggestions = suggestions +module Exports + module Formatters + class LinkFormatter + def initialize(object, custom_field) + @href = object.custom_value_for(custom_field).to_s end - def suggestions_for_stimulus - @suggestions_for_stimulus ||= @suggestions.to_json + def to_html + ApplicationController.helpers.link_to(@href, @href) end - def suggestions_list_component - @suggestions_list_component ||= Primer::Alpha::ActionList.new( - role: :list, - scheme: :inset, - ml: 0, - "data-pattern-input-target": "suggestions" - ) + def to_s + @href end end end diff --git a/app/models/members/roles_diff.rb b/app/models/members/roles_diff.rb index 9fe82cb083f..34d23a11017 100644 --- a/app/models/members/roles_diff.rb +++ b/app/models/members/roles_diff.rb @@ -63,7 +63,7 @@ class Members::RolesDiff def user_previous_member_roles_ids Set.new(user_member.member_roles - .reject { group_member.member_roles.map(&:id).include?(_1.inherited_from) } + .reject { group_member.member_roles.map(&:id).include?(it.inherited_from) } .map(&:role_id).uniq) end diff --git a/app/models/principal.rb b/app/models/principal.rb index 2b0e16c495f..7b7537f0e6e 100644 --- a/app/models/principal.rb +++ b/app/models/principal.rb @@ -130,7 +130,7 @@ class Principal < ApplicationRecord def self.columns_for_name(formatter = nil) raise NotImplementedError, "Redefine in subclass" unless self == Principal - [User, Group, PlaceholderUser].map { _1.columns_for_name(formatter) }.inject(:|) + [User, Group, PlaceholderUser].map { it.columns_for_name(formatter) }.inject(:|) end # Select columns for formatting the user's name. diff --git a/app/models/queries/projects/selects/default.rb b/app/models/queries/projects/selects/default.rb index 609698118c8..8101f17df78 100644 --- a/app/models/queries/projects/selects/default.rb +++ b/app/models/queries/projects/selects/default.rb @@ -34,6 +34,6 @@ class Queries::Projects::Selects::Default < Queries::Selects::Base end def self.all_available - KEYS.map { new(_1) } + KEYS.map { new(it) } end end diff --git a/app/models/type.rb b/app/models/type.rb index 2bbd17aefda..a3b26c86b54 100644 --- a/app/models/type.rb +++ b/app/models/type.rb @@ -36,7 +36,7 @@ class Type < ApplicationRecord include ::Scopes::Scoped - attribute :patterns, Types::Patterns::CollectionType.new + attribute :patterns, WorkPackageTypes::Patterns::CollectionType.new store_attribute :pdf_export_templates_config, :export_templates_disabled, :json store_attribute :pdf_export_templates_config, :export_templates_order, :json diff --git a/app/models/work_package/exports/macros/attributes.rb b/app/models/work_package/exports/macros/attributes.rb index 2a938e8bdd3..71f7947b3d4 100644 --- a/app/models/work_package/exports/macros/attributes.rb +++ b/app/models/work_package/exports/macros/attributes.rb @@ -162,6 +162,9 @@ module WorkPackage::Exports def self.format_attribute_value(ar_name, model, obj) formatter = Exports::Register.formatter_for(model, ar_name, :pdf) value = formatter.format(obj) + # do NOT escape a tag for custom field link + return value.to_html if value.is_a?(::Exports::Formatters::LinkFormatter) + # important NOT to return empty string as this could change meaning of markdown # e.g. **to_be_replaced** could be rendered as **** (horizontal line and a *) value.blank? ? " " : escape_tags(value) diff --git a/app/models/work_package/exports/macros/links.rb b/app/models/work_package/exports/macros/links.rb index 5f3b0dcb63b..d0022fe3bf9 100644 --- a/app/models/work_package/exports/macros/links.rb +++ b/app/models/work_package/exports/macros/links.rb @@ -48,10 +48,6 @@ module WorkPackage::Exports [WorkPackagesLinkHandler] end - def self.html_replacement? - true - end - # Faster inclusion check before the full regex is being applied def self.applicable?(content) /#\d/.match(content) diff --git a/app/models/work_package/pdf_export/common/common.rb b/app/models/work_package/pdf_export/common/common.rb index fe2b3e68a62..f63277488c6 100644 --- a/app/models/work_package/pdf_export/common/common.rb +++ b/app/models/work_package/pdf_export/common/common.rb @@ -361,4 +361,17 @@ module WorkPackage::PDFExport::Common::Common end end end + + def get_cf_link_cell(custom_url) + make_link_href_cell(custom_url.to_s, custom_url.to_s) + end + + def get_value_cell_by_column(work_package, column_name, format_subject) + value = get_column_value(work_package, column_name) + return get_cf_link_cell(value) if value.is_a?(::Exports::Formatters::LinkFormatter) + return get_id_column_cell(work_package, value) if column_name == :id + return get_subject_column_cell(work_package, value) if format_subject && column_name == :subject + + escape_tags(value) + end end diff --git a/app/models/work_package/pdf_export/common/macro.rb b/app/models/work_package/pdf_export/common/macro.rb index 8eb1fce6cef..26895f07f75 100644 --- a/app/models/work_package/pdf_export/common/macro.rb +++ b/app/models/work_package/pdf_export/common/macro.rb @@ -70,17 +70,13 @@ module WorkPackage::PDFExport::Common::Macro end def apply_macros_node_text(node, context) - formatted, applied_macros = apply_macro_text(node.string_content, context) + formatted = apply_macro_text(node.string_content, context) return if formatted == node.string_content - if has_html_macro?(applied_macros) - fragment = Markly::Node.new(:inline_html) - fragment.string_content = formatted - node.insert_before(fragment) - node.delete - else - node.string_content = formatted - end + fragment = Markly::Node.new(:inline_html) + fragment.string_content = formatted + node.insert_before(fragment) + node.delete end def apply_macros_node_html(node, context) @@ -100,7 +96,7 @@ module WorkPackage::PDFExport::Common::Macro macro.process_match(Regexp.last_match, matched_string, context) end end - [text, applicable_macros] + text end def apply_macro_html(html, context) @@ -111,20 +107,10 @@ module WorkPackage::PDFExport::Common::Macro doc.to_html end - def has_html_macro?(macros) - macros.any? { |macro| macro.respond_to?(:html_replacement?) && macro.html_replacement? } - end - def apply_macro_html_node(node, context) if node.text? - formatted, applied_macros = apply_macro_text(node.content, context) - if formatted != node.content - if has_html_macro?(applied_macros) - node.replace(formatted) - else - node.content = formatted - end - end + formatted = apply_macro_text(node.content, context) + node.replace(formatted) if formatted != node.content elsif PREFORMATTED_BLOCKS.exclude?(node.name) node.children.each { |child| apply_macro_html_node(child, context) } end diff --git a/app/models/work_package/pdf_export/export/markdown.rb b/app/models/work_package/pdf_export/export/markdown.rb index 95d7e3a863a..da5d05213a4 100644 --- a/app/models/work_package/pdf_export/export/markdown.rb +++ b/app/models/work_package/pdf_export/export/markdown.rb @@ -173,6 +173,8 @@ module WorkPackage::PDFExport::Export::Markdown end def write_markdown!(markdown, styling_yml) + return if markdown.blank? + markdown_writer(styling_yml) .draw_markdown(markdown, pdf, ->(src) { with_images? ? attachment_image_filepath(src) : nil diff --git a/app/models/work_package/pdf_export/export/report/attributes.rb b/app/models/work_package/pdf_export/export/report/attributes.rb index 911f09bfa12..e69b2478d63 100644 --- a/app/models/work_package/pdf_export/export/report/attributes.rb +++ b/app/models/work_package/pdf_export/export/report/attributes.rb @@ -81,4 +81,8 @@ module WorkPackage::PDFExport::Export::Report::Attributes attribute_data[:value] ] end + + def get_column_value_cell(work_package, column_name) + get_value_cell_by_column(work_package, column_name, wants_report?) + end end diff --git a/app/models/work_package/pdf_export/export/wp/attributes.rb b/app/models/work_package/pdf_export/export/wp/attributes.rb index 255b7c1acc4..bb41a95233f 100644 --- a/app/models/work_package/pdf_export/export/wp/attributes.rb +++ b/app/models/work_package/pdf_export/export/wp/attributes.rb @@ -242,10 +242,6 @@ module WorkPackage::PDFExport::Export::Wp::Attributes end def get_column_value_cell(work_package, column_name) - value = get_column_value(work_package, column_name) - return get_id_column_cell(work_package, value) if column_name == :id - return get_subject_column_cell(work_package, value) if column_name == :subject - - escape_tags(value) + get_value_cell_by_column(work_package, column_name, true) end 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 eac73e4bc5d..12e070543b5 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 @@ -301,12 +301,4 @@ class WorkPackage::PDFExport::WorkPackageListToPdf < WorkPackage::Exports::Query def get_total_sums query.display_sums? ? (query.results.all_total_sums || {}) : {} end - - def get_column_value_cell(work_package, column_name) - value = get_column_value(work_package, column_name) - return get_id_column_cell(work_package, value) if column_name == :id - return get_subject_column_cell(work_package, value) if wants_report? && column_name == :subject - - escape_tags(value) - end end diff --git a/app/models/types/forms/subject_configuration_form_model.rb b/app/models/work_package_types/forms/subject_configuration_form_model.rb similarity index 98% rename from app/models/types/forms/subject_configuration_form_model.rb rename to app/models/work_package_types/forms/subject_configuration_form_model.rb index 3a8a53cff94..878d78962dd 100644 --- a/app/models/types/forms/subject_configuration_form_model.rb +++ b/app/models/work_package_types/forms/subject_configuration_form_model.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Forms class SubjectConfigurationFormModel extend ActiveModel::Naming diff --git a/app/models/types/pattern.rb b/app/models/work_package_types/pattern.rb similarity index 98% rename from app/models/types/pattern.rb rename to app/models/work_package_types/pattern.rb index 8efe8eac5c4..3e10276075b 100644 --- a/app/models/types/pattern.rb +++ b/app/models/work_package_types/pattern.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes Pattern = Data.define(:blueprint, :enabled) do def enabled? = !!enabled diff --git a/app/models/types/pattern_resolver.rb b/app/models/work_package_types/pattern_resolver.rb similarity index 99% rename from app/models/types/pattern_resolver.rb rename to app/models/work_package_types/pattern_resolver.rb index 084a42ecbbb..1fb0dcde3bb 100644 --- a/app/models/types/pattern_resolver.rb +++ b/app/models/work_package_types/pattern_resolver.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes class PatternResolver TOKEN_REGEX = /{{[0-9A-Za-z_]+}}/ ATTRIBUTE_PLACEHOLDER = "N/A" diff --git a/app/models/types/patterns/attribute_token.rb b/app/models/work_package_types/patterns/attribute_token.rb similarity index 97% rename from app/models/types/patterns/attribute_token.rb rename to app/models/work_package_types/patterns/attribute_token.rb index 2b5f5859c5e..27a8125adb9 100644 --- a/app/models/types/patterns/attribute_token.rb +++ b/app/models/work_package_types/patterns/attribute_token.rb @@ -23,12 +23,12 @@ # # 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. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Patterns AttributeToken = Data.define(:key, :label_fn, :resolve_fn) do def label_with_context diff --git a/app/models/types/patterns/collection.rb b/app/models/work_package_types/patterns/collection.rb similarity index 98% rename from app/models/types/patterns/collection.rb rename to app/models/work_package_types/patterns/collection.rb index cc94bad0907..54951129ae1 100644 --- a/app/models/types/patterns/collection.rb +++ b/app/models/work_package_types/patterns/collection.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Patterns Collection = Data.define(:patterns) do extend Dry::Monads[:result] @@ -45,7 +45,7 @@ module Types end def initialize(patterns:) - transformed = patterns.transform_values { Pattern.new(**_1) }.freeze + transformed = patterns.transform_values { Pattern.new(**it) }.freeze super(patterns: transformed) end diff --git a/app/models/types/patterns/collection_contract.rb b/app/models/work_package_types/patterns/collection_contract.rb similarity index 98% rename from app/models/types/patterns/collection_contract.rb rename to app/models/work_package_types/patterns/collection_contract.rb index 74f06b8a2da..c518694360b 100644 --- a/app/models/types/patterns/collection_contract.rb +++ b/app/models/work_package_types/patterns/collection_contract.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Patterns class CollectionContract < Dry::Validation::Contract config.messages.backend = :i18n diff --git a/app/models/types/patterns/collection_type.rb b/app/models/work_package_types/patterns/collection_type.rb similarity index 98% rename from app/models/types/patterns/collection_type.rb rename to app/models/work_package_types/patterns/collection_type.rb index f7c84b23416..c7a4b2f4250 100644 --- a/app/models/types/patterns/collection_type.rb +++ b/app/models/work_package_types/patterns/collection_type.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Patterns class CollectionType < ActiveModel::Type::Value def assert_valid_value(value) diff --git a/app/models/types/patterns/pattern_token.rb b/app/models/work_package_types/patterns/pattern_token.rb similarity index 98% rename from app/models/types/patterns/pattern_token.rb rename to app/models/work_package_types/patterns/pattern_token.rb index 04abe5016e4..ad2b449f791 100644 --- a/app/models/types/patterns/pattern_token.rb +++ b/app/models/work_package_types/patterns/pattern_token.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Patterns PatternToken = Data.define(:pattern, :key) do private_class_method :new diff --git a/app/models/types/patterns/token_property_mapper.rb b/app/models/work_package_types/patterns/token_property_mapper.rb similarity index 99% rename from app/models/types/patterns/token_property_mapper.rb rename to app/models/work_package_types/patterns/token_property_mapper.rb index 780c7c29b98..c9391be3ad9 100644 --- a/app/models/types/patterns/token_property_mapper.rb +++ b/app/models/work_package_types/patterns/token_property_mapper.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Types +module WorkPackageTypes module Patterns class TokenPropertyMapper # rubocop:disable Layout/LineLength diff --git a/app/seeders/composite_seeder.rb b/app/seeders/composite_seeder.rb index 4bdfbde74cb..504c530f5e2 100644 --- a/app/seeders/composite_seeder.rb +++ b/app/seeders/composite_seeder.rb @@ -30,7 +30,7 @@ class CompositeSeeder < Seeder seed_with(data_seeders) if discovered_seeders.any? - print_status "Loading discovered seeders: #{discovered_seeders.map { seeder_name(_1) }.join(', ')}" + print_status "Loading discovered seeders: #{discovered_seeders.map { seeder_name(it) }.join(', ')}" seed_with(discovered_seeders) end end diff --git a/app/seeders/source/translate.rb b/app/seeders/source/translate.rb index 7f29cf923a6..e1638c3e9d2 100644 --- a/app/seeders/source/translate.rb +++ b/app/seeders/source/translate.rb @@ -65,8 +65,8 @@ module Source::Translate def translatable_keys(hash) hash.keys - .filter { translatable?(_1) } - .map { remove_translatable_prefix(_1) } + .filter { translatable?(it) } + .map { remove_translatable_prefix(it) } end def translate_value(value, i18n_key) diff --git a/app/services/add_work_package_note_service.rb b/app/services/add_work_package_note_service.rb index 929c3b2aa09..5f6b9beb75f 100644 --- a/app/services/add_work_package_note_service.rb +++ b/app/services/add_work_package_note_service.rb @@ -46,21 +46,47 @@ class AddWorkPackageNoteService def call(notes, send_notifications: nil, internal: false) in_context(work_package, send_notifications:) do - work_package.add_journal(user:, notes:, internal:) + add_journal_to_work_package(notes, internal) + success, errors = save_journal_with_validation - success, errors = validate_and_yield(work_package, user) do - work_package.save_journals - end + journal = fetch_latest_journal if success - if success - # In test environment, because of the difference in the way of handling transactions, - # the journal needs to be actively loaded without SQL caching in place. - journal = Journal.connection.uncached do - work_package.journals.last - end - end - - ServiceResult.new(success:, result: journal, errors:) + build_service_result(success, journal, errors) end end + + private + + def add_journal_to_work_package(notes, internal) + work_package.add_journal(user:, notes:, internal:) + end + + def save_journal_with_validation + validate_and_yield(work_package, user) do + work_package.save_journals + end + end + + def fetch_latest_journal + # In test environment, because of the difference in the way of handling transactions, + # the journal needs to be actively loaded without SQL caching in place. + Journal.connection.uncached do + work_package.journals.last + end + end + + def build_service_result(success, result, errors) + ServiceResult.new(success:, result:, errors:).on_success { add_attachment_claims_to(it) } + end + + def add_attachment_claims_to(service_result) + attachments_claims = claim_attachments_for(service_result.result) + service_result.add_dependent!(attachments_claims) + end + + def claim_attachments_for(journal) + WorkPackages::ActivitiesTab::CommentAttachmentsClaims::ClaimsService + .new(user: User.current, model: journal) + .call + end end diff --git a/app/services/attachments/base_service.rb b/app/services/attachments/base_service.rb index 3ebf934fd84..fda62e662b3 100644 --- a/app/services/attachments/base_service.rb +++ b/app/services/attachments/base_service.rb @@ -36,8 +36,8 @@ module Attachments # @param whitelist A custom whitelist to validate with, or empty to disable validation # # Warning: When passing an empty whitelist, this results in no validations on the content type taking place. - def self.bypass_whitelist(user:, whitelist: []) - new(user:, contract_options: { whitelist: whitelist.map(&:to_s) }) + def self.bypass_allowlist(user:, allowlist: []) + new(user:, contract_options: { allowlist: allowlist.map(&:to_s) }) end end end diff --git a/app/services/attachments/delete_service.rb b/app/services/attachments/delete_service.rb index bd1f0924cbb..3b70a8bb476 100644 --- a/app/services/attachments/delete_service.rb +++ b/app/services/attachments/delete_service.rb @@ -30,8 +30,9 @@ class Attachments::DeleteService < BaseServices::Delete include Attachments::TouchContainer def call(params = {}) + self.params = params in_context(model.container || model) do - perform(params) + perform end end diff --git a/app/services/attachments/finish_direct_upload_service.rb b/app/services/attachments/finish_direct_upload_service.rb new file mode 100644 index 00000000000..12d59f02c63 --- /dev/null +++ b/app/services/attachments/finish_direct_upload_service.rb @@ -0,0 +1,140 @@ +# 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 Attachments + class FinishDirectUploadService < BaseServices::BaseContracted + def initialize(user:, model:, contract_class: nil, contract_options: {}) + self.model = model + super(user:, contract_class:, contract_options:) + end + + protected + + alias_method :attachment, :model + + def service_context(send_notifications:, &) + # Overwriting the call to in_context to place the semaphore on the container and not on the attachment. + # Since the service will write a journal on the container, there should not be another + # process that modifies the container while this service is running. + in_context(attachment.container, send_notifications:, &) + end + + def validate_params + super.tap do |call| + validate_local_file_exists(call) + end + end + + def before_perform(_) + super.tap do + set_attachment_parameters + end + end + + def persist(call) + super.tap do |service_result| + unless attachment.save + service_result.errors = attachment.errors + service_result.success = false + end + end + end + + def after_perform(service_call) + super.tap do + journalize_container + attachment_created_event + schedule_jobs + end + end + + def validate_local_file_exists(call) + unless local_file + call.errors.add(:base, "File for attachment #{attachment.filename} was not uploaded.") + call.success = false + end + end + + def set_attachment_parameters + attachment.extend(OpenProject::ChangedBySystem) + attachment.change_by_system do + attachment.status = :uploaded + attachment.file = local_file + end + end + + def schedule_jobs + attachment.extract_fulltext + end + + def journalize_container + journable = attachment.container + + return unless journable&.class&.journaled? + + # Touching the journable will lead to the journal created next having its own timestamp. + # That timestamp will not adequately reflect the time the attachment was uploaded. This job + # right here might be executed way later than the time the attachment was uploaded. Ideally, + # the journals would be created bearing the time stamps of the attachment's created_at. + # This remains a TODO. + # But with the timestamp update in place as it is, at least the collapsing of aggregated journals + # from days before with the newly uploaded attachment is prevented. + touch_journable(journable) + + Journals::CreateService + .new(journable, attachment.author) + .call + end + + def touch_journable(journable) + # Not using touch here on purpose, + # as to avoid changing lock versions on the journables for this change + attributes = journable.send(:timestamp_attributes_for_update_in_model) + + timestamps = attributes.index_with { Time.current } + journable.update_columns(timestamps) if timestamps.any? + end + + def attachment_created_event + OpenProject::Notifications.send( + OpenProject::Events::ATTACHMENT_CREATED, + attachment: + ) + end + + def default_contract_class + ::Attachments::CreateContract + end + + def local_file + attachment&.diskfile + end + end +end diff --git a/app/services/base_services/base_callable.rb b/app/services/base_services/base_callable.rb index d229eacc44f..7b746f3f0cd 100644 --- a/app/services/base_services/base_callable.rb +++ b/app/services/base_services/base_callable.rb @@ -39,7 +39,7 @@ module BaseServices self.params = extract_options!(args).deep_symbolize_keys run_callbacks(:call) do - perform(*args, **params) + perform(*args) end end diff --git a/app/services/base_services/base_contracted.rb b/app/services/base_services/base_contracted.rb index 88a7873dd28..35a78203c85 100644 --- a/app/services/base_services/base_contracted.rb +++ b/app/services/base_services/base_contracted.rb @@ -54,13 +54,13 @@ module BaseServices in_context(model, send_notifications:, &) end - def perform(params = {}) - params, send_notifications = extract(params, :send_notifications) + def perform + self.params, send_notifications = extract(params, :send_notifications) service_context(send_notifications:) do - service_call = validate_params(params) - service_call = before_perform(params, service_call) if service_call.success? + service_call = validate_params + service_call = before_perform(service_call) if service_call.success? service_call = validate_contract(service_call) if service_call.success? - service_call = after_validate(params, service_call) if service_call.success? + service_call = after_validate(service_call) if service_call.success? service_call = persist(service_call) if service_call.success? service_call = after_perform(service_call) if service_call.success? @@ -69,19 +69,19 @@ module BaseServices end def extract(params, attribute) - params = params ? params.dup : {} - [params, params.delete(attribute)] + params ||= {} + [params, params[attribute]] end - def validate_params(_params) + def validate_params ServiceResult.success(result: model) end - def before_perform(*) + def before_perform(_) ServiceResult.success(result: model) end - def after_validate(_params, contract_call) + def after_validate(contract_call) contract_call end diff --git a/app/services/base_services/delete.rb b/app/services/base_services/delete.rb index c0a58d9a89f..b76cddc438c 100644 --- a/app/services/base_services/delete.rb +++ b/app/services/base_services/delete.rb @@ -33,8 +33,8 @@ module BaseServices super(user:, contract_class:, contract_options:) end - def persist(service_result) - service_result = super(service_result) # rubocop:disable Style/SuperArguments + def persist(_service_result) + service_result = super unless destroy(service_result.result) service_result.errors = service_result.result.errors diff --git a/app/services/base_services/set_attributes.rb b/app/services/base_services/set_attributes.rb index e62990c849e..26b46ad6187 100644 --- a/app/services/base_services/set_attributes.rb +++ b/app/services/base_services/set_attributes.rb @@ -40,8 +40,8 @@ module BaseServices self.contract_options = contract_options end - def perform(params = {}) - set_attributes(params || {}) + def perform + set_attributes(params) validate_and_result end diff --git a/app/services/base_services/write.rb b/app/services/base_services/write.rb index cb39f7dfc31..453dd392a0f 100644 --- a/app/services/base_services/write.rb +++ b/app/services/base_services/write.rb @@ -30,8 +30,8 @@ module BaseServices class Write < BaseContracted protected - def persist(service_result) - service_result = super(service_result) # rubocop:disable Style/SuperArguments + def persist(_service_result) + service_result = super unless service_result.result.save service_result.errors = service_result.result.errors @@ -47,7 +47,7 @@ module BaseServices service_result end - def before_perform(params, _service_result) + def before_perform(_service_result) set_attributes(params) end diff --git a/app/services/base_type_service.rb b/app/services/base_type_service.rb index 8ba1b086bda..ea4165261e3 100644 --- a/app/services/base_type_service.rb +++ b/app/services/base_type_service.rb @@ -171,8 +171,8 @@ class BaseTypeService type.custom_field_ids = type .attribute_groups .flat_map(&:members) - .select { CustomField.custom_field_attribute? _1 } - .map { _1.gsub(/^custom_field_/, "").to_i } + .select { CustomField.custom_field_attribute? it } + .map { it.gsub(/^custom_field_/, "").to_i } .uniq end diff --git a/app/services/bulk_services/project_mappings/base_create_service.rb b/app/services/bulk_services/project_mappings/base_create_service.rb index 03d7c209301..fea2c8bbd43 100644 --- a/app/services/bulk_services/project_mappings/base_create_service.rb +++ b/app/services/bulk_services/project_mappings/base_create_service.rb @@ -41,7 +41,7 @@ module BulkServices @mapping_context = mapping_context end - def perform(params = {}) + def perform service_call = validate_permissions service_call = validate_contract(service_call, params) if service_call.success? service_call = perform_bulk_create(service_call) if service_call.success? diff --git a/app/services/copy/dependency.rb b/app/services/copy/dependency.rb index e6aca6213c6..6dfed63fb6c 100644 --- a/app/services/copy/dependency.rb +++ b/app/services/copy/dependency.rb @@ -79,7 +79,7 @@ module Copy errors.full_messages.join(". ") end - def perform(params:) + def perform begin copy_dependency(params:) rescue StandardError => e diff --git a/app/services/custom_fields/create_service.rb b/app/services/custom_fields/create_service.rb index 314ea55e46e..1593aa93365 100644 --- a/app/services/custom_fields/create_service.rb +++ b/app/services/custom_fields/create_service.rb @@ -38,7 +38,7 @@ module CustomFields nil end - def perform(params) + def perform super rescue StandardError => e ServiceResult.failure(message: e.message) diff --git a/app/services/groups/concerns/membership_manipulation.rb b/app/services/groups/concerns/membership_manipulation.rb index c5fdbbbde27..10122765ddb 100644 --- a/app/services/groups/concerns/membership_manipulation.rb +++ b/app/services/groups/concerns/membership_manipulation.rb @@ -30,9 +30,7 @@ module Groups::Concerns module MembershipManipulation extend ActiveSupport::Concern - def after_validate(params, _call) - params ||= {} - + def after_validate(_call) with_error_handled do ::Group.transaction do send_notifications = params.fetch(:send_notifications, Journal::NotificationConfiguration.active?) diff --git a/app/services/journals/create_service/association.rb b/app/services/journals/create_service/association.rb index 7242109d183..ad785dab0f4 100644 --- a/app/services/journals/create_service/association.rb +++ b/app/services/journals/create_service/association.rb @@ -42,7 +42,7 @@ class Journals::CreateService def self.for(journable) ASSOCIATION_NAMES - .map { "Journals::CreateService::#{_1}".constantize.new(journable) } + .map { "Journals::CreateService::#{it}".constantize.new(journable) } .select(&:associated?) end diff --git a/app/services/journals/update_service.rb b/app/services/journals/update_service.rb index db3ba619ede..b6837cd920f 100644 --- a/app/services/journals/update_service.rb +++ b/app/services/journals/update_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -31,11 +33,28 @@ module Journals protected def after_perform(call) + if call.success? && activity_comment?(call.result) + attachments_claims = claim_attachments_for(call.result) + call.add_dependent!(attachments_claims) + end + OpenProject::Notifications.send(OpenProject::Events::JOURNAL_UPDATED, journal: call.result, send_notification: Journal::NotificationConfiguration.active?) call end + + private + + def activity_comment?(journal) + journal.notes.present? + end + + def claim_attachments_for(journal) + WorkPackages::ActivitiesTab::CommentAttachmentsClaims::ClaimsService + .new(user: User.current, model: journal) + .call + end end end diff --git a/app/services/members/cleanup_service.rb b/app/services/members/cleanup_service.rb index f6f1f30ff30..e916ceab962 100644 --- a/app/services/members/cleanup_service.rb +++ b/app/services/members/cleanup_service.rb @@ -37,7 +37,7 @@ module Members protected - def perform(*) + def perform prune_watchers unassign_categories diff --git a/app/services/oauth_clients/create_service.rb b/app/services/oauth_clients/create_service.rb index 200c3d46694..a8b2603c0ab 100644 --- a/app/services/oauth_clients/create_service.rb +++ b/app/services/oauth_clients/create_service.rb @@ -36,7 +36,7 @@ module OAuthClients class CreateService < ::BaseServices::Create protected - def after_validate(params, contract_call) + def after_validate(contract_call) OAuthClient.where(integration: params[:integration]).delete_all super end diff --git a/app/services/project_custom_field_project_mappings/bulk_update_service.rb b/app/services/project_custom_field_project_mappings/bulk_update_service.rb index ee11eba5f0a..53af1963650 100644 --- a/app/services/project_custom_field_project_mappings/bulk_update_service.rb +++ b/app/services/project_custom_field_project_mappings/bulk_update_service.rb @@ -35,7 +35,7 @@ module ProjectCustomFieldProjectMappings @project_custom_field_section = project_custom_field_section end - def perform(params) + def perform service_call = validate_permissions service_call = perform_bulk_edit(service_call, params) if service_call.success? diff --git a/app/services/project_custom_fields/drop_service.rb b/app/services/project_custom_fields/drop_service.rb index 0a416228d9b..833bd72365a 100644 --- a/app/services/project_custom_fields/drop_service.rb +++ b/app/services/project_custom_fields/drop_service.rb @@ -34,7 +34,7 @@ module ProjectCustomFields @project_custom_field = project_custom_field end - def perform(params) + def perform service_call = validate_permissions service_call = perform_drop(service_call, params) if service_call.success? diff --git a/app/services/project_phases/set_attributes_service.rb b/app/services/project_phases/set_attributes_service.rb index dd5f47dacad..7f261ccb826 100644 --- a/app/services/project_phases/set_attributes_service.rb +++ b/app/services/project_phases/set_attributes_service.rb @@ -28,7 +28,7 @@ module ProjectPhases class SetAttributesService < ::BaseServices::SetAttributes - def perform(*) + def perform super.tap do set_calculated_duration end diff --git a/app/services/project_queries/publish_service.rb b/app/services/project_queries/publish_service.rb index e291763f6ce..c86b1eeb267 100644 --- a/app/services/project_queries/publish_service.rb +++ b/app/services/project_queries/publish_service.rb @@ -30,7 +30,7 @@ module ProjectQueries class PublishService < BaseServices::Update private - def after_validate(params, service_call) + def after_validate(service_call) model.public = params[:public] service_call diff --git a/app/services/projects/concerns/new_project_service.rb b/app/services/projects/concerns/new_project_service.rb index 040a63b5d52..34d93f1a205 100644 --- a/app/services/projects/concerns/new_project_service.rb +++ b/app/services/projects/concerns/new_project_service.rb @@ -30,13 +30,13 @@ module Projects::Concerns module NewProjectService private - def before_perform(params, service_call) + def before_perform(service_call) super.tap do |super_call| reject_section_scoped_validation(super_call.result) end end - def after_validate(params, service_call) + def after_validate(service_call) super.tap do |super_call| build_missing_project_custom_field_project_mappings(super_call.result) end diff --git a/app/services/projects/copy_service.rb b/app/services/projects/copy_service.rb index f18b6b31478..4e794ce7c97 100644 --- a/app/services/projects/copy_service.rb +++ b/app/services/projects/copy_service.rb @@ -86,7 +86,7 @@ module Projects .merge(target_project_params) end - def before_perform(params, service_call) + def before_perform(service_call) super.tap do |super_call| # Retain values after the set attributes service retain_attributes(source, super_call.result) diff --git a/app/services/projects/enabled_modules_service.rb b/app/services/projects/enabled_modules_service.rb index eb4ac5cb807..1b4bfc85423 100644 --- a/app/services/projects/enabled_modules_service.rb +++ b/app/services/projects/enabled_modules_service.rb @@ -38,7 +38,7 @@ module Projects private - def before_perform(params, _service_result) + def before_perform(_service_result) model.enabled_module_names = params[:enabled_modules] super end diff --git a/app/services/projects/enqueue_copy_service.rb b/app/services/projects/enqueue_copy_service.rb index 7c3b51b5800..23be0188e3f 100644 --- a/app/services/projects/enqueue_copy_service.rb +++ b/app/services/projects/enqueue_copy_service.rb @@ -39,7 +39,7 @@ module Projects private - def perform(params) + def perform call = test_copy(params) if call.success? diff --git a/app/services/projects/schedule_deletion_service.rb b/app/services/projects/schedule_deletion_service.rb index e1f32f71488..e700ca53067 100644 --- a/app/services/projects/schedule_deletion_service.rb +++ b/app/services/projects/schedule_deletion_service.rb @@ -39,7 +39,7 @@ module Projects private - def before_perform(_params, service_result) + def before_perform(service_result) return service_result if model.archived? Projects::ArchiveService diff --git a/app/services/relations/create_service.rb b/app/services/relations/create_service.rb index 37b27353320..c857915ddef 100644 --- a/app/services/relations/create_service.rb +++ b/app/services/relations/create_service.rb @@ -32,8 +32,10 @@ class Relations::CreateService < Relations::BaseService self.contract_class = Relations::CreateContract end - def perform(send_notifications: nil, **attributes) - in_user_context(send_notifications:) do + def perform + attributes = params.except(:send_notifications) + + in_user_context(send_notifications: params[:send_notifications]) do update_relation ::Relation.new, attributes end end diff --git a/app/services/relations/delete_service.rb b/app/services/relations/delete_service.rb index a856e698db4..ee800009164 100644 --- a/app/services/relations/delete_service.rb +++ b/app/services/relations/delete_service.rb @@ -29,7 +29,7 @@ class Relations::DeleteService < BaseServices::Delete include Relations::Concerns::Rescheduling - def after_perform(_result) + def after_perform(_call) result = super if result.success? && deleted_relation.follows? reschedule_result = reschedule_successor(deleted_relation) diff --git a/app/services/relations/update_service.rb b/app/services/relations/update_service.rb index 23ecc219701..a7166079655 100644 --- a/app/services/relations/update_service.rb +++ b/app/services/relations/update_service.rb @@ -35,9 +35,9 @@ class Relations::UpdateService < Relations::BaseService self.contract_class = Relations::UpdateContract end - def perform(attributes) - in_user_context(send_notifications: attributes[:send_notifications]) do - update_relation model, attributes + def perform + in_user_context(send_notifications: params[:send_notifications]) do + update_relation model, params end end end diff --git a/app/services/reminders/set_attributes_service.rb b/app/services/reminders/set_attributes_service.rb index 6c52a2f3708..d55e337a7db 100644 --- a/app/services/reminders/set_attributes_service.rb +++ b/app/services/reminders/set_attributes_service.rb @@ -30,7 +30,7 @@ module Reminders class SetAttributesService < ::BaseServices::SetAttributes - def perform(params = {}) + def perform remind_at_params = params.extract!(:remind_at_date, :remind_at_time) build_remind_at_from_params(params, remind_at_params) unless params.key?(:remind_at) diff --git a/app/services/roles/create_service.rb b/app/services/roles/create_service.rb index 95774a71a39..abf1b5830fe 100644 --- a/app/services/roles/create_service.rb +++ b/app/services/roles/create_service.rb @@ -29,7 +29,7 @@ class Roles::CreateService < BaseServices::Create private - def perform(params) + def perform copy_workflow_id = params.delete(:copy_workflow_from) super_call = super diff --git a/app/services/roles/update_service.rb b/app/services/roles/update_service.rb index 46358b9f2d3..c76635960f1 100644 --- a/app/services/roles/update_service.rb +++ b/app/services/roles/update_service.rb @@ -29,7 +29,7 @@ class Roles::UpdateService < BaseServices::Update private - def before_perform(params, service_call) + def before_perform(service_call) @permissions_old = service_call.result.permissions super end diff --git a/app/services/settings/working_days_and_hours_update_service.rb b/app/services/settings/working_days_and_hours_update_service.rb index 648a40870a0..8dc3a64a764 100644 --- a/app/services/settings/working_days_and_hours_update_service.rb +++ b/app/services/settings/working_days_and_hours_update_service.rb @@ -35,7 +35,7 @@ class Settings::WorkingDaysAndHoursUpdateService < Settings::UpdateService super end - def validate_params(params) + def validate_params contract = Settings::WorkingDaysAndHoursParamsContract.new(model, user, params:) ServiceResult.new success: contract.valid?, errors: contract.errors, diff --git a/app/services/shares/concerns/role_assignment.rb b/app/services/shares/concerns/role_assignment.rb index 30a140fb3ae..1b4a2a73a4e 100644 --- a/app/services/shares/concerns/role_assignment.rb +++ b/app/services/shares/concerns/role_assignment.rb @@ -38,7 +38,7 @@ module Shares::Concerns::RoleAssignment # into account are those that have not been inherited. def existing_ids model.member_roles - .select { _1.inherited_from.nil? } + .select { it.inherited_from.nil? } .map(&:role_id) end end diff --git a/app/services/update_type_service.rb b/app/services/update_type_service.rb index d41b417f9ac..3d3ca1aa14a 100644 --- a/app/services/update_type_service.rb +++ b/app/services/update_type_service.rb @@ -63,7 +63,7 @@ class UpdateTypeService < BaseTypeService end def set_patterns(patterns) - Types::Patterns::Collection + WorkPackageTypes::Patterns::Collection .build(patterns:) .either( ->(collection) { type.patterns = collection }, diff --git a/app/services/user_preferences/update_service.rb b/app/services/user_preferences/update_service.rb index 0c6e867215b..3d9153a898e 100644 --- a/app/services/user_preferences/update_service.rb +++ b/app/services/user_preferences/update_service.rb @@ -32,7 +32,7 @@ module UserPreferences attr_accessor :notifications - def validate_params(params) + def validate_params contract = ParamsContract.new(model, user, params:) ServiceResult.new success: contract.valid?, @@ -40,7 +40,7 @@ module UserPreferences result: model end - def before_perform(params, _service_result) + def before_perform(_service_result) self.notifications = params&.delete(:notification_settings) super diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index 07dedb76ccd..911fe8ffac3 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -32,7 +32,7 @@ module Users protected - def before_perform(params, _service_result) + def before_perform(_service_result) call_hook :service_update_user_before_save, params:, user: model @@ -40,8 +40,8 @@ module Users super end - def persist(service_result) - service_result = super(service_result) # rubocop:disable Style/SuperArguments + def persist(_service_result) + service_result = super if service_result.success? service_result.success = model.pref.save diff --git a/app/services/work_package_types/attribute_groups/transformer.rb b/app/services/work_package_types/attribute_groups/transformer.rb new file mode 100644 index 00000000000..1c83c3ae1d1 --- /dev/null +++ b/app/services/work_package_types/attribute_groups/transformer.rb @@ -0,0 +1,80 @@ +# 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 WorkPackageTypes + module AttributeGroups + class Transformer + def initialize(groups:, user:) + @groups = groups + @user = user + end + + def call + return [] if groups.blank? + + groups.map do |group| + if group["type"] == "query" + transform_query_group(group) + else + transform_attribute_group(group) + end + end + end + + private + + attr_reader :groups, :user + + def transform_attribute_group(group) + name = group["key"]&.to_sym || group["name"] + attributes = group["attributes"].pluck("key") + + [name, attributes] + end + + def transform_query_group(group) + name = group["name"] + props = JSON.parse(group["query"]) + + query = Query.new_default(name: "Embedded table: #{name}") + query.extend(OpenProject::ChangedBySystem) + query.change_by_system { query.user = User.system } + + ::API::V3::UpdateQueryFromV3ParamsService + .new(query, user) + .call(props.with_indifferent_access) + + query.show_hierarchies = false + + [name, [query]] + end + end + end +end diff --git a/app/services/work_package_types/set_attributes_service.rb b/app/services/work_package_types/set_attributes_service.rb index b8fc3e8077a..f9b7abed268 100644 --- a/app/services/work_package_types/set_attributes_service.rb +++ b/app/services/work_package_types/set_attributes_service.rb @@ -32,40 +32,80 @@ module WorkPackageTypes class SetAttributesService < ::BaseServices::SetAttributes def initialize(user:, model:, contract_class:, contract_options: nil) super - @valid_pattern = true + @param_validations = {} end private def set_attributes(params) permitted = params.except(:copy_workflow_from) - @valid_pattern = check_patterns(permitted) - if @valid_pattern - super(permitted) - else - super(permitted.except(:patterns)) - end + check_patterns(permitted) + check_copy_workflow(params) + check_projects(permitted) + + super(permitted.except(*@param_validations.keys)) end def validate_and_result success, errors = validate(model, user, options: {}) - if @valid_pattern + if @param_validations.empty? ServiceResult.new(success:, errors:, result: model) else - errors.add(:patterns, :is_invalid) + @param_validations.each_pair do |key, error| + errors.add(key, error) + end + ServiceResult.failure(errors:, result: model) end end def check_patterns(params) - return true unless params.key?(:patterns) - return true if params.key?(:patterns) && params[:patterns].blank? + return unless params.key?(:patterns) + return if params.key?(:patterns) && params[:patterns].blank? - Types::Patterns::CollectionContract.new.call(params[:patterns]).success? + result = WorkPackageTypes::Patterns::CollectionContract.new.call(params[:patterns]) + if result.failure? + @param_validations.update({ patterns: validation_failure_to_message(result).join(", ") }) + end rescue ArgumentError - false + @param_validations.update({ patterns: :is_invalid }) + end + + def check_copy_workflow(params) + return unless params.key?(:copy_workflow_from) + + result = CopyWorkflowAttributeContract.new.call(params.slice(:copy_workflow_from)) + if result.failure? + @param_validations.update({ copy_workflow_from: validation_failure_to_message(result).join(", ") }) + end + end + + def check_projects(params) + return unless params.key?(:project_ids) + + invalid_project_ids = params[:project_ids].reject { |id| Project.exists?(id) } + unless invalid_project_ids.empty? + @param_validations.update({ project_ids: "Projects with ids #{invalid_project_ids.join(', ')} do not exist." }) + end + end + + def validation_failure_to_message(result) + flatten_error_messages result.errors(full: true).to_h + end + + def flatten_error_messages(errors) + case errors + when String + [errors] + when Array + errors.flat_map { |e| flatten_error_messages(e) } + when Hash + errors.values.flat_map { |e| flatten_error_messages(e) } + else + [] + end end end end diff --git a/app/services/work_packages/activities_tab/comment_attachments_claims/set_attributes_service.rb b/app/services/work_packages/activities_tab/comment_attachments_claims/set_attributes_service.rb index 2907dd5295e..388204afcee 100644 --- a/app/services/work_packages/activities_tab/comment_attachments_claims/set_attributes_service.rb +++ b/app/services/work_packages/activities_tab/comment_attachments_claims/set_attributes_service.rb @@ -36,12 +36,9 @@ module WorkPackages ATTACHMENT_CSS_SELECTOR = "img.op-uc-image" - def perform(params = {}) - super( - params.reverse_merge( - attachment_ids: collect_attachment_ids_from_notes - ) - ) + def perform + self.params = params.reverse_merge(attachment_ids: collect_attachment_ids_from_notes) + super end private diff --git a/app/services/work_packages/copy_service.rb b/app/services/work_packages/copy_service.rb index b8201c46e98..5a90732880d 100644 --- a/app/services/work_packages/copy_service.rb +++ b/app/services/work_packages/copy_service.rb @@ -42,23 +42,25 @@ class WorkPackages::CopyService < BaseServices::BaseCallable self.contract_class = contract_class end - def perform(send_notifications: nil, copy_attachments: true, copy_share_members: true, **attributes) - in_context(work_package, send_notifications:) do - copy(attributes, copy_attachments, copy_share_members, send_notifications) + def perform + attributes = params.except(:send_notifications, :copy_attachments, :copy_share_members) + + in_context(work_package, send_notifications: params[:send_notifications]) do + copy(attributes, params[:send_notifications]) end end protected - def copy(attribute_override, copy_attachments, copy_share_members, send_notifications) + def copy(attribute_override, send_notifications) copied = create(work_package, attribute_override, send_notifications) .on_success do |copy_call| remove_author_watcher(copy_call.result) copy_watchers(copy_call.result) - copy_work_package_attachments(copy_call.result) if copy_attachments - copy_share_members(copy_call.result, send_notifications) if copy_share_members + copy_work_package_attachments(copy_call.result) if copy_attachments? + copy_share_members(copy_call.result, send_notifications) if copy_share_members? end copied.state.copied_from_work_package_id = work_package&.id @@ -139,4 +141,12 @@ class WorkPackages::CopyService < BaseServices::BaseCallable .new(user: User.current, contract_class: EmptyContract) .call(attributes) end + + def copy_attachments? + params[:copy_attachments] || params[:copy_attachments].nil? + end + + def copy_share_members? + params[:copy_share_members] || params[:copy_share_members].nil? + end end diff --git a/app/services/work_packages/create_service.rb b/app/services/work_packages/create_service.rb index 8f48fecb4ab..40dc7080e6a 100644 --- a/app/services/work_packages/create_service.rb +++ b/app/services/work_packages/create_service.rb @@ -40,8 +40,11 @@ class WorkPackages::CreateService < BaseServices::BaseCallable @contract_class = contract_class end - def perform(work_package: WorkPackage.new, send_notifications: nil, **attributes) - in_user_context(send_notifications:) do + def perform + attributes = params.except(:send_notifications, :work_package) + work_package = params[:work_package] || WorkPackage.new + + in_user_context(send_notifications: params[:send_notifications]) do create(attributes, work_package) end end diff --git a/app/services/work_packages/schedule_dependency.rb b/app/services/work_packages/schedule_dependency.rb index 76f291a83a0..bac24732f6c 100644 --- a/app/services/work_packages/schedule_dependency.rb +++ b/app/services/work_packages/schedule_dependency.rb @@ -102,13 +102,21 @@ class WorkPackages::ScheduleDependency def automatically_scheduled_ancestors(work_package) @automatically_scheduled_ancestors ||= {} @automatically_scheduled_ancestors[work_package] ||= begin - parent = parent_of(work_package) + work_packages_to_process = [work_package] + result = [] + processed_ids = Set.new - if parent&.schedule_automatically? - [parent, *automatically_scheduled_ancestors(parent)] - else - [] + while current = work_packages_to_process.shift + processed_ids.add(current.id) + + parent = parent_of(current) + + if parent&.schedule_automatically? + result << parent unless parent.id == work_package.id + work_packages_to_process << parent unless processed_ids.include?(parent.id) + end end + result end end diff --git a/app/services/work_packages/update_ancestors_service.rb b/app/services/work_packages/update_ancestors_service.rb index 1e49a1200e7..5c1769d8a85 100644 --- a/app/services/work_packages/update_ancestors_service.rb +++ b/app/services/work_packages/update_ancestors_service.rb @@ -58,7 +58,7 @@ class WorkPackages::UpdateAncestorsService < BaseServices::BaseCallable end def ancestors(work_packages) - work_packages.reject { initiator?(_1) } + work_packages.reject { initiator?(it) } end def update_current_and_former_ancestors(attributes) @@ -73,7 +73,7 @@ class WorkPackages::UpdateAncestorsService < BaseServices::BaseCallable end def save_updated_work_packages(updated_work_packages) - updated_initiators, updated_ancestors = updated_work_packages.partition { initiator?(_1) } + updated_initiators, updated_ancestors = updated_work_packages.partition { initiator?(it) } # Send notifications for initiator updates success = updated_initiators.all? { |wp| wp.save(validate: false) } diff --git a/app/views/admin/settings/project_custom_fields/index.html.erb b/app/views/admin/settings/project_custom_fields/index.html.erb index e8e72ebd3b5..526cf875437 100644 --- a/app/views/admin/settings/project_custom_fields/index.html.erb +++ b/app/views/admin/settings/project_custom_fields/index.html.erb @@ -29,7 +29,7 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_administration), t("settings.project_attributes.heading") %>