Merge branch 'dev' into merge-release/15.5-20250416082255

This commit is contained in:
Klaus Zanders
2025-04-16 10:27:27 +02:00
committed by GitHub
1234 changed files with 15502 additions and 12248 deletions
+3
View File
@@ -14,6 +14,9 @@ updates:
fullcalendar:
patterns:
- '@fullcalendar*'
ignore:
- dependency-name: "@angular*"
update-types: ["version-update:semver-major"]
- package-ecosystem: "bundler"
directory: "/"
schedule:
+6 -6
View File
@@ -83,7 +83,7 @@ gem "htmldiff"
gem "stringex", "~> 2.8.5"
# CommonMark markdown parser with GFM extension
gem "commonmarker", "~> 2.2.0"
gem "commonmarker", "~> 2.3.0"
# HTML pipeline for transformations on text formatter output
# such as sanitization or additional features
@@ -201,7 +201,7 @@ gem "aws-sdk-core", "~> 3.107"
# File upload via fog + screenshots on travis
gem "aws-sdk-s3", "~> 1.91"
gem "openproject-token", "~> 5.0"
gem "openproject-token", "~> 5.2.0"
gem "plaintext", "~> 0.3.2"
@@ -340,7 +340,7 @@ group :development, :test do
# https://github.com/puma/puma/issues/2835#issuecomment-2302133927
gem "byebug"
gem "pry-byebug", "~> 3.10.0", platforms: [:mri]
gem "pry-byebug", "~> 3.11.0", platforms: [:mri]
gem "pry-rails", "~> 0.3.6"
gem "pry-rescue", "~> 1.6.0"
@@ -410,6 +410,6 @@ gemfiles.each do |file|
send(:eval_gemfile, file) if File.readable?(file)
end
gem "openproject-octicons", "~>19.23.0"
gem "openproject-octicons_helper", "~>19.23.0"
gem "openproject-primer_view_components", "~>0.59.2"
gem "openproject-octicons", "~>19.24.2"
gem "openproject-octicons_helper", "~>19.24.2"
gem "openproject-primer_view_components", "~>0.61.1"
+83 -83
View File
@@ -352,7 +352,7 @@ GEM
awesome_nested_set (3.8.0)
activerecord (>= 4.0.0, < 8.1)
aws-eventstream (1.3.2)
aws-partitions (1.1078.0)
aws-partitions (1.1082.0)
aws-sdk-core (3.222.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
@@ -372,12 +372,12 @@ GEM
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
axe-core-api (4.10.2)
axe-core-api (4.10.3)
dumb_delegator
ostruct
virtus
axe-core-rspec (4.10.2)
axe-core-api (= 4.10.2)
axe-core-rspec (4.10.3)
axe-core-api (= 4.10.3)
dumb_delegator
ostruct
virtus
@@ -399,11 +399,11 @@ GEM
bindata (2.5.0)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.0)
brakeman (7.0.2)
racc
browser (6.2.0)
builder (3.3.0)
byebug (11.1.3)
byebug (12.0.0)
capybara (3.40.0)
addressable
matrix
@@ -437,14 +437,14 @@ GEM
descendants_tracker (~> 0.0.1)
color_conversion (0.1.2)
colored2 (4.0.3)
commonmarker (2.2.0)
commonmarker (2.3.0)
rb_sys (~> 0.9)
commonmarker (2.2.0-aarch64-linux)
commonmarker (2.2.0-aarch64-linux-musl)
commonmarker (2.2.0-arm64-darwin)
commonmarker (2.2.0-x86_64-darwin)
commonmarker (2.2.0-x86_64-linux)
commonmarker (2.2.0-x86_64-linux-musl)
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)
compare-xml (0.66)
nokogiri (~> 1.8)
concurrent-ruby (1.3.4)
@@ -459,7 +459,7 @@ GEM
crass (1.0.6)
css_parser (1.21.1)
addressable
csv (3.3.3)
csv (3.3.4)
cuprite (0.15.1)
capybara (~> 3.0)
ferrum (~> 0.15.0)
@@ -478,11 +478,11 @@ GEM
disposable (0.6.3)
declarative (>= 0.0.9, < 1.0.0)
representable (>= 3.1.1, < 4)
doorkeeper (5.8.1)
doorkeeper (5.8.2)
railties (>= 5)
dotenv (3.1.7)
dotenv-rails (3.1.7)
dotenv (= 3.1.7)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.1)
dry-auto_inject (1.1.0)
@@ -566,7 +566,7 @@ GEM
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faraday (2.12.2)
faraday (2.13.0)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -636,7 +636,7 @@ GEM
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-gmail_v1 (0.41.0)
google-apis-gmail_v1 (0.42.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-env (2.2.2)
base64 (~> 0.2)
@@ -673,7 +673,7 @@ GEM
htmldiff (0.0.1)
htmlentities (4.3.4)
http-2 (1.0.2)
http_parser.rb (0.6.0)
http_parser.rb (0.8.0)
httpclient (2.9.0)
mutex_m
httpx (1.3.4)
@@ -701,7 +701,7 @@ GEM
ice_nine (0.11.2)
interception (0.5)
io-console (0.8.0)
irb (1.15.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
@@ -734,7 +734,7 @@ GEM
addressable (~> 2.8)
childprocess (~> 5.0)
logger (~> 1.6)
lefthook (1.11.6)
lefthook (1.11.10)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (3.0.0)
@@ -786,7 +786,7 @@ GEM
mime-types (3.6.2)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2025.0325)
mime-types-data (3.2025.0402)
mini_magick (5.2.0)
benchmark
logger
@@ -853,17 +853,17 @@ GEM
validate_email
validate_url
webfinger (~> 2.0)
openproject-octicons (19.23.0)
openproject-octicons_helper (19.23.0)
openproject-octicons (19.24.2)
openproject-octicons_helper (19.24.2)
actionview
openproject-octicons (= 19.23.0)
openproject-octicons (= 19.24.2)
railties
openproject-primer_view_components (0.59.2)
openproject-primer_view_components (0.61.1)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
openproject-octicons (>= 19.23.0)
view_component (>= 3.1, < 4.0)
openproject-token (5.1.0)
openproject-token (5.2.0)
activemodel
openssl (3.3.0)
openssl-signature_algorithm (1.3.0)
@@ -905,12 +905,12 @@ GEM
prawn (>= 1.3.0, < 3.0.0)
prettyprint (0.2.0)
prism (1.4.0)
pry (0.14.2)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
pry-byebug (3.11.0)
byebug (~> 12.0)
pry (>= 0.13, < 0.16)
pry-rails (0.3.11)
pry (>= 0.13.0)
pry-rescue (1.6.0)
@@ -920,13 +920,13 @@ GEM
date
stringio
public_suffix (6.0.1)
puffing-billy (4.0.0)
puffing-billy (4.0.1)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
em-synchrony
eventmachine (~> 1.2)
eventmachine_httpserver
http_parser.rb (~> 0.6.0)
http_parser.rb (~> 0.8.0)
multi_json
puma (6.6.0)
nio4r (~> 2.0)
@@ -1020,7 +1020,7 @@ GEM
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.0)
reline (0.6.1)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -1063,7 +1063,7 @@ GEM
rspec-support (3.13.2)
rspec-wait (1.0.1)
rspec (>= 3.4)
rubocop (1.75.1)
rubocop (1.75.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -1071,10 +1071,10 @@ GEM
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.43.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.43.0)
rubocop-ast (1.44.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -1124,9 +1124,9 @@ GEM
nokogiri (>= 1.16.8)
secure_headers (7.1.0)
securerandom (0.4.1)
selenium-devtools (0.134.0)
selenium-devtools (0.135.0)
selenium-webdriver (~> 4.2)
selenium-webdriver (4.30.1)
selenium-webdriver (4.31.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -1215,7 +1215,7 @@ GEM
public_suffix
vcr (6.3.1)
base64
vernier (1.6.0)
vernier (1.7.0)
view_component (3.22.0)
activesupport (>= 5.2.0, < 8.1)
concurrent-ruby (= 1.3.4)
@@ -1305,7 +1305,7 @@ DEPENDENCIES
climate_control
closure_tree (~> 7.4.0)
colored2
commonmarker (~> 2.2.0)
commonmarker (~> 2.3.0)
compare-xml (~> 0.66)
costs!
csv (~> 3.3)
@@ -1388,15 +1388,15 @@ DEPENDENCIES
openproject-job_status!
openproject-ldap_groups!
openproject-meeting!
openproject-octicons (~> 19.23.0)
openproject-octicons_helper (~> 19.23.0)
openproject-octicons (~> 19.24.2)
openproject-octicons_helper (~> 19.24.2)
openproject-openid_connect!
openproject-primer_view_components (~> 0.59.2)
openproject-primer_view_components (~> 0.61.1)
openproject-recaptcha!
openproject-reporting!
openproject-storages!
openproject-team_planner!
openproject-token (~> 5.0)
openproject-token (~> 5.2.0)
openproject-two_factor_authentication!
openproject-webhooks!
openproject-xls_export!
@@ -1408,7 +1408,7 @@ DEPENDENCIES
pg (~> 1.5.0)
plaintext (~> 0.3.2)
prawn (~> 2.4)
pry-byebug (~> 3.10.0)
pry-byebug (~> 3.11.0)
pry-rails (~> 0.3.6)
pry-rescue (~> 1.6.0)
puffing-billy (~> 4.0.0)
@@ -1519,14 +1519,14 @@ CHECKSUMS
auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0
awesome_nested_set (3.8.0) sha256=469daff411d80291dbb80d1973133e498048a7afc2519c545f62d2cdebc60eda
aws-eventstream (1.3.2) sha256=7e2c3a55ca70d7861d5d3c98e47daa463ed539c349caba22b48305b8919572d7
aws-partitions (1.1078.0) sha256=970e0fe975dd4c03e516e20778fcfb8704f595eb6d48eb4aa3ec1b80ce2339b2
aws-partitions (1.1082.0) sha256=b77347af71e71cd457e227997e53635078b4fc8b14961ab606685a385dd7fa8c
aws-sdk-core (3.222.1) sha256=349f39840fca4300384bd6dc85a546a0ad9def81d48d1f99607a6b3ec27bcbb2
aws-sdk-kms (1.99.0) sha256=ba292fc3ffd672532aae2601fe55ff424eee78da8e23c23ba6ce4037138275a8
aws-sdk-s3 (1.183.0) sha256=8c06b0330c76fc57b4a04a94aec25a8474160b05b2436334d9f8e57ea2799f4c
aws-sdk-sns (1.97.0) sha256=e0489eb11cc107496738c2b39628842df970f4534bdc81a2254d2b49f7d0b7f6
aws-sigv4 (1.11.0) sha256=50a8796991a862324442036ad7a395920572b84bb6cd29b945e5e1800e8da1db
axe-core-api (4.10.2) sha256=c84ea0a041380a329f38fcfbc32b3759d35b481a27a368471e93afba5aafb289
axe-core-rspec (4.10.2) sha256=e302be5807e40eaa170f45559b2067791dfc42a58cb4400d2d6217321f12f775
axe-core-api (4.10.3) sha256=6e10f3ed1c031804f16e8154d9d5dc658564d10850cee860e125fe665c3f0148
axe-core-rspec (4.10.3) sha256=ca21d0111e2d0fcd0f1da922c9071337336732aa6a3a8dc21bed94c9a701527e
axiom-types (0.1.1) sha256=c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383
base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507
bcrypt (3.1.20) sha256=8410f8c7b3ed54a3c00cd2456bf13917d695117f033218e2483b2e40b0784099
@@ -1535,11 +1535,11 @@ CHECKSUMS
bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc
bindata (2.5.0) sha256=29dccb8ba1cc9de148f24bb88930840c62db56715f0f80eccadd624d9f3d2623
bootsnap (1.18.4) sha256=ac4c42af397f7ee15521820198daeff545e4c360d2772c601fbdc2c07d92af55
brakeman (7.0.0) sha256=1a0122b0c70f17519a61548a53a332c0acc19e3aa10b445e15e025a4b13b8577
brakeman (7.0.2) sha256=b602d91bcec6c5ce4d4bc9e081e01f621c304b7a69f227d1e58784135f333786
browser (6.2.0) sha256=281d5295788825c9396427c292c2d2be0a5c91875c93c390fde6e5d61a5ace2d
budgets (1.0.0)
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
byebug (11.1.3) sha256=2485944d2bb21283c593d562f9ae1019bf80002143cc3a255aaffd4e9cf4a35b
byebug (12.0.0) sha256=d4a150d291cca40b66ec9ca31f754e93fed8aa266a17335f71bb0afa7fca1a1e
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
capybara-screenshot (1.0.26) sha256=816b9370a07752097c82a05f568aaf5d3b7f45c3db5d3aab2014071e1b3c0c77
capybara_accessible_selectors (0.11.0)
@@ -1554,13 +1554,13 @@ CHECKSUMS
coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc
color_conversion (0.1.2) sha256=99bea5fa412e1527a11389975aa6ad445ff8528ebae202c11d08c45ea2b94c96
colored2 (4.0.3) sha256=63e1038183976287efc43034f5cca17fb180b4deef207da8ba78d051cbce2b37
commonmarker (2.2.0) sha256=046408c0a2dfe316dc507162f42d9cd9715c1e9b7e8b92b910256479c4c51a21
commonmarker (2.2.0-aarch64-linux) sha256=f1013b73101553f0c1ebfb9d2241a8167fd54b01d7da3f6892c60f2ef6a92cef
commonmarker (2.2.0-aarch64-linux-musl) sha256=4dc4a117a0ba6fd4fece377bc005ea61fc649d50eece68f93c014d06f3114d1c
commonmarker (2.2.0-arm64-darwin) sha256=35a20f17966141e4cc75c3ed86820a4ab7314996c6eb2ab8c0a9ad6c0dbd9d85
commonmarker (2.2.0-x86_64-darwin) sha256=77e21d1e89531289c8c59fcbac71940419bc5385fefeaaa36badd4cdbb6580b9
commonmarker (2.2.0-x86_64-linux) sha256=4bba87191d533a7099595f9f8fe5f9ccda283090f117c6fdec8a452f275f851f
commonmarker (2.2.0-x86_64-linux-musl) sha256=80a87ccb7d147dc57d0dd5550a5868602b1914d7cd13c8c23e5c5794ce460736
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
compare-xml (0.66) sha256=e21aa5c0f69ef1177eced997c688fd4df989084e74a1b612257af32e1dd05319
concurrent-ruby (1.3.4) sha256=d4aa926339b0a86b5b5054a0a8c580163e6f5dcbdfd0f4bb916b1a2570731c32
connection_pool (2.5.0) sha256=233b92f8d38e038c1349ccea65dd3772727d669d6d2e71f9897c8bf5cd53ebfc
@@ -1570,7 +1570,7 @@ CHECKSUMS
crack (1.0.0) sha256=c83aefdb428cdc7b66c7f287e488c796f055c0839e6e545fec2c7047743c4a49
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
css_parser (1.21.1) sha256=6cfd3ffc0a97333b39d2b1b49c95397b05e0e3b684d68f77ec471ba4ec2ef7c7
csv (3.3.3) sha256=7e2966befb7bdaf7d5e9b36e1de73e6a5e7a72f584f180a1726aec88a1b0a900
csv (3.3.4) sha256=e96ecd5a8c3494aa5b596282249daba5c6033203c199248e6146e36d2a78d8cd
cuprite (0.15.1) sha256=0b3a25b2e408b345dfa0bae8bc5e7f021f913737c746edece9ca9fbb57eba259
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
dalli (3.2.8) sha256=2e63595084d91fae2655514a02c5d4fc0f16c0799893794abe23bf628bebaaa5
@@ -1582,9 +1582,9 @@ CHECKSUMS
descendants_tracker (0.0.4) sha256=e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897
diff-lcs (1.6.1) sha256=12a5a83f3e37a8e2f4427268e305914d5f1879f22b4e73bb1a09f76a3dd86cd4
disposable (0.6.3) sha256=7f2a3fb251bff6cd83f25b164043d4ec3531209b51b066ed476a9df9c2d384cc
doorkeeper (5.8.1) sha256=6d54f3c36755d8cfcb7e4f04fbcf1ff3492c816090ad78126ec8a722c292d26c
dotenv (3.1.7) sha256=c670df478675d23889e657beaca6fb423228f75ce9f052a0690c0d0daa333cf3
dotenv-rails (3.1.7) sha256=02c07b9601ad64046e196f48f6280c2cc660b073e08b30ad5c2f78b4872e9189
doorkeeper (5.8.2) sha256=a73d07aeaf590b1e7e2a35390446f23131c9f37bc0561653e514d3973f4d50d3
dotenv (3.1.8) sha256=9e1176060ced581f8e6ce4384e91361817763a76e3c625c8bddc18b35bd392c3
dotenv-rails (3.1.8) sha256=46c9d1226a8b58a83b5f61325aa8cffd25cea1c0fafdfbbbee1e5dfea77980c4
drb (2.2.1) sha256=e9d472bf785f558b96b25358bae115646da0dbfd45107ad858b0bc0d935cb340
dry-auto_inject (1.1.0) sha256=f9276cb5d15a3ef138e1f1149e289e287f636de57ef4a6decd233542eb708f78
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
@@ -1613,7 +1613,7 @@ CHECKSUMS
excon (1.2.5) sha256=ca040bb61bc0059968f34a17115a00d2db8562e3c0c5c5c7432072b551c85a9d
factory_bot (6.5.1) sha256=40581ea7bec0aee05514b8f4f99ed477274bdf1884c1372de5209e60322d6ca9
factory_bot_rails (6.4.4) sha256=139e17caa2c50f098fddf5e5e1f29e8067352024e91ca1186d018b36589e5c88
faraday (2.12.2) sha256=157339c25c7b8bcb739f5cf1207cb0cefe8fa1c65027266bcbc34c90c84b9ad6
faraday (2.13.0) sha256=f2697cd61a434dc446ee035f0370de654c2ad64707c4fc2541eb2338702e9614
faraday-follow_redirects (0.3.0) sha256=d92d975635e2c7fe525dd494fcd4b9bb7f0a4a0ec0d5f4c15c729530fdb807f9
faraday-net_http (3.4.0) sha256=a1f1e4cd6a2cf21599c8221595e27582d9936819977bbd4089a601f24c64e54a
fastimage (2.3.1) sha256=23c629f1f3e7d61bcfcc06c25b3d2418bc6bf41d2e615dbf5132c0e3b63ecce9
@@ -1644,7 +1644,7 @@ CHECKSUMS
gon (6.4.0) sha256=e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0
good_job (3.99.1) sha256=7d3869d8a8ee8ef7048fee5d746f41c21987b7822c20038a2f773036bef0830a
google-apis-core (0.16.0) sha256=046a2c30a5ec123b2a6bc5e64348be781ce5fcd18dd4e85982e7a6a8da9d0dcc
google-apis-gmail_v1 (0.41.0) sha256=d09102619a571213e9637efb9ca4504c54d7779b524344045e24088eb2d0f8d0
google-apis-gmail_v1 (0.42.0) sha256=74b26b486fdb5f3212f54d1c24e98043be912079572729135473500c2f478cd5
google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7
google-logging-utils (0.1.0) sha256=70950b1e49314273cf2e167adb47b62af7917a4691b580da7e9be67b9205fcd5
googleauth (1.14.0) sha256=62e7de11791890c3d3dc70582dfd9ab5516530e4e4f56d96451fd62c76475149
@@ -1662,7 +1662,7 @@ CHECKSUMS
htmldiff (0.0.1) sha256=a96d068c8b8ea96cca3154a61785365d83129f47f1df964c6b1666924ce113a1
htmlentities (4.3.4) sha256=125a73c6c9f2d1b62100b7c3c401e3624441b663762afa7fe428476435a673da
http-2 (1.0.2) sha256=607004ffec28fcb5c392cccaef22d9ffe5b5575fc859ab665fb426a14cf3a83c
http_parser.rb (0.6.0) sha256=f11d0aec50ef26a7d1f991e627ac88acdb5979282aeba7a5c3be6ce0636ed196
http_parser.rb (0.8.0) sha256=5a0932f1fa82ce08a8516a2685d5a86031c000560f89946913c555a0697544be
httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8
httpx (1.3.4) sha256=d90bac25b909184479af1bd95e66b0a336a11d76076342caae5809ffd7d63799
i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f
@@ -1673,7 +1673,7 @@ CHECKSUMS
ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db
interception (0.5) sha256=a53818d636752a8df90d8c1bb2f7b6e13a7b828543cb02b50fbde98b849d7907
io-console (0.8.0) sha256=cd6a9facbc69871d69b2cb8b926fc6ea7ef06f06e505e81a64f14a470fddefa2
irb (1.15.1) sha256=d9bca745ac4207a8b728a52b98b766ca909b86ff1a504bcde3d6f8c84faae890
irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba
iso8601 (0.13.0) sha256=298c2b15b7be5fa95a1372813d36a2257656cd8e906dfbc1f5cb409851425aa2
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
json (2.10.2) sha256=34e0eada93022b2a0a3345bb0b5efddb6e9ff5be7c48e409cfb54ff8a36a8b06
@@ -1685,7 +1685,7 @@ CHECKSUMS
ladle (1.0.1) sha256=e8586964108c798d48bf57d2a65bd5602e8e5223a176b6602a0fb36c0bda90dc
language_server-protocol (3.17.0.4) sha256=c484626478664fd13482d8180947c50a8590484b1258b99b7aedb3b69df89669
launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e
lefthook (1.11.6) sha256=e5df4ea92dc1a6229200b9cc5d77f0ee323603d35b5c03ad61ffec51af62f44b
lefthook (1.11.10) sha256=bd99932e48e46469049a58734d46d6d94fceeb78d92edb3f0e0e29f5b269ad58
letter_opener (1.10.0) sha256=2ff33f2e3b5c3c26d1959be54b395c086ca6d44826e8bf41a14ff96fdf1bdbb2
letter_opener_web (3.0.0) sha256=3f391efe0e8b9b24becfab5537dfb17a5cf5eb532038f947daab58cb4b749860
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -1704,7 +1704,7 @@ CHECKSUMS
meta-tags (2.22.1) sha256=e5ae1febbd320d396c7226d7edb868e5d63466c14b9c8b06622a1a74e6dce354
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
mime-types (3.6.2) sha256=6109148e6a6e656607510b74571deff8ecd9a97ab0dcec9b7431bdd0b74460af
mime-types-data (3.2025.0325) sha256=8557e0e43b0b3216c2a518290039c1b65ffdbd6639db241142f7459eeba3c668
mime-types-data (3.2025.0402) sha256=455cc502e76bf4260d9f4fc8484c6324a9d765c4917a6672cb5a13f7818719c0
mini_magick (5.2.0) sha256=2757ffbfdb1d38242d1da9ff1505360ab75d59dc02eb7ab79ff6d5acb1243f4a
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
mini_portile2 (2.8.8) sha256=8e47136cdac04ce81750bb6c09733b37895bf06962554e4b4056d78168d70a75
@@ -1754,15 +1754,15 @@ CHECKSUMS
openproject-job_status (1.0.0)
openproject-ldap_groups (1.0.0)
openproject-meeting (1.0.0)
openproject-octicons (19.23.0) sha256=bf69988c440b4aef249b2cb30e7c4139370e3b5d0c6e77ef2323eb0f48a37c13
openproject-octicons_helper (19.23.0) sha256=d501eb62e323e326ce56eb9e4ea0d499dfca8ec5c382843c78025a529a0a8156
openproject-octicons (19.24.2) sha256=c4d63f4c16796c51e2a43cefae29e6042aa5cd418ea1bc11015add12e25c841e
openproject-octicons_helper (19.24.2) sha256=b7c8e7f4a22f60c8ac9ae70535e92a90121f28d58391aa21053bf751efc1c576
openproject-openid_connect (1.0.0)
openproject-primer_view_components (0.59.2) sha256=7830dc1c678a08d6474e7b492a7836946b7379798b6dd22e09d69e7a34ed7f0f
openproject-primer_view_components (0.61.1) sha256=c7a7c264fd125a32f6e8d8fadaf97f4ed1eee30d809f8caf7150f67ad4ed72b2
openproject-recaptcha (1.0.0)
openproject-reporting (1.0.0)
openproject-storages (1.0.0)
openproject-team_planner (1.0.0)
openproject-token (5.1.0) sha256=5f2b358fb81431811cfde21d30bd21d635094210f415216fc4d81922adc3d456
openproject-token (5.2.0) sha256=dd30482ea94897f24b7dc19d1f1cdd5a149e76635e29bf2ca34d31cf82f9d394
openproject-two_factor_authentication (1.0.0)
openproject-webhooks (1.0.0)
openproject-xls_export (1.0.0)
@@ -1787,13 +1787,13 @@ CHECKSUMS
prawn-table (0.2.2) sha256=336d46e39e003f77bf973337a958af6a68300b941c85cb22288872dc2b36addb
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.4.0) sha256=dc0e3e00e93160213dc2a65519d9002a4a1e7b962db57d444cf1a71565bb703e
pry (0.14.2) sha256=c4fe54efedaca1d351280b45b8849af363184696fcac1c72e0415f9bdac4334d
pry-byebug (3.10.1) sha256=c8f975c32255bfdb29e151f5532130be64ff3d0042dc858d0907e849125581f8
pry (0.15.2) sha256=12d54b8640d3fa29c9211dd4ffb08f3fd8bf7a4fd9b5a73ce5b59c8709385b6b
pry-byebug (3.11.0) sha256=0b0abb7d309bc7f00044d512a3c8567274f7012b944b38becc8440439a1cea72
pry-rails (0.3.11) sha256=a69e28e24a34d75d1f60bcf241192a54253f8f7ef8a62cba1e75750a9653593d
pry-rescue (1.6.0) sha256=985bfd506d9866b587fd86790cf8445266a41b7f92c627fc5b21ec7d92aba6db
psych (5.2.3) sha256=84a54bb952d14604fea22d99938348814678782f58b12648fcdfa4d2fce859ee
public_suffix (6.0.1) sha256=61d44e1cab5cbbbe5b31068481cf16976dd0dc1b6b07bd95617ef8c5e3e00c6f
puffing-billy (4.0.0) sha256=0453380ad9a66d455675148ff7ab95a7544b6a239f0a91e11494affa94b7fff9
puffing-billy (4.0.1) sha256=d23ea4b2fd54d6d213a85c4a1f603459d9b1634d93ac012dab59d36f380e778a
puma (6.6.0) sha256=f25c06873eb3d5de5f0a4ebc783acc81a4ccfe580c760cfe323497798018ad87
puma-plugin-statsd (2.6.0) sha256=c13ffe6a2fa2d3c245af673316e6d2556f5d0c151455d1934dffc96ce814ea03
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
@@ -1829,7 +1829,7 @@ CHECKSUMS
redis (5.4.0) sha256=798900d869418a9fc3977f916578375b45c38247a556b61d58cba6bb02f7d06b
redis-client (0.24.0) sha256=ee65ee39cb2c38608b734566167fd912384f3c1241f59075e22858f23a085dbb
regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61
reline (0.6.0) sha256=57620375dcbe56ec09bac7192bfb7460c716bbf0054dc94345ecaa5438e539d2
reline (0.6.1) sha256=1afcc9d7cb1029cdbe780d72f2f09251ce46d3780050f3ec39c3ccc6b60675fb
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
request_store (1.7.0) sha256=e1b75d5346a315f452242a68c937ef8e48b215b9453a77a6c0acdca2934c88cb
responders (3.1.1) sha256=92f2a87e09028347368639cfb468f5fefa745cb0dc2377ef060db1cdd79a341a
@@ -1847,8 +1847,8 @@ CHECKSUMS
rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858
rspec-support (3.13.2) sha256=cea3a2463fd9b84b9dcc9685efd80ea701aa8f7b3decb3b3ce795ed67737dbec
rspec-wait (1.0.1) sha256=50ce9cc7cd20b8bb67b95a4ed56b59a16d4af7e86ae91b28dbf20eeaa2481d1f
rubocop (1.75.1) sha256=c12900c55b0b52e6ed1384f7f7575beb92047019ce37ca14b9572d80239adc29
rubocop-ast (1.43.0) sha256=92cd649e336ce10212cb2f2b29028f487777ecc477f108f437a1dce1ee3db79a
rubocop (1.75.2) sha256=8efde647e278417e8074421b007e0d7d7c591482ef99d980528b18fea015a7c8
rubocop-ast (1.44.0) sha256=77fdaf4aacf0c7d1ef887b075686124fb4ae5743d42ff7e73bb1c0d6b61b5d0a
rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c
rubocop-factory_bot (2.27.1) sha256=9d744b5916778c1848e5fe6777cc69855bd96548853554ec239ba9961b8573fe
rubocop-openproject (0.2.0) sha256=2300ed6b638a34a9368b458dc5fb618ae396da6a27670b50519ad1e3cea65ccb
@@ -1869,8 +1869,8 @@ CHECKSUMS
sanitize (7.0.0) sha256=269d1b9d7326e69307723af5643ec032ff86ad616e72a3b36d301ac75a273984
secure_headers (7.1.0) sha256=6b1f9d5f9507af2948f4636452c41c09371927836396c2185438ffdf0a731124
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-devtools (0.134.0) sha256=ad53fc638dc72be8d5606c479de65744b5eaefdd83206edd3ddafc88ef1bc029
selenium-webdriver (4.30.1) sha256=c498fef823a44bf2b110b46edaee3e86aeeb21f8b4e7e12088b4995af4a216f7
selenium-devtools (0.135.0) sha256=3862ec1f3e940a030f492b3691d11af35544789f961fcd0633cb89e4ea4251ec
selenium-webdriver (4.31.0) sha256=ddb2d88eee23cddb5d6a9dadd909427a9e5163718338e11a91eef2fbead100e9
semantic (1.6.1) sha256=3cdbb48f59198ebb782a3fdfb87b559e0822a311610db153bae22777a7d0c163
shoulda-context (2.0.0) sha256=7adf45342cd800f507d2a053658cb1cce2884b616b26004d39684b912ea32c34
shoulda-matchers (6.4.0) sha256=9055bb7f4bb342125fb860809798855c630e05ef5e75837b3168b8e6ee1608b0
@@ -1917,7 +1917,7 @@ CHECKSUMS
validate_email (0.1.6) sha256=9dfe9016d527b17a8d3a6e95e4dc50a125400eef899d13d4cc2a254393f82ee4
validate_url (1.0.15) sha256=72fe164c0713d63a9970bd6700bea948babbfbdcec392f2342b6704042f57451
vcr (6.3.1) sha256=37b56e157e720446a3f4d2d39919cabef8cb7b6c45936acffd2ef8229fec03ed
vernier (1.6.0) sha256=dc9b489f720e25881dc5cd1e12ac237a980dc78919477aa8f1e02d8725987346
vernier (1.7.0) sha256=b64c3e0cf1ddf1842e0afca8b09b1664d391b22f0ebfcc924cc2fbb2536231d9
view_component (3.22.0) sha256=70e350abeb77a8710d94b335e68f79bc51da5d1640bc486c5b9bb8b589f0f2c5
virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
+2
View File
@@ -20,6 +20,7 @@
@import "open_project/common/submenu_component"
@import "filter/filters_component"
@import "projects/row_component"
@import "projects/phases/hover_card_component"
@import "op_primer/border_box_table_component"
@import "op_primer/form_helpers"
@import "work_packages/exports/modal_dialog_component"
@@ -27,4 +28,5 @@
@import "work_package_relations_tab/relation_component"
@import "users/hover_card_component"
@import "enterprise_edition/banner_component"
@import "enterprise_edition/upsale_page_component"
@import "work_packages/types/pattern_input"
+1 -1
View File
@@ -1,5 +1,5 @@
<%= render(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
aria: { label: aria_label },
title:,
@@ -48,8 +48,8 @@ See COPYRIGHT and LICENSE files for more details.
concat(
render(Primer::Alpha::Dialog::Footer.new(show_divider: false)) do
concat(render(Primer::ButtonComponent.new(data: { "close-dialog-id": dialog_id })) { cancel_button_text })
concat(render(Primer::ButtonComponent.new(scheme: :primary, type: :submit)) { submit_button_text })
concat(render(Primer::Beta::Button.new(data: { "close-dialog-id": dialog_id })) { cancel_button_text })
concat(render(Primer::Beta::Button.new(scheme: :primary, type: :submit)) { submit_button_text })
end
)
end
@@ -1,22 +1,49 @@
<%=
grid_layout("op-ee-banner", **@system_arguments) do |grid|
grid.with_area(:"icon-container") do
content_tag :div, class: "op-ee-banner--shield" do
render(
Primer::Beta::Octicon.new(
icon: "op-enterprise-addons",
size: :medium,
classes: "op-ee-banner--icon"
)
)
component_wrapper(tag: "turbo-frame") 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
end
grid.with_area(:"title-container") { render(Primer::Beta::Text.new) { title } }
grid.with_area(:"description-container") { render(Primer::Beta::Text.new) { description } }
grid.with_area(:"link-container") do
render(Primer::Beta::Link.new(href: href)) do |link|
link.with_trailing_visual_icon(icon: "link-external")
link_title
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(mt: 1) do
concat render(Primer::Beta::Text.new) { description }
concat " "
concat render(Primer::Beta::Text.new) { plan_text }
end
if features.present?
flex.with_row do
content_tag(:ul) { safe_join features.map { |text| render(Primer::Beta::Text.new(tag: :li)) { text } } }
end
end
flex.with_row(mt: 2) do
render EnterpriseEdition::UpsaleButtonsComponent.new(feature_key)
end
end
end
if @dismissable
grid.with_area(:dismiss) do
render(
Primer::Beta::IconButton.new(
classes: "op-enterprise-banner--close-icon",
scheme: :invisible,
tag: :a,
href: dismiss_enterprise_banner_path(feature_key: @dismiss_key),
data: { turbo_stream: true, turbo_method: :post },
icon: :x,
aria: { label: t("ee.upsale.hide_banner") }
)
)
end
end
end
end
@@ -34,75 +34,51 @@ module EnterpriseEdition
# It will only be rendered if necessary.
class BannerComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include PlanForFeature
# @param feature_key [Symbol, NilClass] The key of the feature to show the banner for.
# @param title [String] The title of the banner.
# @param description [String] The description of the banner.
# @param href [String] The URL to link to.
# @param skip_render [Boolean] Whether to skip rendering the banner.
# @param i18n_scope [String] Provide the i18n scope to look for title, description, and features.
# Defaults to "ee.upsale.{feature_key}"
# @param dismissable [boolean] Allow this banner to be dismissed.
# @param dismiss_key [String] Provide a string to identify this banner when being dismissed. Defaults to feature_key
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(feature_key,
title: nil,
description: nil,
link_title: nil,
href: nil,
skip_render: !EnterpriseToken.show_banners?,
i18n_scope: "ee.upsale.#{feature_key}",
dismissable: false,
dismiss_key: feature_key,
**system_arguments)
@system_arguments = system_arguments
@system_arguments[:tag] = "div"
@system_arguments[:test_selector] = "op-ee-banner-#{feature_key.to_s.tr('_', '-')}"
@system_arguments[:tag] = :div
@system_arguments[:mb] ||= 2
@system_arguments[:id] = "op-enterprise-banner-#{feature_key.to_s.tr('_', '-')}"
@system_arguments[:test_selector] = "op-enterprise-banner"
super
@feature_key = feature_key
@title = title
@description = description
@link_title = link_title
@href = href
@skip_render = skip_render
self.feature_key = feature_key
self.i18n_scope = i18n_scope
@dismissable = dismissable
@dismiss_key = dismiss_key
end
def wrapper_key
"enterprise_banner_#{feature_key}"
end
private
attr_reader :skip_render,
:feature_key
def title
@title || I18n.t("ee.upsale.#{feature_key}.title", default: I18n.t("ee.upsale.title"))
end
def description
@description || begin
I18n.t("ee.upsale.#{feature_key}.description")
rescue StandardError
I18n.t("ee.upsale.#{feature_key}.description_html")
end
rescue I18n::MissingTranslationData => e
raise e.exception(
<<~TEXT.squish
The expected '#{I18n.locale}.ee.upsale.#{feature_key}.description' key does not exist.
Ideally, provide it in the locale file.
If that isn't applicable, a description parameter needs to be provided.
TEXT
)
end
def link_title
@link_title || I18n.t("ee.upsale.#{feature_key}.link_title", default: I18n.t("ee.upsale.link_title"))
end
def href
href_value = @href || OpenProject::Static::Links.links.dig(:enterprise_docs, feature_key, :href)
unless href_value
raise "Neither a custom href is provided nor is a value set " \
"in OpenProject::Static::Links.enterprise_docs[#{feature_key}][:href]"
end
href_value
end
def render?
!skip_render
!(EnterpriseToken.hide_banners? || feature_available? || dismissed?)
end
def feature_available?
EnterpriseToken.allows_to?(feature_key)
end
def dismissed?
return false unless @dismissable
User.current.pref.dismissed_banner?(@dismiss_key)
end
end
end
@@ -28,41 +28,32 @@
/ ++
/
$op-ee-banner--shield-width: 32px
// This is not named op-enterprise-banner because as of now, there is still a legacy angular component that uses that block name.
.op-ee-banner
.op-enterprise-banner
display: grid
grid-template-columns: $op-ee-banner--shield-width auto auto
grid-template-areas: "icon-container title-container" "icon-container description-container" "icon-container link-container"
grid-column-gap: 0.5rem
justify-content: left
@media screen and (min-width: $breakpoint-md)
grid-template-areas: "icon-container title-container title-container" "icon-container description-container link-container"
grid-auto-flow: column
grid-template-areas: "visual content dismiss"
grid-template-columns: min-content 1fr min-content
grid-template-rows: min-content
border: var(--borderWidth-thin) solid var(--borderColor-severe-muted)
border-radius: var(--borderRadius-medium)
background: var(--body-background)
padding: var(--base-size-8)
&--icon-container
@extend .upsale-colored
&--visual
display: grid
align-self: start
justify-self: center
&--shield
@extend .upsale-border-colored
width: $op-ee-banner--shield-width
height: 42px
border-width: 10px 5px 10px 5px
border-radius: 0 0 10px 10px
border-style: solid
display: flex
align-items: center
justify-content: center
padding: var(--base-size-6) var(--base-size-8)
&--icon
width: $op-ee-banner--shield-width
height: $op-ee-banner--shield-width
color: var(--enterprise-upsale-color)
margin-block: var(--base-size-2)
&--title-container
@extend .upsale-colored
font-weight: bold
&--close-icon .Button-visual
color: var(--enterprise-upsale-color)
&--link-container
align-self: end
&--content
padding: var(--base-size-6) var(--base-size-8)
&--dismiss
margin-left: var(--controlStack-medium-gap-condensed)
@@ -0,0 +1,84 @@
# 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
module PlanForFeature
extend ActiveSupport::Concern
included do
attr_accessor :feature_key
attr_accessor :i18n_scope
end
def title
I18n.t(:title, scope: i18n_scope, default: default_title)
end
def default_title
I18n.t(feature_key, scope: :"ee.features")
end
def description
@description || begin
if I18n.exists?(:description_html, scope: i18n_scope)
I18n.t(:description_html, scope: i18n_scope).html_safe
else
I18n.t(:description, scope: i18n_scope)
end
end
rescue I18n::MissingTranslationData => e
raise e.exception(
<<~TEXT.squish
The expected '#{I18n.locale}.#{i18n_scope}.description' nor '#{I18n.locale}.#{i18n_scope}.description_html' key does not exist.
Ideally, provide it in the locale file.
If that isn't applicable, a description parameter needs to be provided.
TEXT
)
end
def features
return @features if defined?(@features)
@features = I18n.t(:features, scope: i18n_scope, default: nil)&.values
end
def plan
@plan ||= OpenProject::Token.lowest_plan_for(feature_key)&.capitalize
end
def plan_text
plan_name = render(Primer::Beta::Text.new(font_weight: :bold, classes: "upsale-colored")) do
I18n.t("ee.upsale.plan_name", plan:)
end
I18n.t("ee.upsale.plan_text_html", plan_name:).html_safe
end
end
end
@@ -0,0 +1,96 @@
# 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 UpsaleButtonsComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
# @param feature_key [Symbol, NilClass] The key of the feature to show the banner for.
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(feature_key, **system_arguments)
super
@system_arguments = system_arguments
@system_arguments[:align_items] ||= :center
@feature_key = feature_key
end
def call
flex_layout(**@system_arguments) do |flex|
buttons.each_with_index do |button, i|
flex.with_column(ml: (i == 0 ? 0 : 2)) do
button
end
end
end
end
private
attr_reader :feature_key
def buttons
[
free_trial_button,
upgrade_now_button,
more_info_button
].compact
end
def free_trial_button
return if EnterpriseToken.active?
helpers.angular_component_tag("opce-free-trial-button")
end
# Allow providing a custom upgrade now button
def upgrade_now_button
nil
end
def more_info_button
render(Primer::Beta::Link.new(href: enterprise_link)) do |link|
link.with_trailing_visual_icon(icon: "link-external")
link_title
end
end
def link_title
I18n.t("ee.upsale.#{feature_key}.link_title", default: I18n.t("ee.upsale.link_title"))
end
def enterprise_link
href_value = OpenProject::Static::Links.links.dig(:enterprise_features, feature_key, :href)
default_value = OpenProject::Static::Links.links.dig(:enterprise_features, :default, :href)
href_value || default_value
end
end
end
@@ -0,0 +1,46 @@
<% if respond_to?(:gon) %>
<% helpers.write_augur_to_gon %>
<% helpers.write_trial_key_to_gon if !EnterpriseToken.current.present? %>
<% end %>
<div class="op-enterprise-upsale-page" data-test-selector="op-enterprise-upsale-page">
<%=
render(Primer::OpenProject::FlexLayout.new(justify_content: :center, align_items: :center)) do |flex|
flex.with_column do
render(Primer::Beta::Octicon.new(icon: :"op-enterprise-addons",
size: :medium,
classes: "upsale-colored"))
end
flex.with_column(ml: 2) do
render(Primer::Beta::Heading.new(tag: :h2)) { title }
end
end
%>
<p class="upsale--feature-reference">
<%= render(Primer::Beta::Text.new) { description } %>
<%= render(Primer::Beta::Text.new) { plan_text } %>
</p>
<% if more_info_text.present? %>
<%= render(Primer::Beta::Text.new(tag: :p, color: :muted)) { more_info_text } %>
<% end %>
<% if features.present? %>
<ul>
<% features.each do |text| %>
<li><%= text %></li>
<% end %>
</ul>
<% end %>
<% if @video.present? %>
<%= video_tag @video, controls: false, class: "widget-box--teaser-video", autoplay: true, loop: true, muted: true %>
<% elsif @image.present? %>
<%= image_tag @image, class: "widget-box--teaser-image widget-box--teaser-image_default" %>
<% else %>
<%= image_tag "enterprise-add-on.svg", class: "widget-box--teaser-image widget-box--teaser-image_default" %>
<% end %>
<%= render EnterpriseEdition::UpsaleButtonsComponent.new(feature_key, justify_content: :center) %>
</div>
@@ -1,4 +1,6 @@
#-- copyright
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
@@ -24,25 +26,36 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# ++
module ProjectCustomFields
class SidePanelComponent < ApplicationComponent
include ApplicationHelper
module EnterpriseEdition
# A full page banner for the enterprise edition
class UpsalePageComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include PlanForFeature
def initialize(project:)
# @param feature_key [Symbol, NilClass] The key of the feature to show the upsale page for.
# @param i18n_scope [String] Provide the i18n scope to look for title, description, and features.
# Defaults to "ee.upsale.{feature_key}"
# @param image [String, NilClass] Path to the image to show on the upsale page, or nil.
# @param video [String, NilClass] Path to the video to show on the upsale page, or nil.
def initialize(feature_key, i18n_scope: "ee.upsale.#{feature_key}", image: nil, video: nil)
super
@project = project
self.feature_key = feature_key
self.i18n_scope = i18n_scope
@image = image
@video = video
end
def more_info_text
I18n.t(:more_info, scope: i18n_scope, default: nil)
end
private
def available_project_custom_fields_grouped_by_section
@available_project_custom_fields_grouped_by_section ||=
@project.available_custom_fields.group_by(&:project_custom_field_section)
def render?
!EnterpriseToken.hide_banners?
end
end
end
@@ -0,0 +1,56 @@
/*!
/ -- 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.
/ ++
/
.op-enterprise-upsale-page
max-width: 50vw
margin: auto
padding-top: 20px
text-align: center
.widget-box--teaser-image,
.widget-box--teaser-video
width: 100%
height: auto
margin-bottom: 20px
box-shadow: var(--shadow-floating-small)
border-radius: 25px
border: 1px solid var(--borderColor-default)
.widget-box--teaser-image_default
width: 30%
margin-bottom: 20px
box-shadow: 4px 4px 10px rgb(0 0 0 / 15%)
@media screen and (max-width: $breakpoint-md)
max-width: none
.widget-box--teaser-image,
.widget-box--teaser-video
width: 90%
border-radius: 15px
+5 -7
View File
@@ -43,17 +43,16 @@ module Filter
# Returns filters, active and inactive.
# In case a filter is active, the active one will be preferred over the inactive one.
def each_filter
allowed_filters.each do |filter|
active_filter = query.find_active_filter(filter.name)
additional_attributes = additional_filter_attributes(filter)
allowed_filters.each do |allowed_filter|
active_filter = query.find_active_filter(allowed_filter.name)
filter = active_filter || allowed_filter
yield active_filter.presence || filter, active_filter.present?, additional_attributes
yield filter, active_filter.present?, additional_filter_attributes(filter)
end
end
def allowed_filters
query
.available_advanced_filters
query.available_advanced_filters
end
def value_hidden_class(selected_operator)
@@ -99,7 +98,6 @@ module Filter
else
{ items: filter.allowed_values.map { |name, id| { name:, id: } } }
end
autocomplete_options.merge(options).merge(model: filter.values)
end
@@ -62,11 +62,11 @@ See COPYRIGHT and LICENSE files for more details.
collection.with_component(Primer::Alpha::Dialog::Footer.new) do
component_collection do |footer|
footer.with_component(Primer::ButtonComponent.new(data: { "close-dialog-id": "new-access-token-dialog" })) do
footer.with_component(Primer::Beta::Button.new(data: { "close-dialog-id": "new-access-token-dialog" })) do
I18n.t("button_cancel")
end
footer.with_component(Primer::ButtonComponent.new(scheme: :primary, type: :submit, test_selector: "create-api-token-button")) do
footer.with_component(Primer::Beta::Button.new(scheme: :primary, type: :submit, test_selector: "create-api-token-button")) do
I18n.t("my.access_token.new_access_token_dialog_submit_button_text")
end
end
@@ -50,9 +50,9 @@
<% end %>
<% end %>
<%= render(Primer::Alpha::Dialog::Footer.new) do %>
<%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(Primer::Beta::Button.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
type: :submit,
data: { "test-selector": "#{MODAL_ID}-submit" },
@@ -1,8 +0,0 @@
<%= flex_layout(align_items: :center) do |type_container|
type_container.with_column(mr: 1, classes: icon_color_class) do
render Primer::Beta::Octicon.new(icon: icon)
end
type_container.with_column do
render(Primer::Beta::Text.new(**text_options)) { text }
end
end %>
@@ -1,7 +1,10 @@
<%=
flex_layout(align_items: :center, classes: "gap-2") do |type_container|
if display_start_gate?
type_container.with_column(classes: icon_color_class) do
type_container.with_column(
classes: icon_color_class,
data: hover_card_data_args(gate: :start)
) do
render Primer::Beta::Octicon.new(icon: gate_icon)
end
end
@@ -15,7 +18,10 @@
render(Primer::Beta::Text.new) { finish_date }
end
if display_finish_gate?
type_container.with_column(classes: icon_color_class) do
type_container.with_column(
classes: icon_color_class,
data: hover_card_data_args(gate: :finish)
) do
render Primer::Beta::Octicon.new(icon: gate_icon)
end
end
+8 -5
View File
@@ -30,6 +30,7 @@
module Projects
class PhaseComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include Projects::Phases::Shared
def initialize(phase:, **)
@phase = phase
@@ -61,12 +62,14 @@ module Projects
phase.finish_gate? && phase.finish_date.present?
end
def gate_icon
:"op-gate"
end
def hover_card_data_args(gate:)
raise ArgumentError, "gate must be either :start or :finish" unless %i[start finish].include?(gate)
def icon_color_class
helpers.hl_inline_class("project_phase_definition", phase.definition_id)
{
hover_card_trigger_target: "trigger",
hover_card_url: hover_card_project_phase_path(phase, gate:),
test_selector: "phase-#{phase.id}-#{gate}-gate"
}
end
private
@@ -0,0 +1,15 @@
<%= flex_layout(align_items: :center) do |type_container|
type_container.with_column(classes: icon_color_class, mr: 1) do
render Primer::Beta::Octicon.new(icon: icon)
end
type_container.with_column(test_selector: "project-life-cycle-step-definition-name", classes: "ellipsis") do
if edit_link?
render(Primer::Beta::Link.new(href: phase_href, **phase_text_options)) { phase_text }
else
render(Primer::Beta::Text.new(**phase_text_options)) { phase_text }
end
end
type_container.with_column(ml: 2, classes: "no-wrap") do
render(Primer::Beta::Text.new(**gates_text_options)) { gates_text }
end
end %>
@@ -28,14 +28,18 @@
# See COPYRIGHT and LICENSE files for more details.
# ++
module Projects
class LifeCycleComponent < ApplicationComponent
class PhaseDefinitionComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
# TODO: This component is currently not in use! It should be a shared component
# between the Projects::Settings::LifeCycleSteps::StepComponent and the
# Settings::ProjectLifeCycleStepDefinitions::RowComponent.
# It should hold the icon, definition name and gate text information.
def text
def phase_text
model.name
end
def phase_href
edit_admin_settings_project_phase_definition_path(model)
end
def gates_text
if model.start_gate? && model.finish_gate?
I18n.t("settings.project_phase_definitions.both_gate")
elsif model.start_gate?
@@ -52,14 +56,22 @@ module Projects
end
def icon_color_class
helpers.hl_inline_class("project_phase_definition", model)
helpers.hl_inline_class("project_phase_definition", model.id)
end
def text_options
def gates_text_options
# The tag: :div is is a hack to fix the line height difference
# caused by font_size: :small. That line height difference
# would otherwise lead to the text being not on the same height as the icon
{ color: :muted, font_size: :small, tag: :div }.merge(options)
{ color: :muted, font_size: :small, tag: :div }.merge(options[:gates_text_options] || {})
end
def phase_text_options
{ font_weight: :bold }.merge(options[:phase_text_options] || {})
end
def edit_link?
options[:edit_link]
end
end
end
@@ -27,12 +27,32 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title "#{t(:label_meeting_edit)}: #{@meeting.title}" %>
<%=
flex_layout(classes: "op-phase-gate-hover-card", data: { test_selector: "phase-gate-hover-card-#{phase.id}" }) do |flex|
flex.with_row do
flex_layout(classes: "op-phase-gate-hover-card--info", justify_content: :space_between) do |f|
f.with_column(classes: "op-phase-gate-hover-card--name flex-self-start") do
flex_layout do |fl|
fl.with_column(classes: icon_color_class, mr: 1) do
render Primer::Beta::Octicon.new(icon: gate_icon)
end
fl.with_column do
render(Primer::Beta::Text.new(data: { test_selector: "phase-gate-hover-card-name" })) { phase_gate_name }
end
end
end
<%= toolbar title: "#{t(:label_meeting)} ##{@meeting.id}" %>
<%= labelled_tabular_form_for @meeting, url: { controller: "/meetings", action: "update" }, html: { id: "meeting-form", method: :put } do |f| -%>
<%= render partial: "form", locals: { f: f } %>
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
<%= link_to t(:button_cancel), { action: "show", id: @meeting },
class: "button -with-icon icon-cancel" %>
<% end if @project %>
f.with_column(classes: "op-phase-gate-hover-card--date flex-self-end") do
flex_layout do |fl|
fl.with_column(mr: 1) do
render Primer::Beta::Octicon.new(icon: :calendar, color: :muted)
end
fl.with_column do
render(Primer::Beta::Text.new(data: { test_selector: "phase-gate-hover-card-date" })) { phase_gate_date }
end
end
end
end
end
end
%>
@@ -0,0 +1,69 @@
# 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 Projects
module Phases
class HoverCardComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include Projects::Phases::Shared
attr_reader :phase
def initialize(phase:, gate:)
raise ArgumentError, "gate must be either 'start' or 'finish'" unless %w[start finish].include?(gate)
super
@phase = phase
@gate = gate.to_sym
end
def phase_gate_name
case @gate
when :start
@phase.start_gate? ? @phase.start_gate_name : nil
else
@phase.finish_gate? ? @phase.finish_gate_name : nil
end
end
def phase_gate_date
date = case @gate
when :start
@phase.start_date
else
@phase.finish_date
end
helpers.format_date(date)
end
end
end
end
@@ -0,0 +1,6 @@
.op-hover-card:has(.op-phase-gate-hover-card)
max-width: 380px
.op-phase-gate-hover-card
&--name
@include text-shortener()
@@ -1,4 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -27,10 +28,13 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module API
module V3
module MeetingMinutes
class MeetingMinutesRepresenter < API::V3::MeetingContents::MeetingContentRepresenter
module Projects
module Phases
module Shared
def gate_icon = :"op-gate"
def icon_color_class
helpers.hl_inline_class("project_phase_definition", phase.definition_id)
end
end
end
@@ -3,16 +3,14 @@
align_items: :center,
justify_content: :space_between
) do |step_container|
step_container.with_column(flex_layout: true, mr: 2, classes: "min-width-0") do |title_container|
title_container.with_column(pt: 1, mr: 1, classes: icon_color_class) do
render Primer::Beta::Octicon.new(icon: icon)
end
title_container.with_column(pt: 1, mr: 2, classes: "ellipsis") do
render(Primer::Beta::Text.new(classes: "filter-target-visible-text")) { definition.name }
end
title_container.with_column(pt: 1, classes: "no-wrap") do
render(Primer::Beta::Text.new(**gate_text_options)) { gate_info }
end
step_container.with_column(mr: 2, classes: "min-width-0") do
render(
Projects::PhaseDefinitionComponent.new(
definition,
edit_link: User.current.admin?,
phase_text_options: { classes: "filter-target-visible-text" }
)
)
end
# py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling
step_container.with_column(py: 1, mr: 2) do
@@ -37,32 +37,6 @@ module Projects
options :definition,
:active?
# TODO: Remove these helper methods once the Projects::LifeCycleComponent
# has been refactored.
def icon
:"op-phase"
end
def icon_color_class
helpers.hl_inline_class("project_phase_definition", definition)
end
def gate_info
if definition.start_gate? && definition.finish_gate?
I18n.t("settings.project_phase_definitions.both_gate")
elsif definition.start_gate?
I18n.t("settings.project_phase_definitions.start_gate")
elsif definition.finish_gate?
I18n.t("settings.project_phase_definitions.finish_gate")
else
I18n.t("settings.project_phase_definitions.no_gate")
end
end
def gate_text_options
{ color: :muted, font_size: :small }.merge(options)
end
def toggle_aria_label
I18n.t("projects.settings.life_cycle.step.use_in_project", step: definition.name)
end
@@ -12,11 +12,11 @@
collection.with_component(Primer::Alpha::Dialog::Footer.new) do
component_collection do |modal_footer|
modal_footer.with_component(Primer::ButtonComponent.new(data: { "close-dialog-id": "project-custom-field-section-dialog#{@project_custom_field_section.id}" })) do
modal_footer.with_component(Primer::Beta::Button.new(data: { "close-dialog-id": "project-custom-field-section-dialog#{@project_custom_field_section.id}" })) do
t("button_cancel")
end
modal_footer.with_component(Primer::ButtonComponent.new(scheme: :primary, type: :submit)) do
modal_footer.with_component(Primer::Beta::Button.new(scheme: :primary, type: :submit)) do
t("button_save")
end
end
@@ -39,28 +39,14 @@ See COPYRIGHT and LICENSE files for more details.
)
end
end
title_container.with_column(classes: icon_color_class) do
render Primer::Beta::Octicon.new(icon: icon)
end
title_container.with_column(classes: "ellipsis", test_selector: "project-life-cycle-step-definition-name") do
render(
if allowed_to_customize_life_cycle?
Primer::Beta::Link.new(
classes: "filter-target-visible-text",
href: edit_admin_settings_project_phase_definition_path(definition),
font_weight: :bold
)
else
Primer::Beta::Text.new(
font_weight: :bold
)
end
) do
definition.name
end
end
title_container.with_column do
render(Primer::Beta::Text.new(**gate_text_options)) { gate_info }
render(
Projects::PhaseDefinitionComponent.new(
definition,
edit_link: allowed_to_customize_life_cycle?,
phase_text_options: { classes: "filter-target-visible-text" }
)
)
end
title_container.with_column(classes: "no-wrap") do
render(Primer::Beta::Text.new) { t("project.count", count: definition.project_count) }
@@ -39,32 +39,6 @@ module Settings
options :first?,
:last?
# TODO: Remove these helper classes once the Projects::LifeCycleComponent
# has been refactored.
def icon
:"op-phase"
end
def icon_color_class
helpers.hl_inline_class("project_phase_definition", definition)
end
def gate_info
if definition.start_gate? && definition.finish_gate?
I18n.t("settings.project_phase_definitions.both_gate")
elsif definition.start_gate?
I18n.t("settings.project_phase_definitions.start_gate")
elsif definition.finish_gate?
I18n.t("settings.project_phase_definitions.finish_gate")
else
I18n.t("settings.project_phase_definitions.no_gate")
end
end
def gate_text_options
{ color: :muted, font_size: :small }.merge(options)
end
private
def move_action(menu:, move_to:, label:, icon:)
@@ -46,6 +46,9 @@ See COPYRIGHT and LICENSE files for more details.
tab.with_text { t("attributes.custom_values") }
end
end
tab_nav.with_tab(selected: selected?(:activities), href: project_settings_work_packages_activities_path) do |tab|
tab.with_text { t("label_activity") }
end
end
end
%>
@@ -36,6 +36,11 @@ See COPYRIGHT and LICENSE files for more details.
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: selected_tab(@tabs) == tab, href: tab[:path]) do |t|
feature = tab[:enterprise_feature]
if feature && !EnterpriseToken.allows_to?(feature)
t.with_icon(icon: :"op-enterprise-addons", classes: "upsale-colored")
end
t.with_text { I18n.t(tab[:label]) }
end
end
@@ -17,7 +17,7 @@
d.with_footer do
component_collection do |buttons|
buttons.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
data: {
"close-dialog-id": DIALOG_ID
}
@@ -26,7 +26,7 @@
t("button_cancel")
end
buttons.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
form: FORM_ID,
data: { turbo: true },
@@ -62,7 +62,7 @@
end
else
flex.with_row do
render Primer::Beta::Blankslate.new(border: true) do |component|
render Primer::Beta::Blankslate.new(border: true, data: { test_selector: "no-relations-blankslate" }) do |component|
component.with_visual_icon(icon: "package-dependents")
component.with_heading(tag: :h4).with_content(t("#{I18N_NAMESPACE}.index.blankslate_heading"))
component.with_description { t("#{I18N_NAMESPACE}.index.blankslate_description") }
@@ -18,14 +18,14 @@
d.with_footer do
component_collection do |buttons|
buttons.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
data: { "close-dialog-id": DIALOG_ID }
)
) do
t(:button_cancel)
end
buttons.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
form: FORM_ID,
data: { turbo: true },
@@ -80,7 +80,8 @@ module WorkPackages
action: index_stimulus_controller(":onSubmit-end@window->#{restricted_comment_stimulus_controller}#onSubmitEnd"),
restricted_comment_stimulus_controller("-highlight-class") => "work-packages-activities-tab-journals-new-component--journal-notes-body__restricted-comment", # rubocop:disable Layout/LineLength
restricted_comment_stimulus_controller("-hidden-class") => "d-none",
restricted_comment_stimulus_controller("-#{index_stimulus_controller}-outlet") => "##{wrapper_key}"
restricted_comment_stimulus_controller("-#{index_stimulus_controller}-outlet") => "##{wrapper_key}",
restricted_comment_stimulus_controller("-is-restricted-value") => false # Initial value
}
end
@@ -128,7 +128,7 @@ module WorkPackages
end
def edit_action_item(menu)
menu.with_item(label: t("js.label_edit_comment"),
menu.with_item(label: edit_action_label,
href: edit_work_package_activity_path(journal.journable, journal, filter:),
content_arguments: {
data: { turbo_stream: true, test_selector: "op-wp-journal-#{journal.id}-edit" }
@@ -137,6 +137,14 @@ module WorkPackages
end
end
def edit_action_label
if journal.user == User.current
t("js.label_edit_comment")
else
t("js.label_moderate_comment")
end
end
def quote_action_item(menu)
menu.with_item(label: t("js.label_quote_comment"),
tag: :button,
@@ -12,14 +12,14 @@
icon: "smiley",
"aria-label": I18n.t("reactions.add_reaction"),
name: I18n.t("reactions.add_reaction"),
mr: 2,
mr: 1,
test_selector: "add-reactions-button"
)
overlay.with_body(pt: 2, test_selector: "emoji-reactions-overlay") do
flex_layout(flex_wrap: :wrap, classes: "op-add-reactions-overlay") do |add_reactions_container|
EmojiReaction.available_emoji_reactions.each do |emoji, reaction|
add_reactions_container.with_column(mr: 2) do
add_reactions_container.with_column(mr: 1) do
render(
Primer::Beta::Button.new(
scheme: button_scheme(reaction),
@@ -34,7 +34,9 @@
turbo_method: :put,
"work-packages--activities-tab--index-target": "reactionButton"
},
aria: { label: I18n.t("reactions.react_with", reaction: reaction.to_s.humanize(capitalize: false)) }
aria: { label: I18n.t("reactions.react_with", reaction: reaction.to_s.humanize(capitalize: false)) },
font_size: 4,
classes: "op-add-reactions-button"
)
) do
emoji
@@ -2,3 +2,7 @@
row-gap: var(--base-size-4, 4px)
@media screen and (max-width: $breakpoint-sm)
max-width: 200px
.op-add-reactions-button
width: var(--control-medium-size)
padding: unset
@@ -1,5 +1,5 @@
<%=
component_wrapper(class: "work-packages-activities-tab-journals-item-component-edit") do
component_wrapper(class: "work-packages-activities-tab-journals-item-component-edit", data: wrapper_data_attributes) do
render(Primer::Box.new(my: 3)) do
primer_form_with(
id: "work-package-journal-form-element", # required for specs
@@ -33,6 +33,7 @@ module WorkPackages
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include WorkPackages::ActivitiesTab::StimulusControllers
def initialize(journal:, filter:)
super
@@ -49,6 +50,12 @@ module WorkPackages
def wrapper_uniq_by
journal.id
end
def wrapper_data_attributes
{
restricted_comment_stimulus_controller("-is-restricted-value") => journal.restricted?
}
end
end
end
end
@@ -3,29 +3,32 @@
if grouped_emoji_reactions.present?
flex_layout(test_selector: "emoji-reactions", flex_wrap: :wrap, classes: "op-emoji-reactions--gap") do |reactions_container|
grouped_emoji_reactions.each do |reaction, data|
reactions_container.with_column(mr: 2) do
render(Primer::Beta::Button.new(
scheme: button_scheme(data[:users]),
color: :default,
bg: counter_color(data[:users]),
id: "#{journal.id}-#{reaction}",
test_selector: "reaction-#{reaction}",
tag: :a,
href: href(reaction:),
data: {
turbo_stream: true,
turbo_method: :put,
"work-packages--activities-tab--index-target": "reactionButton"
},
aria: { label: aria_label_text(reaction, data[:users]) },
disabled: current_user_cannot_react?,
classes: "op-reactions-button"
)) do |button|
button.with_tooltip(text: number_of_user_reactions_text(data[:users]),
test_selector: "reaction-tooltip-#{reaction}") do
button.with_icon(EmojiReactions.emoji(reaction), size: :small)
end
"#{EmojiReaction.emoji(reaction)} #{data[:count]}"
reactions_container.with_column(mr: 1) do
render(
Primer::Beta::Button.new(
scheme: button_scheme(data[:users]),
color: :default,
bg: counter_color(data[:users]),
id: "#{journal.id}-#{reaction}",
test_selector: "reaction-#{reaction}",
tag: :a,
href: href(reaction:),
data: {
turbo_stream: true,
turbo_method: :put,
"work-packages--activities-tab--index-target": "reactionButton"
},
aria: { label: aria_label_text(reaction, data[:users]) },
disabled: current_user_cannot_react?,
classes: "op-reactions-button",
px: 2
)
) do |button|
button.with_tooltip(
text: number_of_user_reactions_text(data[:users]),
test_selector: "reaction-tooltip-#{reaction}"
)
render(Primer::Beta::Text.new(font_size: 4)) { EmojiReaction.emoji(reaction) } + " #{data[:count]}"
end
end
end
@@ -60,6 +60,7 @@ module WorkPackages
def adding_restricted_comment_allowed?
OpenProject::FeatureDecisions.comments_with_restricted_visibility_active? &&
work_package.project.enabled_comments_with_restricted_visibility &&
User.current.allowed_in_project?(:add_comments_with_restricted_visibility, work_package.project)
end
@@ -4,7 +4,7 @@
flex_layout do |start_date|
start_date.with_row(classes: container_classes(:start_date)) do
render(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
tag: :a,
href: single_date_field_button_link("start_date"),
data: { turbo_frame: "wp-datepicker-dialog--content", morph: true },
@@ -31,7 +31,7 @@
flex_layout do |due_date|
due_date.with_row(classes: container_classes(:due_date)) do
render(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
tag: :a,
href: single_date_field_button_link("due_date"),
data: { turbo_frame: "wp-datepicker-dialog--content", morph: true },
@@ -76,7 +76,7 @@
collection.with_component(Primer::Alpha::Dialog::Footer.new) do
component_collection do |footer|
footer.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
data: { action: "work-packages--date-picker--preview#cancel" },
test_selector: "op-datepicker-modal--action"
)
@@ -85,7 +85,7 @@
end
footer.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
type: :submit,
form: DIALOG_FORM_ID,
@@ -1,12 +1,14 @@
<%=
render(Primer::Alpha::Dialog.new(
id: "create-work-package-dialog",
title: I18n.t(:label_work_package_new),
size: :xlarge,
data: {
'keep-open-on-submit': true,
}
)) do |dialog|
render(
Primer::Alpha::Dialog.new(
id: "create-work-package-dialog",
title: I18n.t(:label_work_package_new),
size: :xlarge,
data: {
"keep-open-on-submit": true
}
)
) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
render(WorkPackages::Dialogs::CreateFormComponent.new(work_package:, project:))
@@ -15,18 +17,20 @@
dialog.with_footer do
component_collection do |modal_footer|
modal_footer.with_component(
Primer::ButtonComponent.new(
data: { 'close-dialog-id': "create-work-package-dialog" }
)) do
Primer::Beta::Button.new(
data: { "close-dialog-id": "create-work-package-dialog" }
)
) do
I18n.t(:button_cancel)
end
modal_footer.with_component(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
form: "create-work-package-form",
type: :submit
)) do
)
) do
if @work_package.persisted?
I18n.t(:button_save)
else
@@ -35,12 +35,16 @@ module WorkPackages
include OpTurbo::Streamable
include WorkPackagesHelper
attr_reader :query
def query
model
end
def initialize(query)
super
def format
raise NotImplementedError, "Must be overridden in subclass"
end
@query = query
def export_settings
@export_settings ||= query.export_settings_for(format)
end
end
end
@@ -33,14 +33,15 @@ module WorkPackages
class ColumnSelectionComponent < ApplicationComponent
include WorkPackagesHelper
attr_reader :query, :id, :caption, :label
attr_reader :export_settings, :query, :id, :caption, :label
def initialize(query, id, caption,
def initialize(export_settings, id, caption,
label = I18n.t(:"queries.configure_view.columns.input_label"),
required: true)
super()
@query = query
@export_settings = export_settings
@query = @export_settings.query
@id = id
@caption = caption
@label = label
@@ -59,10 +60,22 @@ module WorkPackages
end
def selected_columns
return columns_from_saved_export_settings if export_settings.settings.key?(:columns)
query
.columns
.map { |column| { id: column.name.to_s, name: column.caption } }
end
private
def columns_from_saved_export_settings
saved_cols = export_settings.settings[:columns]
# Restore the saved columns, retaining the saved order
saved_cols.filter_map do |col|
available_columns.find { |c| c[:id] == col }
end
end
end
end
end
@@ -1,7 +1,7 @@
<%= flex_layout do |container| %>
<%= container.with_row do |_columns| %>
<%= render WorkPackages::Exports::ColumnSelectionComponent.new(
query,
export_settings,
"columns-select-export-csv",
I18n.t("export.dialog.columns.input_caption_table")
) %>
@@ -14,9 +14,13 @@
name: "show_descriptions",
value: "true",
unchecked_value: "false",
checked: export_settings.true?(:show_descriptions),
label: I18n.t("export.dialog.xls.include_descriptions.label"),
caption: I18n.t("export.dialog.xls.include_descriptions.caption"),
visually_hide_label: false
visually_hide_label: false,
data: {
test_selector: "show-descriptions-csv"
}
)
) %>
<% end %>
@@ -32,6 +32,9 @@ module WorkPackages
module Exports
module CSV
class ExportSettingsComponent < BaseExportSettingsComponent
def format
"csv"
end
end
end
end
@@ -130,9 +130,9 @@
end %>
<% end %>
<% dialog.with_footer do %>
<%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(Primer::Beta::Button.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
scheme: :primary,
type: :submit,
form: GENERATE_PDF_FORM_ID,
@@ -51,10 +51,27 @@
<% end %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<% dialog.with_footer(show_divider: true) do %>
<% if offer_to_save_export_settings? %>
<%= render(Primer::BaseComponent.new(tag: :span, mr: :auto)) do %>
<%= render(
Primer::Alpha::CheckBox.new(
id: "#{EXPORT_FORM_ID}-save_export_settings",
name: "save_export_settings",
value: "true",
unchecked_value: "false",
checked: saved_export_settings?,
label: I18n.t("export.dialog.save_export_settings.label"),
data: {
test_selector: "#{EXPORT_FORM_ID}-save-export-settings"
}
)
) %>
<% end %>
<% end %>
<%= render(Primer::Beta::Button.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(
Primer::ButtonComponent.new(
Primer::Beta::Button.new(
data: {
"work-packages--export--dialog-target": "submit"
},
@@ -48,14 +48,23 @@ module WorkPackages
def export_format_url(format)
if @project.nil?
index_work_packages_path(format:)
elsif @query.id.present?
project_work_packages_path(project, query_id: @query.id, format:)
# Global work package list. The query_id might be nil for unsaved queries.
index_work_packages_path(format:, query_id: @query.id)
else
project_work_packages_path(project, format:)
# Project work package list. The query_id might be nil for unsaved queries.
project_work_packages_path(project, query_id: @query.id, format:)
end
end
# Users can save their export settings, but only for saved queries.
def offer_to_save_export_settings?
@query.persisted?
end
def saved_export_settings?
@query.export_settings.any?(&:persisted?)
end
def export_formats_settings
[
{ id: "pdf", icon: :"op-pdf",
@@ -8,9 +8,9 @@
size: :medium,
input_width: :small
)) do |component|
selected = entry[:options].find { |e| e[:default] }[:value]
entry[:options].each do |entry|
component.option(label: entry[:label], value: entry[:value], selected: selected == entry[:value])
selected = find_selected_option(entry)
entry[:options].each do |e|
component.option(label: e[:label], value: e[:value], selected: selected[:value] == e[:value])
end
end %>
<% end %>
@@ -33,6 +33,10 @@ module WorkPackages
module PDF
module Gantt
class ExportSettingsComponent < BaseExportSettingsComponent
def format
"pdf_gantt"
end
def gantt_selects
[
{
@@ -55,6 +59,26 @@ module WorkPackages
]
end
# Reads the saved value from the export settings and returns the selected option.
# When there is no saved value, returns the default option.
# @param [Hash] entry one entry from `gantt_selects`
def find_selected_option(entry)
select_name = entry[:name].to_sym
if export_settings.settings.key?(select_name)
saved_value = export_settings.settings[select_name]
entry[:options].find { |option| option[:value] == saved_value } || find_default_option(entry)
else
find_default_option(entry)
end
end
# Returns the default option for a gantt_select entry.
# @param [Hash] entry one entry from `gantt_selects`
def find_default_option(entry)
entry[:options].find { |option| option[:default] }
end
def gantt_zoom_levels
[
{ label: t("export.dialog.pdf.gantt_zoom_levels.options.days"), value: "day", default: true },
@@ -1,7 +1,7 @@
<%= flex_layout do |container| %>
<% container.with_row do |_columns| %>
<%= render WorkPackages::Exports::ColumnSelectionComponent.new(
query,
export_settings,
"columns-select-export-pdf-report",
I18n.t("export.dialog.columns.input_caption_report"),
I18n.t("export.dialog.columns.input_label_report"),
@@ -31,7 +31,7 @@
<%= render(
Primer::Alpha::CheckBox.new(
name: "show_images",
checked: true,
checked: export_settings.true?(:show_images, default: true),
value: "true",
unchecked_value: "false",
label: I18n.t("export.dialog.pdf.include_images.label"),
@@ -35,13 +35,26 @@ module WorkPackages
class ExportSettingsComponent < BaseExportSettingsComponent
DESCRIPTION_CF = { id: "description", name: WorkPackage.human_attribute_name("description") }.freeze
def format
"pdf_report"
end
def available_long_text_fields
[DESCRIPTION_CF] + WorkPackageCustomField.where(field_format: "text")
.map { |cf| { id: cf.id, name: cf.name } }
end
def selected_long_text_fields
available_long_text_fields
default_long_text_fields = available_long_text_fields
saved_long_text_fields = if export_settings.settings.key?(:long_text_fields)
saved = export_settings.settings.fetch(:long_text_fields, "").split
default_long_text_fields.select do |cf|
saved.include?(cf[:id].to_s)
end
end
saved_long_text_fields || default_long_text_fields
end
def protected_long_text_fields
@@ -1,5 +1,5 @@
<%= render WorkPackages::Exports::ColumnSelectionComponent.new(
query,
export_settings,
"columns-select-export-pdf-table",
I18n.t("export.dialog.columns.input_caption_table")
) %>
@@ -33,6 +33,9 @@ module WorkPackages
module PDF
module Table
class ExportSettingsComponent < BaseExportSettingsComponent
def format
"pdf_table"
end
end
end
end
@@ -1,7 +1,7 @@
<%= flex_layout do |container| %>
<%= container.with_row do |_columns| %>
<%= render WorkPackages::Exports::ColumnSelectionComponent.new(
query,
export_settings,
"columns-select-export-xls",
I18n.t("export.dialog.columns.input_caption_table")
) %>
@@ -14,6 +14,7 @@
name: "show_relations",
value: "true",
unchecked_value: "false",
checked: export_settings.true?(:show_relations),
label: I18n.t("export.dialog.xls.include_relations.label"),
caption: I18n.t("export.dialog.xls.include_relations.caption"),
visually_hide_label: false
@@ -28,6 +29,7 @@
name: "show_descriptions",
value: "true",
unchecked_value: "false",
checked: export_settings.true?(:show_descriptions),
label: I18n.t("export.dialog.xls.include_descriptions.label"),
caption: I18n.t("export.dialog.xls.include_descriptions.caption"),
visually_hide_label: false
@@ -32,6 +32,9 @@ module WorkPackages
module Exports
module XLS
class ExportSettingsComponent < BaseExportSettingsComponent
def format
"xls"
end
end
end
end
@@ -28,7 +28,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(EnterpriseEdition::BannerComponent.new(:automatic_subject_generation, mb: 3))
render(EnterpriseEdition::BannerComponent.new(:work_package_subject_generation))
%>
<%=
@@ -28,47 +28,15 @@
module ProjectLifeCycleSteps
class BaseContract < ::ModelContract
validate :select_custom_fields_permission
validate :consecutive_steps_have_increasing_dates
attribute :start_date
attribute :finish_date
def valid?(context = :saving_phases) = super
validate :validate_edit_project_phase_permission
def select_custom_fields_permission
return if user.allowed_in_project?(:edit_project_phases, model)
def validate_edit_project_phase_permission
return if user.allowed_in_project?(:edit_project_phases, model.project)
errors.add :base, :error_unauthorized
end
def consecutive_steps_have_increasing_dates
# Filter out steps with missing dates before proceeding with comparison
filtered_steps = model.available_phases.select(&:start_date)
# Only proceed with comparisons if there are at least 2 valid steps
return if filtered_steps.size < 2
# Compare consecutive steps in pairs
filtered_steps.each_cons(2) do |previous_step, current_step|
if has_invalid_dates?(previous_step, current_step)
error = current_step.errors.add(:date_range, :non_continuous_dates)
unless model.errors.include?(:"available_phases.date_range")
model.errors.import(error, attribute: :"available_phases.date_range")
end
end
end
end
private
def start_date_for(step)
step.start_date
end
def finish_date_for(step)
step.finish_date || step.start_date # Use the start_date as fallback for single date stages
end
def has_invalid_dates?(previous_step, current_step)
start_date_for(current_step) <= finish_date_for(previous_step)
end
end
end
@@ -162,12 +162,6 @@ module WorkPackages
validate :validate_duration_and_dates_are_not_derivable
def initialize(work_package, user, options: {})
super
@can = WorkPackagePolicy.new(user)
end
def assignable_statuses(include_default: false)
# Do not allow skipping statuses without intermediately saving the work package.
# We therefore take the original status of the work_package, while preserving all
@@ -226,8 +220,6 @@ module WorkPackages
private
attr_reader :can
def validate_after_soonest_start(date_attribute)
return if model.schedule_manually?
return if model.children.any?
@@ -33,26 +33,33 @@ module WorkPackages
def self.model = WorkPackage
attribute :journal_notes do
errors.add(:journal_notes, :error_unauthorized) unless can?(:comment)
errors.add(:journal_notes, :error_unauthorized) unless adding_notes_allowed?
errors.add(:journal_notes, :blank) if model.journal_notes.blank?
end
attribute :journal_restricted do
if model.journal_restricted && !OpenProject::FeatureDecisions.comments_with_restricted_visibility_active?
next unless model.journal_restricted
unless OpenProject::FeatureDecisions.comments_with_restricted_visibility_active?
errors.add(:journal_restricted, :feature_disabled)
end
unless allowed_in_project?(:add_comments_with_restricted_visibility)
errors.add(:journal_restricted, :error_unauthorized)
end
end
private
def can?(permission)
policy.allowed?(model, permission)
def adding_notes_allowed?
allowed_in_work_package?(:add_work_package_notes) || allowed_in_work_package?(:edit_work_packages)
end
attr_writer :policy
def allowed_in_work_package?(permission)
user.allowed_in_work_package?(permission, model)
end
def policy
@policy ||= WorkPackagePolicy.new(user)
def allowed_in_project?(permission)
user.allowed_in_project?(permission, model.project)
end
end
end
+15 -7
View File
@@ -49,18 +49,18 @@ module WorkPackages
attribute_permission :project_id, :move_work_packages
def can_set_parent?
@can.allowed?(model, :manage_subtasks)
allowed_in_project?(:manage_subtasks)
end
private
def user_allowed_to_edit
with_unchanged_project_id do
next if @can.allowed?(model, :edit) ||
@can.allowed?(model, :assign_version) ||
@can.allowed?(model, :change_status) ||
@can.allowed?(model, :manage_subtasks) ||
@can.allowed?(model, :move)
next if allowed_in_work_package?(:edit_work_packages) ||
allowed_in_project?(:assign_versions) ||
allowed_in_project?(:change_work_package_status) ||
allowed_in_project?(:manage_subtasks) ||
allowed_in_project?(:move_work_packages)
next if allowed_journal_addition?
errors.add :base, :error_unauthorized
@@ -74,7 +74,7 @@ module WorkPackages
end
def allowed_journal_addition?
model.changes.empty? && model.journal_notes && can.allowed?(model, :comment)
model.changes.empty? && model.journal_notes && allowed_in_work_package?(:add_work_package_notes)
end
def can_move_to_milestone
@@ -93,5 +93,13 @@ module WorkPackages
errors.add :parent_id, :error_unauthorized
end
end
def allowed_in_project?(permission)
user.allowed_in_project?(permission, model.project)
end
def allowed_in_work_package?(permission)
user.allowed_in_work_package?(permission, model)
end
end
end
@@ -73,8 +73,6 @@ module Admin
def find_attachment
@attachment = @attachments.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
end
@@ -146,8 +146,6 @@ module Admin
def find_model_object
@object = CustomField.hierarchy_root_and_children.find(params[:custom_field_id])
@custom_field = @object
rescue ActiveRecord::RecordNotFound
render_404
end
def find_active_item
@@ -156,8 +154,6 @@ module Admin
else
@object.hierarchy_root
end
rescue ActiveRecord::RecordNotFound
render_404
end
end
end
@@ -212,8 +212,6 @@ module Admin::Settings
def find_custom_field
@custom_field = ProjectCustomField.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def drop_success_streams(call)
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -30,7 +32,7 @@ module Admin::Settings
class VirusScanningSettingsController < ::Admin::SettingsController
menu_item :attachments
before_action :require_ee
before_action :require_ee, except: :show # rubocop:disable Rails/LexicallyScopedActionFilter
before_action :check_clamav, only: %i[update], if: -> { scan_enabled? }
def show_local_breadcrumb
@@ -52,14 +54,16 @@ module Admin::Settings
private
def require_ee
render("upsale") unless EnterpriseToken.allows_to?(:virus_scanning)
return if EnterpriseToken.allows_to?(:virus_scanning)
redirect_to action: :show
end
def mark_unscanned_attachments
@unscanned_attachments = Attachment.status_uploaded
end
def check_clamav
def check_clamav # rubocop:disable Metrics/AbcSize
return if params.dig(:settings, :antivirus_scan_mode) == "disabled"
service = ::Attachments::ClamAVService.new(params[:settings][:antivirus_scan_mode].to_sym,
+4 -14
View File
@@ -133,6 +133,10 @@ class ApplicationController < ActionController::Base
payload: ::OpenProject::Logging::ThreadPoolContextBuilder.build!
end
rescue_from ActiveRecord::RecordNotFound do
render_404
end
before_action :authorization_check_required,
:user_setup,
:set_localization,
@@ -240,16 +244,12 @@ class ApplicationController < ActionController::Base
# Note: find() is Project.friendly.find()
def find_project
@project = Project.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Find project of id params[:project_id]
# Note: find() is Project.friendly.find()
def find_project_by_project_id
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
# Finds and sets @project based on @object.project
@@ -257,8 +257,6 @@ class ApplicationController < ActionController::Base
render_404 if @object.blank?
@project = @object.project
rescue ActiveRecord::RecordNotFound
render_404
end
def find_model_object(object_id = :id)
@@ -267,8 +265,6 @@ class ApplicationController < ActionController::Base
@object = model.find(params[object_id])
instance_variable_set(:"@#{controller_name.singularize}", @object) if @object
end
rescue ActiveRecord::RecordNotFound
render_404
end
def find_model_object_and_project(object_id = :id)
@@ -280,8 +276,6 @@ class ApplicationController < ActionController::Base
else
@project = Project.find(params[:project_id])
end
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: this method is right now only suited for controllers of objects that somehow have an association to Project
@@ -295,8 +289,6 @@ class ApplicationController < ActionController::Base
associated.each do |a|
instance_variable_set("@" + a.class.to_s.downcase, a)
end
rescue ActiveRecord::RecordNotFound
render_404
end
# this method finds all records that are specified in the associations param
@@ -337,8 +329,6 @@ class ApplicationController < ActionController::Base
@projects = @work_packages.filter_map(&:project).uniq
@project = @projects.first if @projects.size == 1
rescue ActiveRecord::RecordNotFound
render_404
end
def back_url
@@ -109,8 +109,6 @@ class AttributeHelpTextsController < ApplicationController
def find_entry
@attribute_help_text = AttributeHelpText.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def find_type_scope
-2
View File
@@ -101,7 +101,5 @@ class CategoriesController < ApplicationController
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -76,8 +76,6 @@ module Accounts::Authorization
@project = Project.find(params[:project_id]) if params[:project_id].present?
do_authorize({ controller: controller_path, action: action_name }, global: params[:project_id].blank?)
rescue ActiveRecord::RecordNotFound
render_404
end
# Deny access if user is not allowed to do the specified action.
@@ -214,8 +212,6 @@ module Accounts::Authorization
before_action(**args) do
@project = Project.find(params[:project_id]) if params[:project_id].present?
do_authorize(permission, global: params[:project_id].blank?)
rescue ActiveRecord::RecordNotFound
render_404
end
end
end
@@ -125,8 +125,6 @@ module CustomFields
def find_custom_option
@custom_option = CustomOption.find params[:option_id]
rescue ActiveRecord::RecordNotFound
render_404
end
def delete_custom_values!(custom_option)
+1 -7
View File
@@ -86,13 +86,7 @@ class CustomActionsController < ApplicationController
return if EnterpriseToken.allows_to?(:custom_actions)
if request.get?
render template: "common/upsale",
locals: {
feature_title: I18n.t("custom_actions.upsale.title"),
feature_description: I18n.t("custom_actions.upsale.description"),
feature_reference: "custom_actions_admin",
feature_video: "enterprise/custom-actions.mp4"
}
render template: "custom_actions/upsale"
else
render_403
end
@@ -75,8 +75,6 @@ class CustomFieldsController < ApplicationController
def find_custom_field
@custom_field = CustomField.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def check_custom_field
-2
View File
@@ -125,8 +125,6 @@ class ForumsController < ApplicationController
def find_forum
@forum = @project.forums.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
def new_forum
-4
View File
@@ -59,8 +59,6 @@ class JournalsController < ApplicationController
journals: @journals }
end
end
rescue ActiveRecord::RecordNotFound
render_404
end
def diff
@@ -87,8 +85,6 @@ class JournalsController < ApplicationController
@journal = Journal.find(params[:id])
@journable = @journal.journable
@project = @journable.project
rescue ActiveRecord::RecordNotFound
render_404
end
def ensure_permitted
@@ -1,4 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -26,46 +27,39 @@
#
# See COPYRIGHT and LICENSE files for more details.
#++
class My::EnterpriseBannersController < ApplicationController
include OpTurbo::ComponentStream
require_relative "../spec_helper"
before_action :require_login
before_action :get_feature_key
RSpec.describe "MeetingAgenda" do
before do
@a = build(:meeting_agenda, text: "Some content...\n\nMore content!\n\nExtraordinary content!!")
end
authorization_checked! :dismiss, :show
# TODO: Test the right user and messages are set in the history
describe "#lock!" do
it "locks the agenda" do
@a.save
@a.reload
@a.lock!
@a.reload
expect(@a.locked).to be_truthy
def show
dismissable = ActiveRecord::Type::Boolean.new.cast(params[:dismissable])
respond_to do |format|
format.html do
render(EnterpriseEdition::BannerComponent.new(@feature_key, dismissable:), layout: false)
end
end
end
describe "#unlock!" do
it "unlocks the agenda" do
@a.locked = true
@a.save
@a.reload
@a.unlock!
@a.reload
expect(@a.locked).to be_falsey
def dismiss
pref = User.current.pref
pref.dismiss_banner(@feature_key)
if pref.save
remove_via_turbo_stream(component: EnterpriseEdition::BannerComponent.new(@feature_key))
respond_with_turbo_streams
else
respond_with_flash_error(message: call.message)
end
end
# a meeting agenda is editable when it is not locked
describe "#editable?" do
it "is editable when not locked" do
@a.locked = false
expect(@a.editable?).to be_truthy
end
private
it "is not editable when locked" do
@a.locked = true
expect(@a.editable?).to be_falsey
end
def get_feature_key
@feature_key = params[:feature_key].to_sym
render_400 unless OpenProject::Token.lowest_plan_for(@feature_key)
end
end
-4
View File
@@ -118,13 +118,9 @@ class NewsController < ApplicationController
def find_news_object
@news = @object = News.find(params[:id].to_i)
rescue ActiveRecord::RecordNotFound
render_404
end
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -33,6 +33,8 @@ class NotificationsController < ApplicationController
before_action :filtered_query, only: :mark_all_read
no_authorization_required! :index, :split_view, :update_counter, :mark_all_read, :date_alerts, :share_upsale
before_action :check_filter, only: %i[index]
def index
render_notifications_layout
end
@@ -93,4 +95,12 @@ class NotificationsController < ApplicationController
@filtered_query = query
end
def check_filter
return if EnterpriseToken.allows_to?(:date_alerts)
if params[:filter] == "reason" && params[:name] == "dateAlert"
redirect_to notifications_date_alert_upsale_path
end
end
end
@@ -115,8 +115,6 @@ module OAuth
def find_app
@application = ::Doorkeeper::Application.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
end
end
@@ -35,8 +35,6 @@ class PlaceholderUsers::MembershipsController < ApplicationController
def find_individual_principal
@individual_principal = PlaceholderUser.find(params[:placeholder_user_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def redirected_to_tab(_membership)
@@ -146,8 +146,6 @@ class PlaceholderUsersController < ApplicationController
def find_placeholder_user
@placeholder_user = PlaceholderUser.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_404
end
protected
@@ -0,0 +1,71 @@
# 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 ProjectPhases
class HoverCardController < ApplicationController
before_action :authorize
before_action :check_feature_flag
before_action :assign_gate
before_action :find_phase
before_action :check_access
layout false
def show; end
private
def check_feature_flag
return if OpenProject::FeatureDecisions.stages_and_gates_active?
render json: { error: "Not found" }, status: :not_found
end
def check_access
return if User.current.allowed_in_project?(:view_project_phases, @phase.project)
render json: { error: "Forbidden" }, status: :forbidden
end
def assign_gate
@gate = params[:gate]
return if @gate.in?(%w[start finish])
render json: { error: "Invalid gate parameter" }, status: :unprocessable_entity
end
def find_phase
@phase = Project::Phase.where(active: true).eager_load(:definition).find_by(id: params[:id])
return if @phase
render json: { error: "Invalid id parameter" }, status: :unprocessable_entity
end
end
end
@@ -1,4 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -27,24 +28,27 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class MeetingAgendasController < MeetingContentsController
menu_item :meetings
class Projects::Settings::WorkPackages::ActivitiesController < Projects::SettingsController
menu_item :settings_work_packages
def close
@meeting.close_agenda_and_copy_to_minutes!
def update
enabled = ActiveRecord::Type::Boolean.new.cast(expected_params[:enabled_comments_with_restricted_visibility])
result = Projects::UpdateService
.new(user: current_user, model: @project, contract_class: Projects::SettingsContract)
.call(enabled_comments_with_restricted_visibility: enabled)
redirect_back_or_default({ controller: "/meetings", action: "show", id: @meeting })
end
if result.success?
flash[:notice] = t("notice_successful_update")
else
flash[:error] = t("notice_unsuccessful_update")
end
def open
@content.unlock!
redirect_back_or_default({ controller: "/meetings", action: "show", id: @meeting })
redirect_to project_settings_work_packages_activities_path
end
private
def find_content
@content = @meeting.agenda || @meeting.build_agenda
@content_type = "meeting_agenda"
def expected_params
params.expect(project: [:enabled_comments_with_restricted_visibility])
end
end
@@ -38,6 +38,8 @@ class Projects::Settings::WorkPackagesController < Projects::SettingsController
redirect_to project_settings_work_packages_categories_path
elsif User.current.allowed_in_project?(:select_custom_fields, @project)
redirect_to project_settings_work_packages_custom_fields_path
elsif User.current.allowed_in_project?(:edit_project, @project)
redirect_to project_settings_work_packages_activities_path
end
end
end
@@ -320,8 +320,6 @@ class RepositoriesController < ApplicationController
end
rescue OpenProject::SCM::Exceptions::SCMEmpty
render "empty"
rescue ActiveRecord::RecordNotFound
render_404
rescue InvalidRevisionParam
show_error_not_found
end
@@ -35,8 +35,6 @@ class Users::MembershipsController < ApplicationController
def find_individual_principal
@individual_principal = User.find(params[:user_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def redirected_to_tab(membership)
-2
View File
@@ -244,8 +244,6 @@ class UsersController < ApplicationController
else
@user = User.find(params[:id])
end
rescue ActiveRecord::RecordNotFound
render_404
end
def authorize_for_user
-2
View File
@@ -136,8 +136,6 @@ class VersionsController < ApplicationController
def find_project
@project = Project.find(params[:project_id])
rescue ActiveRecord::RecordNotFound
render_404
end
def retrieve_selected_type_ids(selectable_types, default_types = nil)
-2
View File
@@ -367,8 +367,6 @@ class WikiController < ApplicationController
@project = Project.find(params[:project_id])
@wiki = @project.wiki
render_404 unless @wiki
rescue ActiveRecord::RecordNotFound
render_404
end
# Finds or created the wiki page associated
@@ -51,7 +51,5 @@ class WorkPackageRelationsTabController < ApplicationController
def set_work_package
@work_package = WorkPackage.find(params[:work_package_id])
@project = @work_package.project # required for authorization via before_action
rescue ActiveRecord::RecordNotFound
render_404
end
end
@@ -139,6 +139,13 @@ class WorkPackages::ActivitiesTabController < ApplicationController
respond_with_turbo_streams
end
def sanitize_restricted_mentions
render plain: sanitized_journal_notes
rescue StandardError => e
handle_internal_server_error(e)
respond_with_turbo_streams
end
def toggle_reaction # rubocop:disable Metrics/AbcSize
emoji_reaction_service =
if @journal.emoji_reactions.exists?(user: User.current, reaction: params[:reaction])
@@ -221,6 +228,10 @@ class WorkPackages::ActivitiesTabController < ApplicationController
User.current.preference&.comments_sorting || OpenProject::Configuration.default_comment_sort_order
end
def sanitized_journal_notes
WorkPackages::ActivitiesTab::RestrictedMentionsSanitizer.sanitize(@work_package, journal_params[:notes])
end
def journal_params
params.expect(journal: %i[notes restricted])
end
@@ -295,12 +306,15 @@ class WorkPackages::ActivitiesTabController < ApplicationController
end
def create_journal_service_call
restricted = to_boolean(journal_params[:restricted], false)
notes = restricted ? sanitized_journal_notes : journal_params[:notes]
AddWorkPackageNoteService
.new(user: User.current,
work_package: @work_package)
.call(journal_params[:notes],
.call(notes,
send_notifications: to_boolean(params[:notify], true),
restricted: to_boolean(journal_params[:restricted], false))
restricted:)
end
def to_boolean(value, default)
@@ -308,14 +322,14 @@ class WorkPackages::ActivitiesTabController < ApplicationController
end
def update_journal_service_call
Journals::UpdateService.new(model: @journal, user: User.current).call(
notes: journal_params[:notes]
)
notes = @journal.restricted? ? sanitized_journal_notes : journal_params[:notes]
Journals::UpdateService.new(model: @journal, user: User.current).call(notes:)
end
def generate_time_based_update_streams(last_update_timestamp)
journals = @work_package
.journals
.restricted_visible
.with_sequence_version
if @filter == :only_comments
+27 -1
View File
@@ -42,7 +42,7 @@ class WorkPackagesController < ApplicationController
:check_allowed_export,
:protect_from_unauthorized_export, only: %i[index export_dialog]
before_action :authorize, only: :show_conflict_flash_message
before_action :authorize, only: %i[show_conflict_flash_message share_upsale]
authorization_checked! :index, :show, :export_dialog, :generate_pdf_dialog, :generate_pdf
before_action :load_and_validate_query, only: :index, unless: -> { request.format.html? }
@@ -128,6 +128,11 @@ class WorkPackagesController < ApplicationController
respond_with_turbo_streams
end
def share_upsale
render :share_upsale,
locals: { menu_name: project_or_global_menu }
end
protected
def load_and_validate_query_for_export
@@ -135,6 +140,8 @@ class WorkPackagesController < ApplicationController
end
def export_list(mime_type)
save_export_settings if params[:save_export_settings]&.to_bool
job_id = WorkPackages::Exports::ScheduleService
.new(user: current_user)
.call(query: @query, mime_type:, params:)
@@ -169,6 +176,24 @@ class WorkPackagesController < ApplicationController
private
def save_export_settings
# Saving export settings is only allowed for saved queries
return false if @query.new_record?
relevant_keys = %i[format columns show_relations show_descriptions long_text_fields
show_images gantt_mode gantt_width paper_size]
user_settings = params.slice(*relevant_keys)
if user_settings[:format] == "pdf"
user_settings[:format] = "pdf_#{params[:pdf_export_type]}"
end
export_settings = @query.export_settings_for(user_settings[:format])
export_settings.settings = user_settings
export_settings.save
end
def authorize_on_work_package
deny_access(not_found: true) unless work_package
end
@@ -201,6 +226,7 @@ class WorkPackagesController < ApplicationController
work_package
.journals
.restricted_visible
.changing
.includes(:user)
.order(order).to_a
+1 -1
View File
@@ -42,7 +42,7 @@ module Projects::LifeCycles
label:,
leading_visual: { icon: :calendar },
datepicker_options: {
inDialog: ProjectLifeCycles::Sections::EditDialogComponent::DIALOG_ID,
inDialog: Overviews::ProjectPhases::EditDialogComponent::DIALOG_ID,
data: { action: "change->overview--project-life-cycles-form#previewForm" }
},
wrapper_data_attributes: {

Some files were not shown because too many files have changed in this diff Show More