diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 252b588506a..4acb564dd27 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,6 +14,9 @@ updates: fullcalendar: patterns: - '@fullcalendar*' + ignore: + - dependency-name: "@angular*" + update-types: ["version-update:semver-major"] - package-ecosystem: "bundler" directory: "/" schedule: diff --git a/Gemfile b/Gemfile index b69b3ada5a8..a06abd0c28c 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index 362d5c26efb..b882050e2cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/frontend/src/assets/videos/notification-center/date-alert-notifications.mp4 b/app/assets/videos/enterprise/date-alert-notifications.mp4 similarity index 100% rename from frontend/src/assets/videos/notification-center/date-alert-notifications.mp4 rename to app/assets/videos/enterprise/date-alert-notifications.mp4 diff --git a/frontend/src/assets/videos/sharing/share-work-package.mp4 b/app/assets/videos/enterprise/share-work-package.mp4 similarity index 100% rename from frontend/src/assets/videos/sharing/share-work-package.mp4 rename to app/assets/videos/enterprise/share-work-package.mp4 diff --git a/app/components/_index.sass b/app/components/_index.sass index 911fab2e6d7..a15d95529d9 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -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" diff --git a/app/components/add_button_component.html.erb b/app/components/add_button_component.html.erb index 88aec27eada..b4903d3eb93 100644 --- a/app/components/add_button_component.html.erb +++ b/app/components/add_button_component.html.erb @@ -1,5 +1,5 @@ <%= render( - Primer::ButtonComponent.new( + Primer::Beta::Button.new( scheme: :primary, aria: { label: aria_label }, title:, diff --git a/app/components/admin/custom_fields/custom_field_projects/new_custom_field_projects_form_modal_component.html.erb b/app/components/admin/custom_fields/custom_field_projects/new_custom_field_projects_form_modal_component.html.erb index f23190f45cd..2b78a95d1af 100644 --- a/app/components/admin/custom_fields/custom_field_projects/new_custom_field_projects_form_modal_component.html.erb +++ b/app/components/admin/custom_fields/custom_field_projects/new_custom_field_projects_form_modal_component.html.erb @@ -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 diff --git a/app/components/enterprise_edition/banner_component.html.erb b/app/components/enterprise_edition/banner_component.html.erb index 06e5ae5862c..9e1464a8c80 100644 --- a/app/components/enterprise_edition/banner_component.html.erb +++ b/app/components/enterprise_edition/banner_component.html.erb @@ -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 diff --git a/app/components/enterprise_edition/banner_component.rb b/app/components/enterprise_edition/banner_component.rb index 757198892bd..0ad9914b4d5 100644 --- a/app/components/enterprise_edition/banner_component.rb +++ b/app/components/enterprise_edition/banner_component.rb @@ -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 diff --git a/app/components/enterprise_edition/banner_component.sass b/app/components/enterprise_edition/banner_component.sass index ba58b9d00e6..38cdf67fbde 100644 --- a/app/components/enterprise_edition/banner_component.sass +++ b/app/components/enterprise_edition/banner_component.sass @@ -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) diff --git a/app/components/enterprise_edition/plan_for_feature.rb b/app/components/enterprise_edition/plan_for_feature.rb new file mode 100644 index 00000000000..eacc80126d7 --- /dev/null +++ b/app/components/enterprise_edition/plan_for_feature.rb @@ -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 diff --git a/app/components/enterprise_edition/upsale_buttons_component.rb b/app/components/enterprise_edition/upsale_buttons_component.rb new file mode 100644 index 00000000000..4369e4adb45 --- /dev/null +++ b/app/components/enterprise_edition/upsale_buttons_component.rb @@ -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 diff --git a/app/components/enterprise_edition/upsale_page_component.html.erb b/app/components/enterprise_edition/upsale_page_component.html.erb new file mode 100644 index 00000000000..da2f3b8e114 --- /dev/null +++ b/app/components/enterprise_edition/upsale_page_component.html.erb @@ -0,0 +1,46 @@ +<% if respond_to?(:gon) %> + <% helpers.write_augur_to_gon %> + <% helpers.write_trial_key_to_gon if !EnterpriseToken.current.present? %> + <% end %> + +
+ <%= + 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 + %> + +

+ <%= render(Primer::Beta::Text.new) { description } %> + <%= render(Primer::Beta::Text.new) { plan_text } %> +

+ <% if more_info_text.present? %> + <%= render(Primer::Beta::Text.new(tag: :p, color: :muted)) { more_info_text } %> + <% end %> + + <% if features.present? %> + + <% 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) %> +
diff --git a/modules/overviews/app/components/project_custom_fields/side_panel_component.rb b/app/components/enterprise_edition/upsale_page_component.rb similarity index 55% rename from modules/overviews/app/components/project_custom_fields/side_panel_component.rb rename to app/components/enterprise_edition/upsale_page_component.rb index aae4f5f3874..a087babf59c 100644 --- a/modules/overviews/app/components/project_custom_fields/side_panel_component.rb +++ b/app/components/enterprise_edition/upsale_page_component.rb @@ -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 diff --git a/app/components/enterprise_edition/upsale_page_component.sass b/app/components/enterprise_edition/upsale_page_component.sass new file mode 100644 index 00000000000..794d4156f7b --- /dev/null +++ b/app/components/enterprise_edition/upsale_page_component.sass @@ -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 diff --git a/app/components/filter/filter_component.rb b/app/components/filter/filter_component.rb index 4b2d0291e6a..94738cf1d8f 100644 --- a/app/components/filter/filter_component.rb +++ b/app/components/filter/filter_component.rb @@ -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 diff --git a/app/components/my/access_token/new_access_token_form_component.html.erb b/app/components/my/access_token/new_access_token_form_component.html.erb index 2079647d98d..06ad515f16b 100644 --- a/app/components/my/access_token/new_access_token_form_component.html.erb +++ b/app/components/my/access_token/new_access_token_form_component.html.erb @@ -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 diff --git a/app/components/projects/configure_view_modal_component.html.erb b/app/components/projects/configure_view_modal_component.html.erb index 05c7ac98765..7cb21a99360 100644 --- a/app/components/projects/configure_view_modal_component.html.erb +++ b/app/components/projects/configure_view_modal_component.html.erb @@ -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" }, diff --git a/app/components/projects/life_cycle_component.html.erb b/app/components/projects/life_cycle_component.html.erb deleted file mode 100644 index 3f9a0f06e6f..00000000000 --- a/app/components/projects/life_cycle_component.html.erb +++ /dev/null @@ -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 %> diff --git a/app/components/projects/phase_component.html.erb b/app/components/projects/phase_component.html.erb index 1079ff7a7af..2ad6467291a 100644 --- a/app/components/projects/phase_component.html.erb +++ b/app/components/projects/phase_component.html.erb @@ -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 diff --git a/app/components/projects/phase_component.rb b/app/components/projects/phase_component.rb index 93f56f23aa2..c48f4ae2539 100644 --- a/app/components/projects/phase_component.rb +++ b/app/components/projects/phase_component.rb @@ -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 diff --git a/app/components/projects/phase_definition_component.html.erb b/app/components/projects/phase_definition_component.html.erb new file mode 100644 index 00000000000..0b3092e71ee --- /dev/null +++ b/app/components/projects/phase_definition_component.html.erb @@ -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 %> diff --git a/app/components/projects/life_cycle_component.rb b/app/components/projects/phase_definition_component.rb similarity index 80% rename from app/components/projects/life_cycle_component.rb rename to app/components/projects/phase_definition_component.rb index f96e1453c73..0f4cb9d72bc 100644 --- a/app/components/projects/life_cycle_component.rb +++ b/app/components/projects/phase_definition_component.rb @@ -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 diff --git a/modules/meeting/app/views/meetings/edit.html.erb b/app/components/projects/phases/hover_card_component.html.erb similarity index 50% rename from modules/meeting/app/views/meetings/edit.html.erb rename to app/components/projects/phases/hover_card_component.html.erb index b83959c2f38..b5cc2c84a3d 100644 --- a/modules/meeting/app/views/meetings/edit.html.erb +++ b/app/components/projects/phases/hover_card_component.html.erb @@ -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 +%> diff --git a/app/components/projects/phases/hover_card_component.rb b/app/components/projects/phases/hover_card_component.rb new file mode 100644 index 00000000000..5c1fbbdd650 --- /dev/null +++ b/app/components/projects/phases/hover_card_component.rb @@ -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 diff --git a/app/components/projects/phases/hover_card_component.sass b/app/components/projects/phases/hover_card_component.sass new file mode 100644 index 00000000000..7a16d26fc66 --- /dev/null +++ b/app/components/projects/phases/hover_card_component.sass @@ -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() diff --git a/modules/meeting/lib/api/v3/meeting_minutes/meeting_minutes_representer.rb b/app/components/projects/phases/shared.rb similarity index 86% rename from modules/meeting/lib/api/v3/meeting_minutes/meeting_minutes_representer.rb rename to app/components/projects/phases/shared.rb index 6d2f9bf580c..ff17be388e7 100644 --- a/modules/meeting/lib/api/v3/meeting_minutes/meeting_minutes_representer.rb +++ b/app/components/projects/phases/shared.rb @@ -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 diff --git a/app/components/projects/settings/life_cycle_steps/step_component.html.erb b/app/components/projects/settings/life_cycle_steps/step_component.html.erb index 7d777ea72c5..c198e9f52bf 100644 --- a/app/components/projects/settings/life_cycle_steps/step_component.html.erb +++ b/app/components/projects/settings/life_cycle_steps/step_component.html.erb @@ -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 diff --git a/app/components/projects/settings/life_cycle_steps/step_component.rb b/app/components/projects/settings/life_cycle_steps/step_component.rb index 08a2cf32264..2dd0f59b647 100644 --- a/app/components/projects/settings/life_cycle_steps/step_component.rb +++ b/app/components/projects/settings/life_cycle_steps/step_component.rb @@ -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 diff --git a/app/components/settings/project_custom_field_sections/dialog_body_form_component.html.erb b/app/components/settings/project_custom_field_sections/dialog_body_form_component.html.erb index 084fbf2ddff..6acb53d204a 100644 --- a/app/components/settings/project_custom_field_sections/dialog_body_form_component.html.erb +++ b/app/components/settings/project_custom_field_sections/dialog_body_form_component.html.erb @@ -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 diff --git a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb index 9c40ff194f1..ae6983e7de3 100644 --- a/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.html.erb @@ -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) } diff --git a/app/components/settings/project_life_cycle_step_definitions/row_component.rb b/app/components/settings/project_life_cycle_step_definitions/row_component.rb index 7f92dd5684f..777fcf1b76c 100644 --- a/app/components/settings/project_life_cycle_step_definitions/row_component.rb +++ b/app/components/settings/project_life_cycle_step_definitions/row_component.rb @@ -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:) diff --git a/app/components/settings/project_work_packages/header_component.html.erb b/app/components/settings/project_work_packages/header_component.html.erb index 2e9adcffcc2..46b857b392f 100644 --- a/app/components/settings/project_work_packages/header_component.html.erb +++ b/app/components/settings/project_work_packages/header_component.html.erb @@ -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 %> diff --git a/app/components/types/edit_page_header_component.html.erb b/app/components/types/edit_page_header_component.html.erb index ae9f50512ba..524997804f2 100644 --- a/app/components/types/edit_page_header_component.html.erb +++ b/app/components/types/edit_page_header_component.html.erb @@ -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 diff --git a/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb b/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb index 4522ef9f5fc..50d11aaecdc 100644 --- a/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb +++ b/app/components/work_package_relations_tab/add_work_package_child_dialog_component.html.erb @@ -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 }, diff --git a/app/components/work_package_relations_tab/index_component.html.erb b/app/components/work_package_relations_tab/index_component.html.erb index 2c063f7f633..45cf075a6b3 100644 --- a/app/components/work_package_relations_tab/index_component.html.erb +++ b/app/components/work_package_relations_tab/index_component.html.erb @@ -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") } diff --git a/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb b/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb index 34b0f36f814..e36a5106b07 100644 --- a/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb +++ b/app/components/work_package_relations_tab/work_package_relation_dialog_component.html.erb @@ -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 }, diff --git a/app/components/work_packages/activities_tab/index_component.rb b/app/components/work_packages/activities_tab/index_component.rb index ac3749569da..baa6b34dc6e 100644 --- a/app/components/work_packages/activities_tab/index_component.rb +++ b/app/components/work_packages/activities_tab/index_component.rb @@ -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 diff --git a/app/components/work_packages/activities_tab/journals/item_component.rb b/app/components/work_packages/activities_tab/journals/item_component.rb index 2f7154252a1..492185b8030 100644 --- a/app/components/work_packages/activities_tab/journals/item_component.rb +++ b/app/components/work_packages/activities_tab/journals/item_component.rb @@ -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, diff --git a/app/components/work_packages/activities_tab/journals/item_component/add_reactions.html.erb b/app/components/work_packages/activities_tab/journals/item_component/add_reactions.html.erb index 4e4b254e909..a952d1358e4 100644 --- a/app/components/work_packages/activities_tab/journals/item_component/add_reactions.html.erb +++ b/app/components/work_packages/activities_tab/journals/item_component/add_reactions.html.erb @@ -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 diff --git a/app/components/work_packages/activities_tab/journals/item_component/add_reactions.sass b/app/components/work_packages/activities_tab/journals/item_component/add_reactions.sass index bae487d87b6..dbc22364b61 100644 --- a/app/components/work_packages/activities_tab/journals/item_component/add_reactions.sass +++ b/app/components/work_packages/activities_tab/journals/item_component/add_reactions.sass @@ -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 diff --git a/app/components/work_packages/activities_tab/journals/item_component/edit.html.erb b/app/components/work_packages/activities_tab/journals/item_component/edit.html.erb index 34b292dadc0..bb97da905c3 100644 --- a/app/components/work_packages/activities_tab/journals/item_component/edit.html.erb +++ b/app/components/work_packages/activities_tab/journals/item_component/edit.html.erb @@ -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 diff --git a/app/components/work_packages/activities_tab/journals/item_component/edit.rb b/app/components/work_packages/activities_tab/journals/item_component/edit.rb index bfb45e59d52..628c1cab98e 100644 --- a/app/components/work_packages/activities_tab/journals/item_component/edit.rb +++ b/app/components/work_packages/activities_tab/journals/item_component/edit.rb @@ -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 diff --git a/app/components/work_packages/activities_tab/journals/item_component/reactions.html.erb b/app/components/work_packages/activities_tab/journals/item_component/reactions.html.erb index f40c99678dd..1408169ae93 100644 --- a/app/components/work_packages/activities_tab/journals/item_component/reactions.html.erb +++ b/app/components/work_packages/activities_tab/journals/item_component/reactions.html.erb @@ -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 diff --git a/app/components/work_packages/activities_tab/journals/new_component.rb b/app/components/work_packages/activities_tab/journals/new_component.rb index f84262eb508..4b691638f75 100644 --- a/app/components/work_packages/activities_tab/journals/new_component.rb +++ b/app/components/work_packages/activities_tab/journals/new_component.rb @@ -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 diff --git a/app/components/work_packages/date_picker/date_form_component.html.erb b/app/components/work_packages/date_picker/date_form_component.html.erb index d544079f8fc..7390a544aa3 100644 --- a/app/components/work_packages/date_picker/date_form_component.html.erb +++ b/app/components/work_packages/date_picker/date_form_component.html.erb @@ -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 }, diff --git a/app/components/work_packages/date_picker/dialog_content_component.html.erb b/app/components/work_packages/date_picker/dialog_content_component.html.erb index 52138d01df6..43bbbfb747d 100644 --- a/app/components/work_packages/date_picker/dialog_content_component.html.erb +++ b/app/components/work_packages/date_picker/dialog_content_component.html.erb @@ -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, diff --git a/app/components/work_packages/dialogs/create_dialog_component.html.erb b/app/components/work_packages/dialogs/create_dialog_component.html.erb index b4643d2d510..d23259f7409 100644 --- a/app/components/work_packages/dialogs/create_dialog_component.html.erb +++ b/app/components/work_packages/dialogs/create_dialog_component.html.erb @@ -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 diff --git a/app/components/work_packages/exports/base_export_settings_component.rb b/app/components/work_packages/exports/base_export_settings_component.rb index 16fdbc293dd..8f9be5894b4 100644 --- a/app/components/work_packages/exports/base_export_settings_component.rb +++ b/app/components/work_packages/exports/base_export_settings_component.rb @@ -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 diff --git a/app/components/work_packages/exports/column_selection_component.rb b/app/components/work_packages/exports/column_selection_component.rb index 0ac1182a797..accc783a382 100644 --- a/app/components/work_packages/exports/column_selection_component.rb +++ b/app/components/work_packages/exports/column_selection_component.rb @@ -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 diff --git a/app/components/work_packages/exports/csv/export_settings_component.html.erb b/app/components/work_packages/exports/csv/export_settings_component.html.erb index 387bbccad53..0bb18b9f16b 100644 --- a/app/components/work_packages/exports/csv/export_settings_component.html.erb +++ b/app/components/work_packages/exports/csv/export_settings_component.html.erb @@ -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 %> diff --git a/app/components/work_packages/exports/csv/export_settings_component.rb b/app/components/work_packages/exports/csv/export_settings_component.rb index 1ca563e7dc1..0084440ea17 100644 --- a/app/components/work_packages/exports/csv/export_settings_component.rb +++ b/app/components/work_packages/exports/csv/export_settings_component.rb @@ -32,6 +32,9 @@ module WorkPackages module Exports module CSV class ExportSettingsComponent < BaseExportSettingsComponent + def format + "csv" + end end end end diff --git a/app/components/work_packages/exports/generate/modal_dialog_component.html.erb b/app/components/work_packages/exports/generate/modal_dialog_component.html.erb index 746f9f13084..25757428b8a 100644 --- a/app/components/work_packages/exports/generate/modal_dialog_component.html.erb +++ b/app/components/work_packages/exports/generate/modal_dialog_component.html.erb @@ -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, diff --git a/app/components/work_packages/exports/modal_dialog_component.html.erb b/app/components/work_packages/exports/modal_dialog_component.html.erb index c27c7a4e1a7..6c95eec0deb 100644 --- a/app/components/work_packages/exports/modal_dialog_component.html.erb +++ b/app/components/work_packages/exports/modal_dialog_component.html.erb @@ -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" }, diff --git a/app/components/work_packages/exports/modal_dialog_component.rb b/app/components/work_packages/exports/modal_dialog_component.rb index 0707cb75ca6..492553704b8 100644 --- a/app/components/work_packages/exports/modal_dialog_component.rb +++ b/app/components/work_packages/exports/modal_dialog_component.rb @@ -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", diff --git a/app/components/work_packages/exports/pdf/gantt/export_settings_component.html.erb b/app/components/work_packages/exports/pdf/gantt/export_settings_component.html.erb index 5fbfbf17a05..eaea64b4869 100644 --- a/app/components/work_packages/exports/pdf/gantt/export_settings_component.html.erb +++ b/app/components/work_packages/exports/pdf/gantt/export_settings_component.html.erb @@ -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 %> diff --git a/app/components/work_packages/exports/pdf/gantt/export_settings_component.rb b/app/components/work_packages/exports/pdf/gantt/export_settings_component.rb index 3f1a2497d10..d409b9acf90 100644 --- a/app/components/work_packages/exports/pdf/gantt/export_settings_component.rb +++ b/app/components/work_packages/exports/pdf/gantt/export_settings_component.rb @@ -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 }, diff --git a/app/components/work_packages/exports/pdf/report/export_settings_component.html.erb b/app/components/work_packages/exports/pdf/report/export_settings_component.html.erb index bdd6ed4f8f5..a81056fb77f 100644 --- a/app/components/work_packages/exports/pdf/report/export_settings_component.html.erb +++ b/app/components/work_packages/exports/pdf/report/export_settings_component.html.erb @@ -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"), diff --git a/app/components/work_packages/exports/pdf/report/export_settings_component.rb b/app/components/work_packages/exports/pdf/report/export_settings_component.rb index b540b3fd3ce..46b2a7bf97c 100644 --- a/app/components/work_packages/exports/pdf/report/export_settings_component.rb +++ b/app/components/work_packages/exports/pdf/report/export_settings_component.rb @@ -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 diff --git a/app/components/work_packages/exports/pdf/table/export_settings_component.html.erb b/app/components/work_packages/exports/pdf/table/export_settings_component.html.erb index 64eb44bb3e7..cc5f7543a9a 100644 --- a/app/components/work_packages/exports/pdf/table/export_settings_component.html.erb +++ b/app/components/work_packages/exports/pdf/table/export_settings_component.html.erb @@ -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") ) %> diff --git a/app/components/work_packages/exports/pdf/table/export_settings_component.rb b/app/components/work_packages/exports/pdf/table/export_settings_component.rb index b8020bebacd..40519785397 100644 --- a/app/components/work_packages/exports/pdf/table/export_settings_component.rb +++ b/app/components/work_packages/exports/pdf/table/export_settings_component.rb @@ -33,6 +33,9 @@ module WorkPackages module PDF module Table class ExportSettingsComponent < BaseExportSettingsComponent + def format + "pdf_table" + end end end end diff --git a/app/components/work_packages/exports/xls/export_settings_component.html.erb b/app/components/work_packages/exports/xls/export_settings_component.html.erb index 7077876b9c9..73b40c2bbe4 100644 --- a/app/components/work_packages/exports/xls/export_settings_component.html.erb +++ b/app/components/work_packages/exports/xls/export_settings_component.html.erb @@ -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 diff --git a/app/components/work_packages/exports/xls/export_settings_component.rb b/app/components/work_packages/exports/xls/export_settings_component.rb index eb42c268054..3bbdbde2300 100644 --- a/app/components/work_packages/exports/xls/export_settings_component.rb +++ b/app/components/work_packages/exports/xls/export_settings_component.rb @@ -32,6 +32,9 @@ module WorkPackages module Exports module XLS class ExportSettingsComponent < BaseExportSettingsComponent + def format + "xls" + end end end end diff --git a/app/components/work_packages/types/subject_configuration_component.html.erb b/app/components/work_packages/types/subject_configuration_component.html.erb index 062014b3d7e..03b77fd4e42 100644 --- a/app/components/work_packages/types/subject_configuration_component.html.erb +++ b/app/components/work_packages/types/subject_configuration_component.html.erb @@ -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)) %> <%= diff --git a/app/contracts/project_life_cycle_steps/base_contract.rb b/app/contracts/project_life_cycle_steps/base_contract.rb index 6d6b4c552c7..1570a1f6aed 100644 --- a/app/contracts/project_life_cycle_steps/base_contract.rb +++ b/app/contracts/project_life_cycle_steps/base_contract.rb @@ -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 diff --git a/app/contracts/work_packages/base_contract.rb b/app/contracts/work_packages/base_contract.rb index 55b20e962f4..a6561859f9c 100644 --- a/app/contracts/work_packages/base_contract.rb +++ b/app/contracts/work_packages/base_contract.rb @@ -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? diff --git a/app/contracts/work_packages/create_note_contract.rb b/app/contracts/work_packages/create_note_contract.rb index 21d8d7700cb..d9652dbe2ca 100644 --- a/app/contracts/work_packages/create_note_contract.rb +++ b/app/contracts/work_packages/create_note_contract.rb @@ -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 diff --git a/app/contracts/work_packages/update_contract.rb b/app/contracts/work_packages/update_contract.rb index 7d325f4bdc7..40182352139 100644 --- a/app/contracts/work_packages/update_contract.rb +++ b/app/contracts/work_packages/update_contract.rb @@ -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 diff --git a/app/controllers/admin/attachments/quarantined_attachments_controller.rb b/app/controllers/admin/attachments/quarantined_attachments_controller.rb index 80b7ec2556f..3b28ca495d5 100644 --- a/app/controllers/admin/attachments/quarantined_attachments_controller.rb +++ b/app/controllers/admin/attachments/quarantined_attachments_controller.rb @@ -73,8 +73,6 @@ module Admin def find_attachment @attachment = @attachments.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 end end end diff --git a/app/controllers/admin/custom_fields/hierarchy/items_controller.rb b/app/controllers/admin/custom_fields/hierarchy/items_controller.rb index a23eda79907..ed67330f040 100644 --- a/app/controllers/admin/custom_fields/hierarchy/items_controller.rb +++ b/app/controllers/admin/custom_fields/hierarchy/items_controller.rb @@ -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 diff --git a/app/controllers/admin/settings/project_custom_fields_controller.rb b/app/controllers/admin/settings/project_custom_fields_controller.rb index 39b328304bd..8a53a428268 100644 --- a/app/controllers/admin/settings/project_custom_fields_controller.rb +++ b/app/controllers/admin/settings/project_custom_fields_controller.rb @@ -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) diff --git a/app/controllers/admin/settings/virus_scanning_settings_controller.rb b/app/controllers/admin/settings/virus_scanning_settings_controller.rb index e7eabbc7981..0f530857b47 100644 --- a/app/controllers/admin/settings/virus_scanning_settings_controller.rb +++ b/app/controllers/admin/settings/virus_scanning_settings_controller.rb @@ -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, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 657e26c1116..bd5793ae38c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/attribute_help_texts_controller.rb b/app/controllers/attribute_help_texts_controller.rb index 062a2733782..1d42804a5a1 100644 --- a/app/controllers/attribute_help_texts_controller.rb +++ b/app/controllers/attribute_help_texts_controller.rb @@ -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 diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 286f766e469..feb3c809d20 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -101,7 +101,5 @@ class CategoriesController < ApplicationController def find_project @project = Project.find(params[:project_id]) - rescue ActiveRecord::RecordNotFound - render_404 end end diff --git a/app/controllers/concerns/accounts/authorization.rb b/app/controllers/concerns/accounts/authorization.rb index 93320f71eb1..7bb34d46159 100644 --- a/app/controllers/concerns/accounts/authorization.rb +++ b/app/controllers/concerns/accounts/authorization.rb @@ -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 diff --git a/app/controllers/concerns/custom_fields/shared_actions.rb b/app/controllers/concerns/custom_fields/shared_actions.rb index 3fab2fca572..0c1b9553772 100644 --- a/app/controllers/concerns/custom_fields/shared_actions.rb +++ b/app/controllers/concerns/custom_fields/shared_actions.rb @@ -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) diff --git a/app/controllers/custom_actions_controller.rb b/app/controllers/custom_actions_controller.rb index a2ec48df759..0601c9322bb 100644 --- a/app/controllers/custom_actions_controller.rb +++ b/app/controllers/custom_actions_controller.rb @@ -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 diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb index c280ec2dfeb..3bf72aedf15 100644 --- a/app/controllers/custom_fields_controller.rb +++ b/app/controllers/custom_fields_controller.rb @@ -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 diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index 414248f8161..b4d7c3ba3aa 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -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 diff --git a/app/controllers/journals_controller.rb b/app/controllers/journals_controller.rb index e1b49530e78..0e3ce394057 100644 --- a/app/controllers/journals_controller.rb +++ b/app/controllers/journals_controller.rb @@ -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 diff --git a/modules/meeting/spec/models/meeting_agenda_spec.rb b/app/controllers/my/enterprise_banners_controller.rb similarity index 59% rename from modules/meeting/spec/models/meeting_agenda_spec.rb rename to app/controllers/my/enterprise_banners_controller.rb index becc0486d6b..e627b18ae77 100644 --- a/modules/meeting/spec/models/meeting_agenda_spec.rb +++ b/app/controllers/my/enterprise_banners_controller.rb @@ -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 diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb index 4e851f18bb9..8fd8bba3a53 100644 --- a/app/controllers/news_controller.rb +++ b/app/controllers/news_controller.rb @@ -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 diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 394d8243cd0..6551f019f2b 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -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 diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 04686f9e908..2964cca876d 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -115,8 +115,6 @@ module OAuth def find_app @application = ::Doorkeeper::Application.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 end end end diff --git a/app/controllers/placeholder_users/memberships_controller.rb b/app/controllers/placeholder_users/memberships_controller.rb index 7b164709921..35bb55a1c08 100644 --- a/app/controllers/placeholder_users/memberships_controller.rb +++ b/app/controllers/placeholder_users/memberships_controller.rb @@ -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) diff --git a/app/controllers/placeholder_users_controller.rb b/app/controllers/placeholder_users_controller.rb index 65ce10e47e4..e1317bde96b 100644 --- a/app/controllers/placeholder_users_controller.rb +++ b/app/controllers/placeholder_users_controller.rb @@ -146,8 +146,6 @@ class PlaceholderUsersController < ApplicationController def find_placeholder_user @placeholder_user = PlaceholderUser.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 end protected diff --git a/app/controllers/project_phases/hover_card_controller.rb b/app/controllers/project_phases/hover_card_controller.rb new file mode 100644 index 00000000000..0e5bbc827de --- /dev/null +++ b/app/controllers/project_phases/hover_card_controller.rb @@ -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 diff --git a/modules/meeting/app/controllers/meeting_agendas_controller.rb b/app/controllers/projects/settings/work_packages/activities_controller.rb similarity index 61% rename from modules/meeting/app/controllers/meeting_agendas_controller.rb rename to app/controllers/projects/settings/work_packages/activities_controller.rb index 1c1888126bd..18a756f1174 100644 --- a/modules/meeting/app/controllers/meeting_agendas_controller.rb +++ b/app/controllers/projects/settings/work_packages/activities_controller.rb @@ -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 diff --git a/app/controllers/projects/settings/work_packages_controller.rb b/app/controllers/projects/settings/work_packages_controller.rb index 69ef4f49f6c..1f02129df55 100644 --- a/app/controllers/projects/settings/work_packages_controller.rb +++ b/app/controllers/projects/settings/work_packages_controller.rb @@ -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 diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index 37682ac30da..cff4751c01b 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -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 diff --git a/app/controllers/users/memberships_controller.rb b/app/controllers/users/memberships_controller.rb index 681163c3474..2daf11b1f9a 100644 --- a/app/controllers/users/memberships_controller.rb +++ b/app/controllers/users/memberships_controller.rb @@ -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) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c257e1949e4..3a0b2e866ef 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -244,8 +244,6 @@ class UsersController < ApplicationController else @user = User.find(params[:id]) end - rescue ActiveRecord::RecordNotFound - render_404 end def authorize_for_user diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb index 15d5ea200f5..2c2a189a32d 100644 --- a/app/controllers/versions_controller.rb +++ b/app/controllers/versions_controller.rb @@ -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) diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb index f3c8853a944..a9824dca7fd 100644 --- a/app/controllers/wiki_controller.rb +++ b/app/controllers/wiki_controller.rb @@ -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 diff --git a/app/controllers/work_package_relations_tab_controller.rb b/app/controllers/work_package_relations_tab_controller.rb index 097b4b8296e..a1ce2acaa11 100644 --- a/app/controllers/work_package_relations_tab_controller.rb +++ b/app/controllers/work_package_relations_tab_controller.rb @@ -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 diff --git a/app/controllers/work_packages/activities_tab_controller.rb b/app/controllers/work_packages/activities_tab_controller.rb index e96e4725049..eb40620b760 100644 --- a/app/controllers/work_packages/activities_tab_controller.rb +++ b/app/controllers/work_packages/activities_tab_controller.rb @@ -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 diff --git a/app/controllers/work_packages_controller.rb b/app/controllers/work_packages_controller.rb index 3cce30d327f..72ba79a9dc7 100644 --- a/app/controllers/work_packages_controller.rb +++ b/app/controllers/work_packages_controller.rb @@ -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 diff --git a/app/forms/projects/life_cycles/form.rb b/app/forms/projects/life_cycles/form.rb index 52a764f544c..4ab76c6b1e3 100644 --- a/app/forms/projects/life_cycles/form.rb +++ b/app/forms/projects/life_cycles/form.rb @@ -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: { diff --git a/modules/meeting/app/contracts/meeting_contents/base_contract.rb b/app/forms/projects/settings/work_packages/activities/form.rb similarity index 61% rename from modules/meeting/app/contracts/meeting_contents/base_contract.rb rename to app/forms/projects/settings/work_packages/activities/form.rb index 3948bec28c8..3dd3a1f37ff 100644 --- a/modules/meeting/app/contracts/meeting_contents/base_contract.rb +++ b/app/forms/projects/settings/work_packages/activities/form.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,29 +27,35 @@ # # See COPYRIGHT and LICENSE files for more details. #++ +module Projects::Settings::WorkPackages::Activities + class Form < ApplicationForm + form do |f| + f.check_box( + name: :enabled_comments_with_restricted_visibility, + label: I18n.t("settings.work_packages.activities.enable_comments_with_restricted_visibility"), + caption: caption_text, + checked: model.project.enabled_comments_with_restricted_visibility + ) -module MeetingContents - class BaseContract < ::ModelContract - include Attachments::ValidateReplacements - - def self.model - MeetingContent + f.submit( + name: :submit, + label: I18n.t(:button_save), + scheme: :primary + ) end - attribute :meeting - attribute :text - attribute :lock_version - attribute :locked - attribute :type - - validate :type_in_allowed + def initialize(**_options) + super() + end private - def type_in_allowed - unless [MeetingAgenda.name, MeetingMinutes.name].include?(model.type) - errors.add(:type, :inclusion) + def caption_text + link = render(Primer::Beta::Link.new(href: OpenProject::Static::Links.url_for(:user_guides_work_package_activity))) do + I18n.t("label_learn_more") end + + I18n.t("settings.work_packages.activities.helper_text", link:).html_safe end end end diff --git a/app/forms/statuses/form.rb b/app/forms/statuses/form.rb index c8b3c46490e..2948f1356aa 100644 --- a/app/forms/statuses/form.rb +++ b/app/forms/statuses/form.rb @@ -88,12 +88,7 @@ module Statuses if readonly_work_packages_restricted? statuses_form.html_content do - angular_component_tag "opce-enterprise-banner", - inputs: { - collapsible: true, - textMessage: t("text_wp_status_read_only_html"), - moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:status_read_only][:href] - } + render(EnterpriseEdition::BannerComponent.new(:readonly_work_packages)) end end diff --git a/app/forms/work_packages/activities_tab/journals/restricted_note_form.rb b/app/forms/work_packages/activities_tab/journals/restricted_note_form.rb index 41a22021e83..30e8b5dd9ba 100644 --- a/app/forms/work_packages/activities_tab/journals/restricted_note_form.rb +++ b/app/forms/work_packages/activities_tab/journals/restricted_note_form.rb @@ -36,7 +36,7 @@ module WorkPackages::ActivitiesTab::Journals checked: false, data: { "work-packages--activities-tab--restricted-comment-target": "restrictedCheckbox", - action: "input->work-packages--activities-tab--restricted-comment#toggleBackgroundColor" + action: "input->work-packages--activities-tab--restricted-comment#toggleRestriction" } ) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1a1ad14f73d..8146e13e3b0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -242,7 +242,9 @@ module ApplicationHelper css << ("action-#{action_name}") end - css << "ee-banners-#{EnterpriseToken.show_banners? ? 'visible' : 'hidden'}" + if EnterpriseToken.hide_banners? + css << "ee-banners-hidden" + end css << "env-#{Rails.env}" diff --git a/app/helpers/enterprise_helper.rb b/app/helpers/enterprise_helper.rb index 116b59d8c8a..c0c86946239 100644 --- a/app/helpers/enterprise_helper.rb +++ b/app/helpers/enterprise_helper.rb @@ -46,14 +46,14 @@ module EnterpriseHelper def enterprise_token_plan_name(enterprise_token) <<~LABEL.squish - #{I18n.t(enterprise_token.plan, scope: [:enterprise_plans])} + #{I18n.t(enterprise_token.plan, scope: [:enterprise_plans], default: enterprise_token.plan.to_s.capitalize)} (#{I18n.t(:label_token_version)} #{enterprise_token.version}) LABEL end def enterprise_plan_additional_features(enterprise_token) (enterprise_token.try(:features) || []) - .filter_map { |feature| I18n.t(feature, scope: [:enterprise_features], default: nil) } + .filter_map { |feature| I18n.t(feature, scope: :"ee.features", default: nil) } .sort .join(", ") end diff --git a/app/helpers/homescreen_helper.rb b/app/helpers/homescreen_helper.rb index c828f5a6623..cd0ebf088e3 100644 --- a/app/helpers/homescreen_helper.rb +++ b/app/helpers/homescreen_helper.rb @@ -54,7 +54,7 @@ module HomescreenHelper ## # Determine whether we should render the links on homescreen? def show_homescreen_links? - EnterpriseToken.show_banners? || OpenProject::Configuration.show_community_links? + OpenProject::Configuration.show_community_links? end ## diff --git a/app/helpers/types_helper.rb b/app/helpers/types_helper.rb index f4458d04870..6fc73f7bbb8 100644 --- a/app/helpers/types_helper.rb +++ b/app/helpers/types_helper.rb @@ -63,7 +63,8 @@ module ::TypesHelper name: "subject_configuration", path: edit_tab_type_path(id: @type.id, tab: :subject_configuration), label: "types.edit.subject_configuration.tab", - view_component: WorkPackages::Types::SubjectConfigurationComponent + view_component: WorkPackages::Types::SubjectConfigurationComponent, + enterprise_feature: :work_package_subject_generation } tabs.insert(2, subject_configuration_tab) diff --git a/app/helpers/work_packages_controller_helper.rb b/app/helpers/work_packages_controller_helper.rb index 6e1ae6e7803..e70718cdb66 100644 --- a/app/helpers/work_packages_controller_helper.rb +++ b/app/helpers/work_packages_controller_helper.rb @@ -61,8 +61,6 @@ module WorkPackagesControllerHelper request.format = "html" render_400(message: @query.errors.full_messages.join(". ")) end - rescue ActiveRecord::RecordNotFound - render_404 end def atom_list diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb index b74af8119e0..3fcab7a3a61 100644 --- a/app/models/custom_field.rb +++ b/app/models/custom_field.rb @@ -373,7 +373,7 @@ class CustomField < ApplicationRecord if project&.persisted? project.shared_versions elsif options[:scope] == :visible - Version.visible.or(Version.systemwide) + Version.visible else Version.systemwide end diff --git a/app/models/enterprise_token.rb b/app/models/enterprise_token.rb index 55b2d0c44f8..2dcad4af441 100644 --- a/app/models/enterprise_token.rb +++ b/app/models/enterprise_token.rb @@ -45,12 +45,8 @@ class EnterpriseToken < ApplicationRecord current && !current.expired? end - def show_banners?(feature: nil) - if feature - OpenProject::Configuration.ee_manager_visible? && (!active? || !allows_to?(feature)) - else - OpenProject::Configuration.ee_manager_visible? && !active? - end + def hide_banners? + OpenProject::Configuration.ee_hide_banners? end def banner_type_for(feature:) @@ -88,6 +84,7 @@ class EnterpriseToken < ApplicationRecord :reprieve_days, :reprieve_days_left, :restrictions, + :available_features, :plan, :features, :version, diff --git a/modules/meeting/spec/features/meetings_locking_spec.rb b/app/models/export_setting.rb similarity index 57% rename from modules/meeting/spec/features/meetings_locking_spec.rb rename to app/models/export_setting.rb index ab1d7f1064c..9a1a831d8be 100644 --- a/modules/meeting/spec/features/meetings_locking_spec.rb +++ b/app/models/export_setting.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,40 +28,38 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require "spec_helper" +class ExportSetting < ApplicationRecord + belongs_to :query -RSpec.describe "Meetings locking", :js do - let(:project) { create(:project, enabled_module_names: %w[meetings]) } - let(:user) { create(:admin) } - let!(:meeting) { create(:meeting, project:) } - let!(:agenda) { create(:meeting_agenda, meeting:) } - let(:agenda_field) do - TextEditorField.new(page, - "", - selector: test_selector("op-meeting--meeting_agenda")) + POSSIBLE_FORMATS = %w[csv xls pdf_table pdf_report pdf_gantt].freeze + + validates :query_id, presence: true + validates :format, presence: true, inclusion: { in: POSSIBLE_FORMATS } + validates :settings, presence: true + + validate :unique_format_per_query + + def settings + # Read idiomatic symbol keys for JSONB column + super.symbolize_keys end - before do - login_as(user) + def settings=(value) + # Provide database with string keys + super(value.stringify_keys) end - it "shows an error when trying to update a meeting update while editing" do - visit meeting_path(meeting) + # Some boolean settings are saved as string. Use this method to conveniently check if they are + # set to true. + def true?(key, default: false) + %w[true 1].include?(settings.fetch(key, default).to_s) + end - # Edit agenda - within "#tab-content-agenda" do - find(".button--edit-agenda").click + private - agenda_field.set_value("Some new text") - - agenda.text = "blabla" - agenda.save! - - click_on "Save" + def unique_format_per_query + if ExportSetting.exists?(query_id: query_id, format: format) && (new_record? || format_changed?) + errors.add(:format, "there already is an export setting for this query with this format") end - - expect(page).to have_text "Information has been updated by at least one other user in the meantime." - - agenda_field.expect_value("Some new text") end end diff --git a/app/models/oauth_client_token.rb b/app/models/oauth_client_token.rb index 7bbbedaf660..78965079dcd 100644 --- a/app/models/oauth_client_token.rb +++ b/app/models/oauth_client_token.rb @@ -37,4 +37,6 @@ class OAuthClientToken < ApplicationRecord validates :access_token, presence: true validates :refresh_token, presence: true + + scope :for_user_and_client, ->(user, client) { where(user:, oauth_client: client) } end diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 3ade3126ee4..e4a3ec9e0eb 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -289,10 +289,8 @@ class PermittedParams whitelist.merge(custom_field_values(:project)) end - def project_phases - params.require(:project).permit( - available_phases_attributes: %i[id date date_range] - ) + def project_phase + params.require(:project_phase).permit(%i[id date_range]) end def project_custom_field_project_mapping diff --git a/app/models/project.rb b/app/models/project.rb index 75575e64e3f..fa0ae2cdf3f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -99,6 +99,7 @@ class Project < ApplicationRecord validates_associated :available_phases, on: :saving_phases store_attribute :settings, :deactivate_work_package_attachments, :boolean + store_attribute :settings, :enabled_comments_with_restricted_visibility, :boolean acts_as_favorable diff --git a/app/models/queries/principals.rb b/app/models/queries/principals.rb index 378f53019ef..8197f099cac 100644 --- a/app/models/queries/principals.rb +++ b/app/models/queries/principals.rb @@ -32,6 +32,7 @@ module Queries::Principals filter Filters::TypeFilter filter Filters::MemberFilter filter Filters::MentionableOnWorkPackageFilter + filter Filters::RestrictedMentionableOnWorkPackageFilter filter Filters::StatusFilter filter Filters::NameFilter filter Filters::AnyNameAttributeFilter diff --git a/app/models/queries/principals/filters/mentionable_on_work_package_filter.rb b/app/models/queries/principals/filters/mentionable_on_work_package_filter.rb index a5d6d0ff847..14156cf49ad 100644 --- a/app/models/queries/principals/filters/mentionable_on_work_package_filter.rb +++ b/app/models/queries/principals/filters/mentionable_on_work_package_filter.rb @@ -47,7 +47,7 @@ class Queries::Principals::Filters::MentionableOnWorkPackageFilter < end def human_name - "mentionable" # intenral use + "mentionable" # Only for Internal use, not visible in the UI end def apply_to(query_scope) diff --git a/app/models/queries/principals/filters/restricted_mentionable_on_work_package_filter.rb b/app/models/queries/principals/filters/restricted_mentionable_on_work_package_filter.rb new file mode 100644 index 00000000000..b3c442f849c --- /dev/null +++ b/app/models/queries/principals/filters/restricted_mentionable_on_work_package_filter.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Principals::Filters::RestrictedMentionableOnWorkPackageFilter < + Queries::Principals::Filters::PrincipalFilter + validate :values_are_a_single_work_package_id + + def allowed_values + raise NotImplementedError, "There would be too many candidates" + end + + def allowed_values_subset + @allowed_values_subset ||= ::WorkPackage.visible + end + + def type + :list_optional + end + + def key + :restricted_mentionable_on_work_package + end + + def human_name + "restricted mentionable" # Only for Internal use, not visible in the UI + end + + def apply_to(query_scope) + case operator + when "=" + query_scope.where(id: project_members.select(:user_id)) + when "!" + query_scope.where.not(id: project_members.select(:user_id)) + end + end + + def permission + :view_comments_with_restricted_visibility + end + + private + + def type_strategy + @type_strategy ||= Queries::Filters::Strategies::HugeList.new(self) + end + + def values_are_a_single_work_package_id + errors.add(:values, :single_value_requirement) if values.size > 1 + end + + def project_members + Member.of_project(work_package.project) + .joins(roles: :role_permissions) + .where(role_permissions: { permission: }) + end + + def work_package + WorkPackage.find(values.first) + end +end diff --git a/app/models/queries/work_packages/filter/version_filter.rb b/app/models/queries/work_packages/filter/version_filter.rb index 620781a822e..a746b74f24a 100644 --- a/app/models/queries/work_packages/filter/version_filter.rb +++ b/app/models/queries/work_packages/filter/version_filter.rb @@ -62,7 +62,7 @@ class Queries::WorkPackages::Filter::VersionFilter < if project project.shared_versions else - Version.visible.or(Version.systemwide) + Version.visible end end end diff --git a/app/models/query.rb b/app/models/query.rb index e3572776494..4049933269b 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -43,6 +43,7 @@ class Query < ApplicationRecord has_many :ical_tokens, through: :ical_token_query_assignments, class_name: "Token::ICal" + has_many :export_settings, dependent: :destroy # no `dependent: :destroy` as the ical_tokens are destroyed in the following before_destroy callback # dependent: :destroy is not possible as this would only delete the ical_token_query_assignments before_destroy :destroy_ical_tokens @@ -420,6 +421,10 @@ class Query < ApplicationRecord subproject_filter end + def export_settings_for(format) + export_settings.where(format:).first_or_initialize + end + private ## diff --git a/app/models/types/pattern_resolver.rb b/app/models/types/pattern_resolver.rb index 5f6aba53aee..703f2ca2f5e 100644 --- a/app/models/types/pattern_resolver.rb +++ b/app/models/types/pattern_resolver.rb @@ -31,6 +31,7 @@ module Types class PatternResolver TOKEN_REGEX = /{{[0-9A-Za-z_]+}}/ + ATTRIBUTE_PLACEHOLDER = "N/A" def initialize(pattern) @mapper = Patterns::TokenPropertyMapper.new @@ -59,7 +60,7 @@ module Types when Array value.join(", ") when NilClass - "NA" + ATTRIBUTE_PLACEHOLDER else value.to_s end diff --git a/app/models/types/patterns/token_property_mapper.rb b/app/models/types/patterns/token_property_mapper.rb index a739a0e3fec..1881e8bf7b4 100644 --- a/app/models/types/patterns/token_property_mapper.rb +++ b/app/models/types/patterns/token_property_mapper.rb @@ -33,6 +33,7 @@ module Types class TokenPropertyMapper DEFAULT_FUNCTION = ->(key, context) do return nil if context.nil? + return nil unless context.respond_to?(key.to_sym) context.public_send(key.to_sym) end.curry @@ -46,6 +47,7 @@ module Types category: { fn: ->(wp) { wp.category&.name }, label: -> { WorkPackage.human_attribute_name(:category) } }, creation_date: { fn: ->(wp) { wp.created_at }, label: -> { WorkPackage.human_attribute_name(:created_at) } }, estimated_time: { fn: ->(wp) { wp.estimated_hours }, label: -> { WorkPackage.human_attribute_name(:estimated_hours) } }, + remaining_time: { fn: ->(wp) { wp.remaining_hours }, label: -> { WorkPackage.human_attribute_name(:remaining_hours) } }, finish_date: { fn: ->(wp) { wp.due_date }, label: -> { WorkPackage.human_attribute_name(:due_date) } }, parent_id: { fn: ->(wp) { wp.parent&.id }, label: -> { WorkPackage.human_attribute_name(:id) } }, parent_assignee: { fn: ->(wp) { wp.parent&.assigned_to&.name }, label: -> { @@ -58,6 +60,8 @@ module Types label: -> { WorkPackage.human_attribute_name(:created_at) } }, parent_estimated_time: { fn: ->(wp) { wp.parent&.estimated_hours }, label: -> { WorkPackage.human_attribute_name(:estimated_hours) } }, + parent_remaining_time: { fn: ->(wp) { wp.parent&.remaining_hours }, + label: -> { WorkPackage.human_attribute_name(:remaining_hours) } }, parent_finish_date: { fn: ->(wp) { wp.parent&.due_date }, label: -> { WorkPackage.human_attribute_name(:due_date) } }, parent_priority: { fn: ->(wp) { wp.parent&.priority }, label: -> { WorkPackage.human_attribute_name(:priority) } }, @@ -93,7 +97,7 @@ module Types private def default_tokens - TOKEN_PROPERTY_MAP.keys.each_with_object({ project: {}, work_package: {}, parent: {} }) do |key, obj| + TOKEN_PROPERTY_MAP.keys.each_with_object({ work_package: {}, project: {}, parent: {} }) do |key, obj| label = TOKEN_PROPERTY_MAP.dig(key, :label).call case key.to_s diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 0e90fe646d1..e34f27969f1 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -147,6 +147,14 @@ class UserPreference < ApplicationRecord super.presence || { enabled: false }.with_indifferent_access end + def dismissed_banner?(feature) + dismissed_enterprise_banners.key?(feature.to_s) + end + + def dismiss_banner(feature) + dismissed_enterprise_banners[feature.to_s] = Time.zone.now.utc + end + def supported_settings_method?(method_name) UserPreferences::Schema.properties.include?(method_name.to_s.gsub(/\?|=\z/, "")) end diff --git a/app/models/version.rb b/app/models/version.rb index 002869967a9..e72133043dd 100644 --- a/app/models/version.rb +++ b/app/models/version.rb @@ -53,6 +53,7 @@ class Version < ApplicationRecord scope :visible, ->(*args) { joins(:project) .merge(Project.allowed_to(args.first || User.current, :view_work_packages)) + .or(Version.systemwide) } scope :systemwide, -> { where(sharing: "system") } diff --git a/app/models/work_package/journalized.rb b/app/models/work_package/journalized.rb index 0ff0d06be3f..2bfd855df77 100644 --- a/app/models/work_package/journalized.rb +++ b/app/models/work_package/journalized.rb @@ -33,6 +33,7 @@ module WorkPackage::Journalized acts_as_journalized journals_association_extension: proc { def restricted_visible if OpenProject::FeatureDecisions.comments_with_restricted_visibility_active? && + proxy_association.owner.project.enabled_comments_with_restricted_visibility && User.current.allowed_in_project?(:view_comments_with_restricted_visibility, proxy_association.owner.project) all else diff --git a/app/models/work_package/pdf_export/common/attachments.rb b/app/models/work_package/pdf_export/common/attachments.rb index 2a1574f8618..75a3f820461 100644 --- a/app/models/work_package/pdf_export/common/attachments.rb +++ b/app/models/work_package/pdf_export/common/attachments.rb @@ -70,8 +70,23 @@ module WorkPackage::PDFExport::Common::Attachments resize_image(local_file.path) end - def attachment_by_api_content_src(work_package, src) - # find attachment by api-path - work_package.attachments.find { |a| api_url_helpers.attachment_content(a.id) == src } + def attachment_by_api_content_src(_work_package, src) + attachment_regex = %r{/attachments/(\d+)/content} + return nil unless src&.match?(attachment_regex) + + attachments_id = src.scan(attachment_regex).first.first + + # return nil if the src is not an attachment url + return nil unless api_url_helpers.attachment_content(attachments_id) == src + + attachment = Attachment.find_by(id: attachments_id.to_i) + return nil if attachment.nil? + return nil unless attachment.visible? + + attachment + rescue StandardError + # if the attachment is not found or the id is invalid, we return nil + Rails.logger.error "Failed to access attachment #{src}" + nil end end diff --git a/app/policies/work_package_policy.rb b/app/policies/work_package_policy.rb deleted file mode 100644 index c30b4316f1c..00000000000 --- a/app/policies/work_package_policy.rb +++ /dev/null @@ -1,106 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -class WorkPackagePolicy < BasePolicy - private - - def cache(work_package) - @cache ||= Hash.new do |wp_hash, wp| - wp_hash[wp] = allowed_hash(wp) - end - - @cache[work_package] - end - - def allowed_hash(work_package) - # copy checks for the move_work_packages permission. This makes - # sense only because the work_packages/moves controller handles - # copying multiple work packages. - { - edit: edit_allowed?(work_package), - move: move_allowed?(work_package), - copy: move_allowed?(work_package), - duplicate: copy_allowed?(work_package), # duplicating is another form of copying - delete: delete_allowed?(work_package), - manage_subtasks: manage_subtasks_allowed?(work_package), - comment: comment_allowed?(work_package), - change_status: change_status_allowed?(work_package), - assign_version: assign_version_allowed?(work_package) - } - end - - def edit_allowed?(work_package) - work_package.persisted? && user.allowed_in_work_package?(:edit_work_packages, work_package) - end - - def move_allowed?(work_package) - user.allowed_in_project?(:move_work_packages, work_package.project) - end - - def copy_allowed?(work_package) - type_active_in_project?(work_package) && add_allowed?(work_package) - end - - def delete_allowed?(work_package) - user.allowed_in_project?(:delete_work_packages, work_package.project) - end - - def add_allowed?(work_package) - user.allowed_in_project?(:add_work_packages, work_package.project) - end - - def type_active_in_project?(work_package) - return false unless work_package.project - - @type_active_cache ||= Hash.new do |hash, project| - hash[project] = project.types.pluck(:id) - end - - @type_active_cache[work_package.project].include?(work_package.type_id) - end - - def manage_subtasks_allowed?(work_package) - user.allowed_in_project?(:manage_subtasks, work_package.project) - end - - def comment_allowed?(work_package) - user.allowed_in_work_package?(:add_work_package_notes, work_package) || edit_allowed?(work_package) - end - - def assign_version_allowed?(work_package) - user.allowed_in_project?(:assign_versions, work_package.project) - end - - def change_status_allowed?(work_package) - @change_status_cache ||= Hash.new do |hash, project| - hash[project] = user.allowed_in_project?(%i[edit_work_packages change_work_package_status], work_package.project) - end - - @change_status_cache[work_package.project] - end -end diff --git a/app/services/notifications/create_from_model_service.rb b/app/services/notifications/create_from_model_service.rb index 0613b821920..9bdd65d434e 100644 --- a/app/services/notifications/create_from_model_service.rb +++ b/app/services/notifications/create_from_model_service.rb @@ -31,18 +31,18 @@ class Notifications::CreateFromModelService MENTION_USER_TAG_ID_PATTERN = ']*(?:data-type="user"[^>]*data-id="(\d+)")|(?:data-id="(\d+)"[^>]*data-type="user")[^>]*>' - .freeze + MENTION_USER_HASH_ID_PATTERN = '\buser#(\d+)\b' - .freeze + MENTION_USER_LOGIN_PATTERN = - '\buser:"(.+?)"'.freeze + '\buser:"(.+?)"' MENTION_GROUP_TAG_ID_PATTERN = ']*(?:data-type="group"[^>]*data-id="(\d+)")|(?:data-id="(\d+)"[^>]*data-type="group")[^>]*>' - .freeze + MENTION_GROUP_HASH_ID_PATTERN = '\bgroup#(\d+)\b' - .freeze + COMBINED_MENTION_PATTERN = [MENTION_USER_TAG_ID_PATTERN, MENTION_USER_HASH_ID_PATTERN, @@ -247,7 +247,7 @@ class Notifications::CreateFromModelService def settings_for_allowed_users(user_scope, reason) NotificationSetting .where(reason => true) - .where(user: user_scope.where(id: User.allowed(strategy.permission, project))) + .where(user: user_scope.where(id: User.allowed(strategy.permission(journal, reason), project))) end # Returns the text of the model (currently suited to work package description and subject) eligible diff --git a/app/services/notifications/create_from_model_service/comment_strategy.rb b/app/services/notifications/create_from_model_service/comment_strategy.rb index ff48df4bb26..66cbabf6920 100644 --- a/app/services/notifications/create_from_model_service/comment_strategy.rb +++ b/app/services/notifications/create_from_model_service/comment_strategy.rb @@ -31,7 +31,7 @@ module Notifications::CreateFromModelService::CommentStrategy %i(watched subscribed) end - def self.permission + def self.permission(*) :view_news end diff --git a/app/services/notifications/create_from_model_service/message_strategy.rb b/app/services/notifications/create_from_model_service/message_strategy.rb index 8ca67d94d61..18637d42b32 100644 --- a/app/services/notifications/create_from_model_service/message_strategy.rb +++ b/app/services/notifications/create_from_model_service/message_strategy.rb @@ -31,7 +31,7 @@ module Notifications::CreateFromModelService::MessageStrategy %i(watched subscribed) end - def self.permission + def self.permission(*) :view_messages end diff --git a/app/services/notifications/create_from_model_service/news_strategy.rb b/app/services/notifications/create_from_model_service/news_strategy.rb index b069d337e88..b5181aaf76d 100644 --- a/app/services/notifications/create_from_model_service/news_strategy.rb +++ b/app/services/notifications/create_from_model_service/news_strategy.rb @@ -31,7 +31,7 @@ module Notifications::CreateFromModelService::NewsStrategy %i(subscribed) end - def self.permission + def self.permission(*) :view_news end diff --git a/app/services/notifications/create_from_model_service/wiki_page_strategy.rb b/app/services/notifications/create_from_model_service/wiki_page_strategy.rb index d67ee4b7490..47e32d4b57f 100644 --- a/app/services/notifications/create_from_model_service/wiki_page_strategy.rb +++ b/app/services/notifications/create_from_model_service/wiki_page_strategy.rb @@ -31,7 +31,7 @@ module Notifications::CreateFromModelService::WikiPageStrategy %i(watched subscribed) end - def self.permission + def self.permission(*) :view_wiki_pages end diff --git a/app/services/notifications/create_from_model_service/work_package_strategy.rb b/app/services/notifications/create_from_model_service/work_package_strategy.rb index d8db68c83f4..5e7310fa6c1 100644 --- a/app/services/notifications/create_from_model_service/work_package_strategy.rb +++ b/app/services/notifications/create_from_model_service/work_package_strategy.rb @@ -31,8 +31,14 @@ module Notifications::CreateFromModelService::WorkPackageStrategy %i(mentioned assigned responsible watched commented created processed prioritized scheduled shared) end - def self.permission - :view_work_packages + def self.permission(journal, _reason) + if journal&.restricted? + # we assume that if a journal is restricted it is a comment and respects the + # view comments with restricted visibility permissions + :view_comments_with_restricted_visibility + else + :view_work_packages + end end def self.supports_ian?(_reason) diff --git a/app/services/project_life_cycle_steps/preview_attributes_service.rb b/app/services/project_life_cycle_steps/preview_attributes_service.rb deleted file mode 100644 index 1c155ff7537..00000000000 --- a/app/services/project_life_cycle_steps/preview_attributes_service.rb +++ /dev/null @@ -1,47 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module ProjectLifeCycleSteps - class PreviewAttributesService < ::BaseServices::SetAttributes - def perform(*) - super.tap do |service_call| - clear_unchanged_fields(service_call) - end - end - - private - - def clear_unchanged_fields(service_call) - service_call - .result - .available_phases - .select(&:not_set?) - .each { _1.errors.clear } - end - end -end diff --git a/app/services/project_life_cycle_steps/update_service.rb b/app/services/project_life_cycle_steps/update_service.rb index a8a5a33aa92..369a19136ef 100644 --- a/app/services/project_life_cycle_steps/update_service.rb +++ b/app/services/project_life_cycle_steps/update_service.rb @@ -28,5 +28,11 @@ module ProjectLifeCycleSteps class UpdateService < ::BaseServices::Update + def after_perform(call) + project = call.result.project + project.touch_and_save_journals + + call + end end end diff --git a/app/services/users/replace_mentions_service.rb b/app/services/users/replace_mentions_service.rb index e4aface0388..b08aecd4cd9 100644 --- a/app/services/users/replace_mentions_service.rb +++ b/app/services/users/replace_mentions_service.rb @@ -61,7 +61,6 @@ module Users ) SQL }, - { class: MeetingContent, column: :text }, { class: Journal::MeetingContentJournal, column: :text }, { class: Message, column: :content }, { class: Journal::MessageJournal, column: :content }, diff --git a/app/services/work_packages/activities_tab/restricted_mentions_sanitizer.rb b/app/services/work_packages/activities_tab/restricted_mentions_sanitizer.rb new file mode 100644 index 00000000000..7e02e3dab6a --- /dev/null +++ b/app/services/work_packages/activities_tab/restricted_mentions_sanitizer.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +class WorkPackages::ActivitiesTab::RestrictedMentionsSanitizer + def self.sanitize(work_package, notes) + new(work_package, notes).call + end + + def initialize(work_package, notes) + @work_package = work_package + @notes = notes + end + + def call + return "" if notes.blank? + + convert_unmentionable_principals_to_plain_text + CGI.unescapeHTML(parser.to_html) + end + + private + + attr_reader :work_package, :notes + + def convert_unmentionable_principals_to_plain_text + mentionable_principals_ids = mentionable_principals.pluck(:id) + + parser.css("mention").each do |mention| + unless mentionable_principals_ids.include?(mention["data-id"].to_i) + mention.replace(mention.content) + end + end + end + + def parser + @parser ||= Nokogiri::HTML.fragment(notes) + end + + def mentionable_principals + @mentionable_principals ||= Queries::Principals::PrincipalQuery.new(user: User.current) + .where(:restricted_mentionable_on_work_package, "=", [work_package.id]) + .where(:status, "!", [Principal.statuses[:locked]]) + .where(:type, "=", %w[User Group]) + .results + end +end diff --git a/app/views/admin/settings/virus_scanning_settings/show.html.erb b/app/views/admin/settings/virus_scanning_settings/show.html.erb index b094a9e789b..dfbd8749893 100644 --- a/app/views/admin/settings/virus_scanning_settings/show.html.erb +++ b/app/views/admin/settings/virus_scanning_settings/show.html.erb @@ -36,6 +36,8 @@ See COPYRIGHT and LICENSE files for more details. ) %> +<%= render EnterpriseEdition::BannerComponent.new(:virus_scanning) %> + <%= styled_form_tag( admin_settings_virus_scanning_path, method: :patch, @@ -52,6 +54,7 @@ See COPYRIGHT and LICENSE files for more details. ["ClamAV (Socket)", :clamav_socket], ["ClamAV (Host)", :clamav_host] ], + disabled: !EnterpriseToken.allows_to?(:virus_scanning), container_class: "-wide", data: { action: "refresh-on-form-changes#triggerTurboStream" @@ -67,5 +70,7 @@ See COPYRIGHT and LICENSE files for more details. <%= render partial: "av_form", locals: { selected: Setting.antivirus_scan_mode } %> - <%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %> + <%= styled_button_tag t(:button_save), + disabled: !EnterpriseToken.allows_to?(:virus_scanning), + class: "-primary -with-icon icon-checkmark" %> <% end %> diff --git a/app/views/admin/settings/work_packages_general/_enterprise_feature_hint.html.erb b/app/views/admin/settings/work_packages_general/_enterprise_feature_hint.html.erb deleted file mode 100644 index 0dfed0a1db9..00000000000 --- a/app/views/admin/settings/work_packages_general/_enterprise_feature_hint.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<% unless EnterpriseToken.allows_to?(ee_feature) %> -
- <%= - angular_component_tag "opce-enterprise-banner", - inputs: { - collapsible: true, - textMessage: explanation, - topMargin: true, - moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:attribute_highlighting][:href] - } - %> -
-<% end %> diff --git a/app/views/admin/settings/work_packages_general/show.html.erb b/app/views/admin/settings/work_packages_general/show.html.erb index a9bcc82aff0..a62ee75ddc9 100644 --- a/app/views/admin/settings/work_packages_general/show.html.erb +++ b/app/views/admin/settings/work_packages_general/show.html.erb @@ -63,16 +63,11 @@ See COPYRIGHT and LICENSE files for more details. options[:disabled] = "disabled" unless EnterpriseToken.allows_to?(:conditional_highlighting) end ) %> - <%= render partial: "enterprise_feature_hint", - locals: { - ee_feature: :conditional_highlighting, - explanation: t("js.work_packages.table_configuration.upsale.attribute_highlighting"), - link_out: { - href: "https://www.openproject.org/enterprise-edition/?op_edition=community-edition&op_referrer=settings-wp-attribute-highlighting#attribute-highlighting", - caption: t("js.work_packages.table_configuration.upsale.check_out_link") - } - } %> - +
+ <%= + render(EnterpriseEdition::BannerComponent.new(:conditional_highlighting, mt: 2)) + %> +
<% if EnterpriseToken.allows_to? :conditional_highlighting %>
"> <%= setting_multiselect( diff --git a/app/views/common/upsale.html.erb b/app/views/common/upsale.html.erb deleted file mode 100644 index 0ec58904a67..00000000000 --- a/app/views/common/upsale.html.erb +++ /dev/null @@ -1,64 +0,0 @@ -<% more_info = local_assigns[:feature_more_info] %> -<% feature_video = local_assigns[:feature_video] %> -<% feature_image = local_assigns[:feature_image] %> -<% additional_classes = local_assigns[:additional_classes] %> -<% write_augur_to_gon %> - -<% if !EnterpriseToken.current.present? %> - <% write_trial_key_to_gon %> -<% end %> - -
-

<%= feature_title %>

- <% if feature_description.empty? %> -

- <%= t("js.admin.enterprise.upsale.benefits.description") %> -
- <%= t("js.admin.enterprise.upsale.benefits.premium_features_text") %> -
- <%= t("js.admin.enterprise.upsale.benefits.professional_support_text") %> -

- <% else %> -

- <%= feature_description %> -

- <% end %> - -

- <%= more_info %> -

- - <% if feature_video.present? %> - <%= video_tag feature_video, controls: false, class: "widget-box--teaser-video", autoplay: true, loop: true, muted: true %> - <% elsif feature_image.present? %> - <%= image_tag feature_image, class: "widget-box--teaser-image" %> - <% else %> - <%= image_tag "enterprise-add-on.svg", class: "widget-box--teaser-image_default" %> - <% end %> - -

<%= feature_title %> <%= t("admin.enterprise.enterprise_info_html") %> -
- <%= t("admin.enterprise.upgrade_info") %> -

- <%= link_to( - OpenProject::Static::Links.links[:contact_us][:href], - { class: "button", - aria: { label: t("admin.enterprise.buttons.contact") }, - target: "_blank", - title: t("admin.enterprise.buttons.contact") } - ) do %> - <%= t("admin.enterprise.buttons.contact") %> - <% end %> - - <%= link_to( - OpenProject::Static::Links.links[:pricing][:href], - { class: "button -primary", - aria: { label: t("admin.enterprise.buttons.upgrade") }, - target: "_blank", - title: t("admin.enterprise.buttons.upgrade") } - ) do %> - <%= spot_icon("enterprise-addons") %> - <%= t("admin.enterprise.buttons.upgrade") %> - <% end %> - -
diff --git a/modules/meeting/app/views/meeting_contents/show.html.erb b/app/views/custom_actions/upsale.html.erb similarity index 79% rename from modules/meeting/app/views/meeting_contents/show.html.erb rename to app/views/custom_actions/upsale.html.erb index 785b51b1236..dd1c45083f9 100644 --- a/modules/meeting/app/views/meeting_contents/show.html.erb +++ b/app/views/custom_actions/upsale.html.erb @@ -27,10 +27,11 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= render partial: "meeting_contents/show", - locals: { - content: @content, - journaled_version: @journaled_version, - content_type: @content_type, - title: "#{t(:"label_#{@content_type}")}: #{link_to @meeting, @meeting}" - } %> +<% html_title t(:label_administration), t("ee.upsale.custom_actions.title") %> + +<%= + render EnterpriseEdition::UpsalePageComponent.new( + :custom_actions, + video: "enterprise/custom-actions.mp4" + ) +%> diff --git a/app/views/custom_fields/_form.html.erb b/app/views/custom_fields/_form.html.erb index ec51f911e44..933dbd6de5a 100644 --- a/app/views/custom_fields/_form.html.erb +++ b/app/views/custom_fields/_form.html.erb @@ -56,14 +56,9 @@ See COPYRIGHT and LICENSE files for more details. "admin--custom-fields-target": "format" } %>
> - <%= angular_component_tag "opce-enterprise-banner", - inputs: { - collapsible: true, - topMargin: true, - opReferrer: "custom-field-hierarchy", - textMessage: I18n.t("custom_fields.upsale.custom_field_format_hierarchy"), - moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:boards][:href] - } %> + <%= + render(EnterpriseEdition::BannerComponent.new(:custom_field_hierarchies, mt: 2)) + %>
diff --git a/app/views/custom_styles/upsale.html.erb b/app/views/custom_styles/upsale.html.erb index 0dd36994624..c96fa5e4ae6 100644 --- a/app/views/custom_styles/upsale.html.erb +++ b/app/views/custom_styles/upsale.html.erb @@ -29,11 +29,7 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_administration), t(:label_custom_style) %> -<%= render template: "common/upsale", - locals: { - feature_title: t(:label_custom_style), - feature_description: t("admin.custom_styles.customize"), - feature_more_info: t("admin.custom_styles.enterprise_more_info"), - feature_reference: "enterprise-custom-styles", - feature_video: "enterprise/custom-design.mp4" - } %> +<%= render EnterpriseEdition::UpsalePageComponent.new( + :define_custom_style, + video: "enterprise/custom-design.mp4" +) %> diff --git a/app/views/filters/_autocomplete.html.erb b/app/views/filters/_autocomplete.html.erb index 840ebb66ba5..9c3341f1dc5 100644 --- a/app/views/filters/_autocomplete.html.erb +++ b/app/views/filters/_autocomplete.html.erb @@ -19,6 +19,5 @@ # the action can't be registered on the input field at the time # of the #connect lifecycle hook of the filter--filters-form # Stimulus controller. - }.merge(autocomplete_options.except(:component)) - %> + }.merge(autocomplete_options.except(:component)) %> <% end %> diff --git a/app/views/notifications/date_alerts.html.erb b/app/views/notifications/date_alerts.html.erb index 7bd17ca6e45..a608afa64c2 100644 --- a/app/views/notifications/date_alerts.html.erb +++ b/app/views/notifications/date_alerts.html.erb @@ -1,5 +1,6 @@ <% html_title t("js.notifications.title") %> -<% content_for :content_body do %> - <%= angular_component_tag "opce-ian-date-alerts-upsale" %> -<% end %> +<%= render EnterpriseEdition::UpsalePageComponent.new( + :date_alerts, + video: "enterprise/date-alert-notifications.mp4" +) %> diff --git a/app/views/notifications/share_upsale.html.erb b/app/views/notifications/share_upsale.html.erb index 6b72a636ae6..6bd4c849dbc 100644 --- a/app/views/notifications/share_upsale.html.erb +++ b/app/views/notifications/share_upsale.html.erb @@ -1,5 +1,6 @@ <% html_title t("js.notifications.title") %> -<% content_for :content_body do %> - <%= angular_component_tag "opce-share-upsale" %> -<% end %> +<%= render EnterpriseEdition::UpsalePageComponent.new( + :work_package_sharing, + video: "enterprise/share-work-package.mp4" +) %> diff --git a/app/views/placeholder_users/index.html.erb b/app/views/placeholder_users/index.html.erb index 64afcc31938..228f9481e81 100644 --- a/app/views/placeholder_users/index.html.erb +++ b/app/views/placeholder_users/index.html.erb @@ -62,11 +62,6 @@ See COPYRIGHT and LICENSE files for more details. <%= render PlaceholderUsers::TableComponent.new(rows: @placeholder_users) %> <% else %> - <%= render template: "common/upsale", - locals: { - feature_title: I18n.t("placeholder_users.upsale.title"), - feature_description: I18n.t("placeholder_users.upsale.description"), - feature_reference: "placeholder_users", - feature_video: "enterprise/placeholder_users.mp4" - } %> + <%= render EnterpriseEdition::UpsalePageComponent.new(:placeholder_users, + video: "enterprise/placeholder_users.mp4") %> <% end %> diff --git a/app/views/project_phases/hover_card/show.html.erb b/app/views/project_phases/hover_card/show.html.erb new file mode 100644 index 00000000000..5edea1318e2 --- /dev/null +++ b/app/views/project_phases/hover_card/show.html.erb @@ -0,0 +1,3 @@ + + <%= render Projects::Phases::HoverCardComponent.new(phase: @phase, gate: @gate) %> + diff --git a/app/views/projects/settings/modules/_form.html.erb b/app/views/projects/settings/modules/_form.html.erb index da66258b55d..8c0bc3a5294 100644 --- a/app/views/projects/settings/modules/_form.html.erb +++ b/app/views/projects/settings/modules/_form.html.erb @@ -33,7 +33,8 @@ See COPYRIGHT and LICENSE files for more details.
- - - + diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.sass b/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.sass index 994d8398d9e..7b5ac8afe3b 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.sass +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.sass @@ -65,3 +65,7 @@ $board-list-top-offset: 18px .boards-list--tooltip align-self: start margin-top: $board-list-top-offset + +.boards-list--enterprise-banner + display: block + margin-left: $spot-spacing-1 diff --git a/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts b/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts index a16250bd449..94c382b289e 100644 --- a/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts +++ b/frontend/src/app/features/boards/board/board-partitioned-page/board-list-container.component.ts @@ -60,9 +60,6 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements addList: this.I18n.t('js.boards.add_list'), unnamedList: this.I18n.t('js.boards.label_unnamed_list'), hiddenListWarning: this.I18n.t('js.boards.text_hidden_list_warning'), - teaser_text: this.I18n.t('js.boards.upsale.teaser_text'), - upgrade_to_ee_text: this.I18n.t('js.boards.upsale.upgrade'), - more_info_link: enterpriseDocsUrl.boards, }; /** Container reference */ @@ -91,7 +88,7 @@ export class BoardListContainerComponent extends UntilDestroyedMixin implements showHiddenListWarning:boolean = false; - needEnterpriseEdition = this.Banner.eeShowBanners; + needEnterpriseEdition = this.Banner.showBannerFor('board_view'); private currentQueryUpdatedMonitoring:Subscription; @@ -129,7 +126,7 @@ readonly I18n:I18nService, ); this.board$.subscribe((board) => { - this.needEnterpriseEdition = this.Banner.eeShowBanners && !board.isFree; + this.needEnterpriseEdition = this.Banner.showBannerFor('board_view') && !board.isFree; }); this.Boards.currentBoard$.next(id); diff --git a/frontend/src/app/features/boards/openproject-boards.module.ts b/frontend/src/app/features/boards/openproject-boards.module.ts index ccf84715db0..65847007c8d 100644 --- a/frontend/src/app/features/boards/openproject-boards.module.ts +++ b/frontend/src/app/features/boards/openproject-boards.module.ts @@ -52,6 +52,7 @@ import { SubprojectBoardHeaderComponent } from 'core-app/features/boards/board/b import { SubtasksBoardHeaderComponent } from 'core-app/features/boards/board/board-actions/subtasks/subtasks-board-header.component'; import { StatusBoardHeaderComponent } from 'core-app/features/boards/board/board-actions/status/status-board-header.component'; import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autocompleter/openproject-autocompleter.module'; +import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openproject-enterprise.module'; @NgModule({ imports: [ @@ -59,6 +60,7 @@ import { OpenprojectAutocompleterModule } from 'core-app/shared/components/autoc OpenprojectWorkPackagesModule, OpenprojectModalModule, OpenprojectAutocompleterModule, + OpenprojectEnterpriseModule, // Dynamic Module for actions DynamicModule, diff --git a/frontend/src/app/features/enterprise/enterprise-banner-frame.component.html b/frontend/src/app/features/enterprise/enterprise-banner-frame.component.html new file mode 100644 index 00000000000..7aa8d8d3aff --- /dev/null +++ b/frontend/src/app/features/enterprise/enterprise-banner-frame.component.html @@ -0,0 +1,6 @@ + + diff --git a/frontend/src/app/features/hal/resources/meeting-content-resource.ts b/frontend/src/app/features/enterprise/enterprise-banner-frame.component.ts similarity index 58% rename from frontend/src/app/features/hal/resources/meeting-content-resource.ts rename to frontend/src/app/features/enterprise/enterprise-banner-frame.component.ts index 00e80326f49..b3f9cb4b6d8 100644 --- a/frontend/src/app/features/hal/resources/meeting-content-resource.ts +++ b/frontend/src/app/features/enterprise/enterprise-banner-frame.component.ts @@ -26,19 +26,32 @@ // See COPYRIGHT and LICENSE files for more details. //++ -import { HalResource } from 'core-app/features/hal/resources/hal-resource'; -import { Attachable } from 'core-app/features/hal/resources/mixins/attachable-mixin'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; +import { BannersService } from 'core-app/core/enterprise/banners.service'; -export interface MeetingContentResourceLinks { - addAttachment(attachment:HalResource):Promise; +@Component({ + templateUrl: './enterprise-banner-frame.component.html', + selector: 'op-enterprise-banner-frame', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class EnterpriseBannerFrameComponent implements OnInit { + @Input() public feature:string; + + @Input() public dismissable = false; + + visible:boolean; + frameURL:string; + + constructor( + protected pathHelper:PathHelperService, + protected banners:BannersService, + ) { + } + + ngOnInit() { + this.visible = this.banners.showBannerFor(this.feature); + this.frameURL = this.pathHelper.bannerFramePath(this.feature, this.dismissable); + } } - -class MeetingContentBaseResource extends HalResource { - public $links:MeetingContentResourceLinks; - - private attachmentsBackend = false; -} - -export const MeetingContentResource = Attachable(MeetingContentBaseResource); - -export type MeetingContentResource = HalResource; diff --git a/frontend/src/app/features/enterprise/free-trial-button/free-trial-button.component.html b/frontend/src/app/features/enterprise/free-trial-button/free-trial-button.component.html index c42e0558a60..5342ba5ee8c 100644 --- a/frontend/src/app/features/enterprise/free-trial-button/free-trial-button.component.html +++ b/frontend/src/app/features/enterprise/free-trial-button/free-trial-button.component.html @@ -4,7 +4,7 @@ - -
- - - - - {{text.you_contribute}} - - - - - - diff --git a/frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.sass b/frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.sass deleted file mode 100644 index 52a027076d2..00000000000 --- a/frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.sass +++ /dev/null @@ -1,52 +0,0 @@ -@import '../../app/spot/styles/sass/variables' - -.op-enterprise-banner - - &--header - font-size: 20px - line-height: 24px - display: grid - grid-template-columns: auto auto 1fr - align-items: center - - &--title - margin-left: $spot-spacing-0_5 - - &--description - display: block - @include spot-body-small - margin-bottom: $spot-spacing-0_75 - - &--image - width: 115px - height: 75px - padding-right: 20px - margin: auto - - &--buttons - display: flex - justify-content: flex-end - align-items: center - - a.button - text-decoration: none - - &--info-button - padding-top: $spot-spacing-1 - padding-right: $spot-spacing-1_5 - display: flex - justify-content: center - align-items: center - text-decoration: none - - .spot-icon:before - padding-right: 0.875rem - - &--collapsible-button - background: transparent - color: $spot-color-main - border: 0 - padding: $spot-spacing-0_5 $spot-spacing-0_5 - cursor: pointer - display: flex - justify-content: flex-end diff --git a/frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.ts b/frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.ts deleted file mode 100644 index fc6af3c8022..00000000000 --- a/frontend/src/app/shared/components/enterprise-banner/enterprise-banner.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Injector, Input, OnInit } from '@angular/core'; -import { BannersService } from 'core-app/core/enterprise/banners.service'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { imagePath } from 'core-app/shared/helpers/images/path-helper'; -import { OpModalService } from '../modal/modal.service'; -import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; -import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; -import { pricingUrl } from 'core-app/core/setup/globals/constants.const'; - -@Component({ - selector: 'op-enterprise-banner', - changeDetection: ChangeDetectionStrategy.OnPush, - styleUrls: ['./enterprise-banner.component.sass'], - templateUrl: './enterprise-banner.component.html', -}) -export class EnterpriseBannerComponent implements OnInit { - @Input() public leftMargin = false; - - @Input() public topMargin = false; - - @Input() public textMessage:string; - - @Input() public linkMessage:string; - - @Input() public opReferrer:string; - - @Input() public moreInfoLink:string; - - @Input() public collapsible:boolean; - - public collapsed = false; - - link:string; - - pricingUrl = pricingUrl; - - text = { - enterpriseFeature: this.I18n.t('js.upsale.ee_only'), - become_hero: this.I18n.t('js.admin.enterprise.upsale.become_hero'), - you_contribute: this.I18n.t('js.admin.enterprise.upsale.you_contribute'), - button_trial: this.I18n.t('js.admin.enterprise.upsale.button_start_trial'), - upgrade: this.I18n.t('js.admin.enterprise.upsale.button_upgrade'), - more_info_link: `${this.pathHelper.appBasePath}/admin/enterprise`, - more_info_text: this.I18n.t('js.admin.enterprise.upsale.more_info'), - }; - - image = { - enterprise_edition: imagePath('enterprise-add-on.svg'), - }; - - constructor( - readonly elementRef:ElementRef, - protected I18n:I18nService, - protected bannersService:BannersService, - protected opModalService:OpModalService, - readonly injector:Injector, - readonly pathHelper:PathHelperService, - ) { - populateInputsFromDataset(this); - } - - ngOnInit():void { - this.link = this.bannersService.getEnterPriseEditionUrl({ referrer: this.opReferrer }); - this.collapsed = this.collapsible; - } - - toggleCollapse():void { - this.collapsed = !this.collapsed; - } -} diff --git a/frontend/src/app/shared/components/enterprise-page/enterprise-page.component.html b/frontend/src/app/shared/components/enterprise-page/enterprise-page.component.html deleted file mode 100644 index 37870323435..00000000000 --- a/frontend/src/app/shared/components/enterprise-page/enterprise-page.component.html +++ /dev/null @@ -1,51 +0,0 @@ -
-

-

{{feature_description}}

-

- {{text.benefits_description}} -
- {{text.premium_features_text}} -
- {{text.professional_support_text}} -

- -

- - - - - - -

-

{{text.upgrade_info}}

- - - {{ text.button_contact_us }} - - - - - {{ text.upgrade }} - - -
diff --git a/frontend/src/app/shared/components/enterprise-page/enterprise-page.component.ts b/frontend/src/app/shared/components/enterprise-page/enterprise-page.component.ts deleted file mode 100644 index d2313ffa800..00000000000 --- a/frontend/src/app/shared/components/enterprise-page/enterprise-page.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core'; -import { I18nService } from 'core-app/core/i18n/i18n.service'; -import { imagePath } from 'core-app/shared/helpers/images/path-helper'; -import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; -import { contactUrl, pricingUrl } from 'core-app/core/setup/globals/constants.const'; - - -@Component({ - selector: 'op-enterprise-page', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './enterprise-page.component.html', -}) -export class EnterprisePageComponent { - @Input() public feature_more_info:string; - - @Input() public feature_video:string; - - @Input() public feature_image:string; - - @Input() public feature_title:string; - - @Input() public feature_description:string; - - @Input() public more_info:string; - - @Input() public hide_breadcrumb = false; - - text = { - button_contact_us: this.I18n.t('js.admin.enterprise.upsale.button_contact_us'), - upgrade: this.I18n.t('js.admin.enterprise.upsale.button_upgrade'), - upgrade_link: pricingUrl, - contact_link: contactUrl.en, - benefits_description: this.I18n.t('js.admin.enterprise.upsale.benefits.description'), - premium_features_text: this.I18n.t('js.admin.enterprise.upsale.benefits.premium_features_text'), - professional_support_text: this.I18n.t('js.admin.enterprise.upsale.benefits.professional_support_text'), - enterprise_info_html: (feature_title:string):string => this.I18n.t('js.admin.enterprise.upsale.enterprise_info_html', { - feature_title, - }), - upgrade_info: this.I18n.t('js.admin.enterprise.upsale.upgrade_info'), - button_contact: this.I18n.t('js.admin.enterprise.upsale.buttons.contact'), - button_upgrade: this.I18n.t('js.admin.enterprise.upsale.buttons.upgrade'), - }; - - image = { - enterprise_edition: imagePath('enterprise-add-on.svg'), - }; - - constructor( - readonly elementRef:ElementRef, - protected I18n:I18nService, - readonly pathHelper:PathHelperService, - ) { - - } -} diff --git a/frontend/src/app/shared/components/grids/openproject-grids.module.ts b/frontend/src/app/shared/components/grids/openproject-grids.module.ts index b462966ea91..aa4072eeb99 100644 --- a/frontend/src/app/shared/components/grids/openproject-grids.module.ts +++ b/frontend/src/app/shared/components/grids/openproject-grids.module.ts @@ -85,6 +85,7 @@ import { WidgetProjectFavoritesComponent, } from 'core-app/shared/components/grids/widgets/project-favorites/widget-project-favorites.component'; import { IconModule } from 'core-app/shared/components/icon/icon.module'; +import { OpenprojectEnterpriseModule } from 'core-app/features/enterprise/openproject-enterprise.module'; @NgModule({ imports: [ @@ -97,6 +98,7 @@ import { IconModule } from 'core-app/shared/components/icon/icon.module'; OpenprojectWorkPackageGraphsModule, OpenprojectCalendarModule, OpenprojectTimeEntriesModule, + OpenprojectEnterpriseModule, OpenprojectAttachmentsModule, diff --git a/frontend/src/app/shared/components/grids/widgets/add/add.modal.html b/frontend/src/app/shared/components/grids/widgets/add/add.modal.html index cfdc1b30433..ac2a64d572a 100644 --- a/frontend/src/app/shared/components/grids/widgets/add/add.modal.html +++ b/frontend/src/app/shared/components/grids/widgets/add/add.modal.html @@ -7,16 +7,10 @@
- - - +
  • - <%= link_to({ controller: "/#{@content_type.pluralize}", action: "history", meeting_id: @meeting }, class: "button") do %> - <%= op_icon("button--icon icon-wiki") %> - <%= t(:label_history) %> - <% end %> -
  • - <% end %> -<% end %> - -

    -<%= t(:label_version) %> <%= link_to @diff.content_from.version, send(:"#{@content_type}_version_path", @meeting, @diff.content_from.version) %> -(<%= link_to_user(@diff.content_from.user) %>, <%= format_time(@diff.content_from.created_at) %>) -→ -<%= t(:label_version) %> <%= link_to @diff.content_to.version, send(:"#{@content_type}_version_path", @meeting, @diff.content_to.version) %>/<%= @content.last_journal&.version || 0 %> -(<%= link_to_user(@diff.content_to.user) %>, <%= format_time(@diff.content_to.created_at) %>) -

    - -
    - -
    -<%= simple_format_without_paragraph @diff.to_html %> -
    diff --git a/modules/meeting/app/views/meeting_contents/history.html.erb b/modules/meeting/app/views/meeting_contents/history.html.erb deleted file mode 100644 index 261849c3f42..00000000000 --- a/modules/meeting/app/views/meeting_contents/history.html.erb +++ /dev/null @@ -1,148 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<% html_title "#{t(:label_history)}: #{@meeting.title}" %> - -<%= toolbar title: t(:"label_#{@content_type}"), - link_to: link_to(@meeting, @meeting) %> - -

    <%= t(:label_history) %>

    - -<%= form_tag({ action: "diff" }, method: :get) do %> -
    -
    - - - - - - - - - - - - - - - - - - - - - <% show_diff = @content_versions.size > 1 %> - <% @content_versions.each_with_index do |content_version,index| %> - - - - - - - - - <% end %> - -
    -
    -
    - # - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - <%= Meeting.human_attribute_name(:updated_at) %> - -
    -
    -
    -
    -
    - - <%= Meeting.human_attribute_name(:author) %> - -
    -
    -
    -
    -
    - - <%= Meeting.human_attribute_name(:comments) %> - -
    -
    -
    - <%= if content_version.version == @content.last_journal.version - link_to(content_version.version, tab_meeting_path(@meeting, tab: @content_type.sub(/^meeting_/, "")), id: "version-#{content_version.version}") - else - link_to(content_version.version, send(:"#{@content_type}_version_path", @meeting, content_version.version), id: "version-#{content_version.version}") - end %> - - <% if show_diff && (index < @content_versions.size-1) %> - <%= radio_button_tag( - "version_from", - content_version.version, - (index == 0), - class: "meetings--checkbox-version-to", - data: { - "journal-history-target": "fromVersion", - action: "journal-history#selectToVersion" - } - ) %> - - <% end %> - - <% if show_diff && (index > 0) %> - <%= radio_button_tag( - "version_to", - content_version.version, - (index == 1), - data: { - "journal-history-target": "toVersion", - action: "journal-history#selectFromVersion" - } - ) %> - - <% end %> - <%= format_time(content_version.created_at) %><%= User.find content_version.user_id %><%= h content_version.notes %>
    - -
    -
    -<%= styled_button_tag t(:label_view_diff), class: "-small -primary" if show_diff %> -<%= pagination_links_full @content_versions %> -<% end %> diff --git a/modules/meeting/app/views/meetings/_form.html.erb b/modules/meeting/app/views/meetings/_form.html.erb deleted file mode 100644 index 687604e9aeb..00000000000 --- a/modules/meeting/app/views/meetings/_form.html.erb +++ /dev/null @@ -1,156 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<%= error_messages_for "meeting" %> - -
    - -
    - <%= f.text_field :title, required: true, size: 60, container_class: "-wide" %> -
    - - <% copy_from = local_assigns[:copy_from] %> - <% if copy_from.present? %> - <%= f.hidden_field :type, value: copy_from.type %> - <%= hidden_field_tag "copied_from_meeting_id", copy_from.id %> -
    - <%= styled_label_tag "copy_agenda", t("meeting.copy.agenda") %> -
    - <%= styled_check_box_tag "copy_agenda", - 1, - true, - no_label: true, - class: "radio-button" %> -
    -
    - <%= t("meeting.copy.agenda_text") %> -
    -
    -
    - <%= styled_label_tag "copy_attachments", t("meeting.copy.attachments") %> -
    - <%= styled_check_box_tag "copy_attachments", - 1, - false, - no_label: true, - class: "radio-button" %> -
    -
    - <%= t("meeting.copy.attachments_text") %> -
    -
    - <% elsif @meeting.new_record? %> - <%= f.hidden_field :type, value: "Meeting" %> - <% end %> - - <% if global_meeting_create_context? %> -
    - -
    - <%= angular_component_tag "opce-project-autocompleter", - inputs: { - filters: [{ name: "user_action", operator: "=", values: ["meetings/create"] }], - inputName: "project_id", - inputValue: @project&.id, - appendTo: "body", - hiddenFieldAction: "change->refresh-on-form-changes#triggerTurboStream" - }, - id: "project_id", - class: "form--select-container -wide remote-field--input", - data: { - "test-selector": "project_id" - } %> -
    -
    - <% end %> - -
    - <%= f.text_field :location, size: 60, container_class: "-wide" %> -
    - -
    - - -
    - <%= f.date_picker :start_date, - id: "meeting-form-start-date", - required: true, - no_label: true %> - - <%= f.text_field :start_time_hour, - id: "meeting-form-start-time", - required: true, - type: "time", - no_label: true, - step: 5.minutes, - suffix: formatted_time_zone_offset, - container_class: "-xslim" %> -
    -
    - -
    - -
    - <%= f.number_field :duration, - id: "meeting-form-duration", - required: true, - size: 5, - no_label: true, - min: 0.00, - step: 0.25, - max: 168, - suffix: t(:text_in_hours), - container_class: "-xslim" %> -
    -
    - - <%= render partial: "meetings/participants_section" %> - -
    - <%= styled_label_tag "send_notfications", t(:"meeting.email.send_emails") %> -
    - <%= styled_check_box_tag "send_notifications", - 1, - false %> -
    -
    - <%= t(:"meeting.email.send_invitation_emails") %> -
    -
    -
    diff --git a/modules/meeting/app/views/meetings/new.html.erb b/modules/meeting/app/views/meetings/new.html.erb deleted file mode 100644 index 1ea14428b71..00000000000 --- a/modules/meeting/app/views/meetings/new.html.erb +++ /dev/null @@ -1,73 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> -<% copy_from = local_assigns[:copy_from] %> -<% if copy_from - page_title = t("meeting.copy.title", title: copy_from.title) - breadcrumb_element = t("meeting.copy.title", title: "#{copy_from.title}") - else - page_title = t(:label_meeting_new) - breadcrumb_element = t(:label_meeting_new) - end %> - -<% html_title page_title %> - -<%= - render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title { page_title } - header.with_breadcrumbs( - [if @project.present? - { href: project_overview_path(@project.id), text: @project.name } - else - { href: home_path, text: organization_name } - end, - { href: @project.present? ? project_meetings_path(@project.id) : meetings_path, - text: I18n.t(:label_meeting_plural) }, - breadcrumb_element.html_safe] - ) - end -%> - -<%= labelled_tabular_form_for @meeting, - as: :meeting, - url: { controller: "/meetings", action: "create", project_id: @project }, - html: { - id: "meeting-form", - target: "_top", - data: { - turbo: false, - controller: "refresh-on-form-changes", - "refresh-on-form-changes-target": "form", - "refresh-on-form-changes-turbo-stream-url-value": new_meeting_url - } - } do |f| -%> - <%= render partial: "form", locals: { f:, copy_from: } %> - <%= styled_button_tag t(:button_create), class: "-primary" %> - <%= link_to t(:button_cancel), { action: "index", project_id: @project }, - class: "button" %> -<% end %> diff --git a/modules/meeting/app/views/meetings/show.html.erb b/modules/meeting/app/views/meetings/show.html.erb deleted file mode 100644 index 85880071451..00000000000 --- a/modules/meeting/app/views/meetings/show.html.erb +++ /dev/null @@ -1,154 +0,0 @@ -<%#-- copyright -OpenProject is an open source project management software. -Copyright (C) the OpenProject GmbH - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License version 3. - -OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -Copyright (C) 2006-2013 Jean-Philippe Lang -Copyright (C) 2010-2013 the ChiliProject Team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -See COPYRIGHT and LICENSE files for more details. - -++#%> - -<%= toolbar title: t(:label_meeting), - link_to: link_to(@meeting), - html: { class: "meeting--main-toolbar -with-dropdown" } do %> - <% unless User.current.anonymous? %> -
  • -
    - <%= watcher_link @meeting, User.current %> -
    -
  • - <% end %> - <% if authorize_for(:meetings, :edit) %> -
  • - <%= link_to(edit_project_meeting_path(@project, @meeting), class: "button", accesskey: accesskey(:edit)) do %> - <%= op_icon("button--icon icon-edit") %> - <%= t(:button_edit) %> - <% end %> -
  • - <% end %> - - -<% end %> - -
    -
    -
    - <%= avatar(@meeting.author) %> -

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

    -
    -
    -

    - <%= Meeting.human_attribute_name(:start_time) %>: <%= format_date @meeting.start_time %> <%= format_time @meeting.start_time, include_date: false %> - - <%= format_time @meeting.end_time, include_date: false %> <%= formatted_time_zone_offset %>

    -
    -
    -

    - <%= Meeting.human_attribute_name(:location) %>: <%= auto_link(h(@meeting.location), link: :all, html: { target: "_blank" }) %> -

    -
    -
    -

    - <%= Meeting.human_attribute_name(:participants_invited) %>: <%= format_participant_list @meeting.participants.invited %> -

    -
    -
    -

    - <%= Meeting.human_attribute_name(:participants_attended) %>: <%= format_participant_list @meeting.participants.attended %> -

    -
    -
    -
    - -<%= render_tabs( - [ - { name: "agenda", - action: :create_meeting_agendas, - partial: "meeting_contents/show", - path: meeting_agenda_path(@meeting), - label: :label_meeting_agenda, - content: @meeting.agenda || @meeting.build_agenda, - content_type: "meeting_agenda" }, - { - name: "minutes", - action: :create_meeting_minutes, - partial: "meeting_contents/show", - path: meeting_minutes_path(@meeting), - label: :label_meeting_minutes, content: @meeting.minutes || @meeting.build_minutes, - content_type: "meeting_minutes" - } - ] - ) %> - -<% if @meeting.journals.changing.present? %> -
    -

    <%= t(:label_history) %>

    - <% @meeting.journals.each do |journal| %> - <%= render_meeting_journal @meeting, journal %> - <% end %> -
    -<% end %> diff --git a/modules/meeting/config/locales/crowdin/af.yml b/modules/meeting/config/locales/crowdin/af.yml index 1113c17b775..bf00967e1ee 100644 --- a/modules/meeting/config/locales/crowdin/af.yml +++ b/modules/meeting/config/locales/crowdin/af.yml @@ -70,11 +70,9 @@ af: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ af: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ar.yml b/modules/meeting/config/locales/crowdin/ar.yml index 55d73ae2ef0..707b4581537 100644 --- a/modules/meeting/config/locales/crowdin/ar.yml +++ b/modules/meeting/config/locales/crowdin/ar.yml @@ -74,11 +74,9 @@ ar: invalid_time_format: "ليس وقتًا صالحًا. التصميم المطلوب: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "الاجتماع" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "جدول الأعمال" - meeting_minutes: "محضر الجلسة" meeting_section: "Section" activity: filter: @@ -233,9 +231,6 @@ ar: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/az.yml b/modules/meeting/config/locales/crowdin/az.yml index b0cba020d43..5e1f8a2ecb8 100644 --- a/modules/meeting/config/locales/crowdin/az.yml +++ b/modules/meeting/config/locales/crowdin/az.yml @@ -70,11 +70,9 @@ az: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ az: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/be.yml b/modules/meeting/config/locales/crowdin/be.yml index 75d27ac9df6..f35bc3fa9d0 100644 --- a/modules/meeting/config/locales/crowdin/be.yml +++ b/modules/meeting/config/locales/crowdin/be.yml @@ -72,11 +72,9 @@ be: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Раздзел" activity: filter: @@ -225,9 +223,6 @@ be: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/bg.yml b/modules/meeting/config/locales/crowdin/bg.yml index c3c1397bdb6..98a47358cf9 100644 --- a/modules/meeting/config/locales/crowdin/bg.yml +++ b/modules/meeting/config/locales/crowdin/bg.yml @@ -70,11 +70,9 @@ bg: invalid_time_format: "не е валидно време. Задължителен формат: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "среща" + meeting: "One-time meeting" meeting_agenda_item: "Точка от дневния ред" meeting_agenda: "Дневен ред" - meeting_minutes: "Минути" meeting_section: "Раздел" activity: filter: @@ -217,9 +215,6 @@ bg: new_date_time: "Нова дата/час" label_mail_all_participants: "Изпрати имейл до всички участници" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ca.yml b/modules/meeting/config/locales/crowdin/ca.yml index a65f0d1ae37..c1fb78fa9fe 100644 --- a/modules/meeting/config/locales/crowdin/ca.yml +++ b/modules/meeting/config/locales/crowdin/ca.yml @@ -70,11 +70,9 @@ ca: invalid_time_format: "no és una hora vàlida. El format requerit és: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Reunió" + meeting: "One-time meeting" meeting_agenda_item: "Element d'agenda" meeting_agenda: "Agenda" - meeting_minutes: "Acta" meeting_section: "Secció" activity: filter: @@ -217,9 +215,6 @@ ca: new_date_time: "Data/hora nova" label_mail_all_participants: "Envia un correu a tots els participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ckb-IR.yml b/modules/meeting/config/locales/crowdin/ckb-IR.yml index 420191c7d02..0734d03c25a 100644 --- a/modules/meeting/config/locales/crowdin/ckb-IR.yml +++ b/modules/meeting/config/locales/crowdin/ckb-IR.yml @@ -70,11 +70,9 @@ ckb-IR: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ ckb-IR: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/cs.yml b/modules/meeting/config/locales/crowdin/cs.yml index bf45a4b194d..958c8e2ffd9 100644 --- a/modules/meeting/config/locales/crowdin/cs.yml +++ b/modules/meeting/config/locales/crowdin/cs.yml @@ -72,11 +72,9 @@ cs: invalid_time_format: "není platný čas. Požadovaný formát: HH:MM" models: recurring_meeting: "Opakovaná schůzka" - structured_meeting: "Jednorázová schůzka" - meeting: "Schůzka" + meeting: "One-time meeting" meeting_agenda_item: "Pořad jednání" meeting_agenda: "Agenda" - meeting_minutes: "Zápis" meeting_section: "Sekce" activity: filter: @@ -225,9 +223,6 @@ cs: new_date_time: "Nové datum/čas" label_mail_all_participants: "Poslat e-mail všem účastníkům" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Strukturované" one_time: "Jednorázový" recurring: "Opakované" recurring_text: "Vytvořte sérii schůzek s dynamickou šablonou pro každou událost." @@ -341,7 +336,7 @@ cs: notice_meeting_updated: "Tato stránka byla aktualizována někým jiným. Pro zobrazení změn znovu načtena." permission_create_meetings: "Vytvořit schůzku\n" permission_edit_meetings: "Upravit schůzku" - permission_delete_meetings: "Odstranit schůzky" + permission_delete_meetings: "Smazat schůzku" permission_view_meetings: "Zobrazit schůzky" permission_create_meeting_agendas: "Vytvořit agendy schůzek" permission_create_meeting_agendas_explanation: "Umožňuje upravovat obsah programu klasické schůzky." diff --git a/modules/meeting/config/locales/crowdin/da.yml b/modules/meeting/config/locales/crowdin/da.yml index 1e55cad97f9..989c69c6d0c 100644 --- a/modules/meeting/config/locales/crowdin/da.yml +++ b/modules/meeting/config/locales/crowdin/da.yml @@ -70,11 +70,9 @@ da: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Møde" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Dagsorden" - meeting_minutes: "Referat" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ da: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/de.yml b/modules/meeting/config/locales/crowdin/de.yml index a8f02b4bc48..eaab85b395b 100644 --- a/modules/meeting/config/locales/crowdin/de.yml +++ b/modules/meeting/config/locales/crowdin/de.yml @@ -70,11 +70,9 @@ de: invalid_time_format: "ist keine gültige Zeit. Erforderliches Format: HH:MM" models: recurring_meeting: "Wiederkehrende Besprechung" - structured_meeting: "Einmalige Besprechung" - meeting: "Besprechung" + meeting: "One-time meeting" meeting_agenda_item: "Tagesordnungspunkt" meeting_agenda: "Agenda" - meeting_minutes: "Protokoll" meeting_section: "Abschnitt" activity: filter: @@ -217,9 +215,6 @@ de: new_date_time: "Neue(s) Datum/Zeit" label_mail_all_participants: "E-Mail an alle Teilnehmer senden" types: - classic: "Klassisch (nicht unterstützt)" - classic_text: "Hinweis: Klassische Meetings werden in der nächsten Version von OpenProject entfernt." - structured: "Dynamisch" one_time: "Einmalig" recurring: "Terminserie" recurring_text: "Erstellen Sie eine Terminserie mit einer dynamischen Vorlage und wiederkehrenden Besprechungen." diff --git a/modules/meeting/config/locales/crowdin/el.yml b/modules/meeting/config/locales/crowdin/el.yml index a4721012abe..d517416a27a 100644 --- a/modules/meeting/config/locales/crowdin/el.yml +++ b/modules/meeting/config/locales/crowdin/el.yml @@ -70,11 +70,9 @@ el: invalid_time_format: "δεν είναι έγκυρη ώρα. Απαιτούμενη μορφοποίηση: ΩΩ:ΛΛ" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Συνάντηση" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Ατζέντα" - meeting_minutes: "Πρακτικά" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ el: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/eo.yml b/modules/meeting/config/locales/crowdin/eo.yml index f34cbd19092..ef127e9207e 100644 --- a/modules/meeting/config/locales/crowdin/eo.yml +++ b/modules/meeting/config/locales/crowdin/eo.yml @@ -70,11 +70,9 @@ eo: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ eo: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/es.yml b/modules/meeting/config/locales/crowdin/es.yml index 9480e1b6c09..2ea302c1a24 100644 --- a/modules/meeting/config/locales/crowdin/es.yml +++ b/modules/meeting/config/locales/crowdin/es.yml @@ -70,11 +70,9 @@ es: invalid_time_format: "no es una hora válida. Formato requerido: HH:MM" models: recurring_meeting: "Reunión periódica" - structured_meeting: "Reunión única" - meeting: "Reunión" + meeting: "Reunión única" meeting_agenda_item: "Puntos de Agenda" meeting_agenda: "Agenda" - meeting_minutes: "Minutas" meeting_section: "Sección" activity: filter: diff --git a/modules/meeting/config/locales/crowdin/et.yml b/modules/meeting/config/locales/crowdin/et.yml index 64754d4cb57..92fb3912f81 100644 --- a/modules/meeting/config/locales/crowdin/et.yml +++ b/modules/meeting/config/locales/crowdin/et.yml @@ -70,11 +70,9 @@ et: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Koosolek" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Päevakava" - meeting_minutes: "Minutit" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ et: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/eu.yml b/modules/meeting/config/locales/crowdin/eu.yml index 8f42af6048f..6761be6bc9b 100644 --- a/modules/meeting/config/locales/crowdin/eu.yml +++ b/modules/meeting/config/locales/crowdin/eu.yml @@ -70,11 +70,9 @@ eu: invalid_time_format: "ez da baliozko ordua. Eskatzen den formatua: OO:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Hitzordua" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Aktak" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ eu: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/fa.yml b/modules/meeting/config/locales/crowdin/fa.yml index 762a457007f..8a5d9b62b5c 100644 --- a/modules/meeting/config/locales/crowdin/fa.yml +++ b/modules/meeting/config/locales/crowdin/fa.yml @@ -70,11 +70,9 @@ fa: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "جلسه" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "دستور کار" - meeting_minutes: "دقیقه ها" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ fa: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/fi.yml b/modules/meeting/config/locales/crowdin/fi.yml index eb93eb0f076..b17a867b772 100644 --- a/modules/meeting/config/locales/crowdin/fi.yml +++ b/modules/meeting/config/locales/crowdin/fi.yml @@ -70,11 +70,9 @@ fi: invalid_time_format: "ei ole kelvollinen aika. Vaadittava muoto: TT:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Kokous" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Esityslista" - meeting_minutes: "Pöytäkirja" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ fi: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/fil.yml b/modules/meeting/config/locales/crowdin/fil.yml index 00885d93a85..177f9818089 100644 --- a/modules/meeting/config/locales/crowdin/fil.yml +++ b/modules/meeting/config/locales/crowdin/fil.yml @@ -70,11 +70,9 @@ fil: invalid_time_format: "ay hindi balidong oras. Ang hinihinging format ay: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Pagpupulong" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ fil: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/fr.yml b/modules/meeting/config/locales/crowdin/fr.yml index b8ae4ca1d4e..3e1504ae040 100644 --- a/modules/meeting/config/locales/crowdin/fr.yml +++ b/modules/meeting/config/locales/crowdin/fr.yml @@ -70,11 +70,9 @@ fr: invalid_time_format: "n’est pas une heure valide. Format requis : HH:MM" models: recurring_meeting: "Réunion récurrente" - structured_meeting: "Réunion ponctuelle" - meeting: "Réunion" + meeting: "Réunion ponctuelle" meeting_agenda_item: "" meeting_agenda: "Ordre du jour" - meeting_minutes: "Compte-rendu" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ fr: new_date_time: "Nouvelle date/heure" label_mail_all_participants: "Envoyer un mail à tous les participants" types: - classic: "Classique (non pris en charge)" - classic_text: "Note : les réunions classiques seront supprimées dans la prochaine version d'OpenProject." - structured: "Structurée" one_time: "Ponctuelle" recurring: "Récurrente" recurring_text: "Créez une série de réunions avec un modèle dynamique pour chaque occurrence." diff --git a/modules/meeting/config/locales/crowdin/he.yml b/modules/meeting/config/locales/crowdin/he.yml index 4c536423b50..c54c0f1e73e 100644 --- a/modules/meeting/config/locales/crowdin/he.yml +++ b/modules/meeting/config/locales/crowdin/he.yml @@ -72,11 +72,9 @@ he: invalid_time_format: "תבנית הזמן אינה תקינה. התבנית הנדרשת היא: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "פגישה" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "סדר היום" - meeting_minutes: "דקות" meeting_section: "Section" activity: filter: @@ -225,9 +223,6 @@ he: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/hi.yml b/modules/meeting/config/locales/crowdin/hi.yml index 364014c2466..05daf999429 100644 --- a/modules/meeting/config/locales/crowdin/hi.yml +++ b/modules/meeting/config/locales/crowdin/hi.yml @@ -70,11 +70,9 @@ hi: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ hi: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/hr.yml b/modules/meeting/config/locales/crowdin/hr.yml index 639952337f7..1967eb6f5ec 100644 --- a/modules/meeting/config/locales/crowdin/hr.yml +++ b/modules/meeting/config/locales/crowdin/hr.yml @@ -71,11 +71,9 @@ hr: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Sastanak" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Dnevni red" - meeting_minutes: "Minute" meeting_section: "Section" activity: filter: @@ -221,9 +219,6 @@ hr: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/hu.yml b/modules/meeting/config/locales/crowdin/hu.yml index 8d85ed6c45d..546b9306443 100644 --- a/modules/meeting/config/locales/crowdin/hu.yml +++ b/modules/meeting/config/locales/crowdin/hu.yml @@ -70,11 +70,9 @@ hu: invalid_time_format: "nem egy érvényes időpont. Előírt formátum: óó:pp" models: recurring_meeting: "Ismétlődő megbeszélés" - structured_meeting: "One-time meeting" - meeting: "Megbeszélés" + meeting: "One-time meeting" meeting_agenda_item: "Napirendi pont" meeting_agenda: "Napirend" - meeting_minutes: "Jegyzőkönyv" meeting_section: "Szekció " activity: filter: @@ -217,9 +215,6 @@ hu: new_date_time: "Új időpont" label_mail_all_participants: "Email küldése minden résztvevőnek" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/id.yml b/modules/meeting/config/locales/crowdin/id.yml index 5d6baefc6c8..84ca498ba8a 100644 --- a/modules/meeting/config/locales/crowdin/id.yml +++ b/modules/meeting/config/locales/crowdin/id.yml @@ -69,11 +69,9 @@ id: invalid_time_format: "bukanlah waktu yang valid. Format seharusnya; JJ:MM" models: recurring_meeting: "Rapat berulang" - structured_meeting: "Rapat satu waktu" - meeting: "Rapat" + meeting: "One-time meeting" meeting_agenda_item: "Item agenda" meeting_agenda: "Agenda" - meeting_minutes: "Laporan" meeting_section: "Bagian" activity: filter: @@ -213,9 +211,6 @@ id: new_date_time: "Tanggal/waktu baru" label_mail_all_participants: "Kirim email ke semua peserta" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/it.yml b/modules/meeting/config/locales/crowdin/it.yml index 55df9b8d62b..448ad2018e2 100644 --- a/modules/meeting/config/locales/crowdin/it.yml +++ b/modules/meeting/config/locales/crowdin/it.yml @@ -70,11 +70,9 @@ it: invalid_time_format: "non è un tempo valido. Formato richiesto: HH: mm" models: recurring_meeting: "Riunione ricorrente" - structured_meeting: "Riunione una tantum" - meeting: "Riunione" + meeting: "Riunione una tantum" meeting_agenda_item: "Attività" meeting_agenda: "Ordine del giorno" - meeting_minutes: "Verbali" meeting_section: "Sezione" activity: filter: @@ -217,9 +215,6 @@ it: new_date_time: "Nuova data/ora" label_mail_all_participants: "Invia e-mail a tutti i partecipanti" types: - classic: "Classica (non supportata)" - classic_text: "Nota: le riunioni classiche verranno rimosse nella prossima versione di OpenProject." - structured: "Strutturato" one_time: "Una tantum" recurring: "Ricorrente" recurring_text: "Crea una serie di riunioni con modello dinamico per ogni ricorrenza." diff --git a/modules/meeting/config/locales/crowdin/ja.yml b/modules/meeting/config/locales/crowdin/ja.yml index 9ce5c92d751..9198c371870 100644 --- a/modules/meeting/config/locales/crowdin/ja.yml +++ b/modules/meeting/config/locales/crowdin/ja.yml @@ -69,11 +69,9 @@ ja: invalid_time_format: "有効な時間ではありません。必要なフォーマット: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "会議" + meeting: "One-time meeting" meeting_agenda_item: "議題項目" meeting_agenda: "アジェンダ" - meeting_minutes: "議事録" meeting_section: "セクション" activity: filter: @@ -213,9 +211,6 @@ ja: new_date_time: "新しい日付/時刻" label_mail_all_participants: "すべての参加者にメールを送信" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ka.yml b/modules/meeting/config/locales/crowdin/ka.yml index 09f0cca68b2..7efe7341e53 100644 --- a/modules/meeting/config/locales/crowdin/ka.yml +++ b/modules/meeting/config/locales/crowdin/ka.yml @@ -70,11 +70,9 @@ ka: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "შეხვედრა" + meeting: "One-time meeting" meeting_agenda_item: "განრიგის პუნქტი" meeting_agenda: "დღის განრიგი" - meeting_minutes: "წუთი" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ ka: new_date_time: "ახალი თარიღი/დრო" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/kk.yml b/modules/meeting/config/locales/crowdin/kk.yml index 54c5f2ddbdc..1ba246d14e0 100644 --- a/modules/meeting/config/locales/crowdin/kk.yml +++ b/modules/meeting/config/locales/crowdin/kk.yml @@ -70,11 +70,9 @@ kk: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ kk: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ko.yml b/modules/meeting/config/locales/crowdin/ko.yml index 6dcd7435ad2..d47879081b1 100644 --- a/modules/meeting/config/locales/crowdin/ko.yml +++ b/modules/meeting/config/locales/crowdin/ko.yml @@ -69,11 +69,9 @@ ko: invalid_time_format: "은(는) 유효한 시간이 아닙니다. 필요한 형식: HH:MM" models: recurring_meeting: "반복 미팅" - structured_meeting: "일회성 미팅" - meeting: "미팅" + meeting: "일회성 미팅" meeting_agenda_item: "의제 항목" meeting_agenda: "의제" - meeting_minutes: "의사록" meeting_section: "섹션" activity: filter: @@ -213,9 +211,6 @@ ko: new_date_time: "새로운 날짜/시간" label_mail_all_participants: "모든 참가자에게 이메일 보내기" types: - classic: "클래식(지원되지 않음)" - classic_text: "참고: 클래식 미팅은 다음 버전의 OpenProject에서 제거됩니다." - structured: "구조화됨" one_time: "일회성" recurring: "반복" recurring_text: "각 항목에 대해 다이내믹 템플릿을 사용하여 미팅 시리즈를 만듭니다." diff --git a/modules/meeting/config/locales/crowdin/lt.yml b/modules/meeting/config/locales/crowdin/lt.yml index 9dad0423637..d1d038394c1 100644 --- a/modules/meeting/config/locales/crowdin/lt.yml +++ b/modules/meeting/config/locales/crowdin/lt.yml @@ -72,11 +72,9 @@ lt: invalid_time_format: "netinkamas laikas. Reikalingas formatas: HH:MM (pvz.: 10:30)" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Susitikimas" + meeting: "One-time meeting" meeting_agenda_item: "Darbotvarkės punktas" meeting_agenda: "Dienotvarkė" - meeting_minutes: "Minutės" meeting_section: "Skiltis" activity: filter: @@ -225,9 +223,6 @@ lt: new_date_time: "Nauja data/laikas" label_mail_all_participants: "Siųsti e-laišką visiems dalyviams" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/lv.yml b/modules/meeting/config/locales/crowdin/lv.yml index fdd5182899b..cb2822bd5a7 100644 --- a/modules/meeting/config/locales/crowdin/lv.yml +++ b/modules/meeting/config/locales/crowdin/lv.yml @@ -71,11 +71,9 @@ lv: invalid_time_format: "nav derīgs laiks. Vajadzīgais formāts: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Sanāksmes" + meeting: "One-time meeting" meeting_agenda_item: "Darba kārtības punkts" meeting_agenda: "Darba kārtība" - meeting_minutes: "Protokols" meeting_section: "Section" activity: filter: @@ -221,9 +219,6 @@ lv: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/mn.yml b/modules/meeting/config/locales/crowdin/mn.yml index 220133df1e0..12119ef18e6 100644 --- a/modules/meeting/config/locales/crowdin/mn.yml +++ b/modules/meeting/config/locales/crowdin/mn.yml @@ -70,11 +70,9 @@ mn: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ mn: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ms.yml b/modules/meeting/config/locales/crowdin/ms.yml index b05e184d832..2fc75cee27c 100644 --- a/modules/meeting/config/locales/crowdin/ms.yml +++ b/modules/meeting/config/locales/crowdin/ms.yml @@ -69,11 +69,9 @@ ms: invalid_time_format: "bukan masa yang sah. Format yang diperlukan: JJ:MM" models: recurring_meeting: "Mesyuarat berulang" - structured_meeting: "Mesyuarat sekali" - meeting: "Mesyuarat" + meeting: "One-time meeting" meeting_agenda_item: "Item agenda" meeting_agenda: "Agenda" - meeting_minutes: "Minit mesyuarat" meeting_section: "Bahagian" activity: filter: @@ -213,9 +211,6 @@ ms: new_date_time: "Tarikh/masa baharu" label_mail_all_participants: "Hantar e-mel kepada semua peserta" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Berstruktur" one_time: "Sekali" recurring: "Berulang" recurring_text: "Buat siri mesyuarat dengan templat dinamik untuk setiap kejadian." diff --git a/modules/meeting/config/locales/crowdin/ne.yml b/modules/meeting/config/locales/crowdin/ne.yml index 52b5257862b..3e3173f25f2 100644 --- a/modules/meeting/config/locales/crowdin/ne.yml +++ b/modules/meeting/config/locales/crowdin/ne.yml @@ -70,11 +70,9 @@ ne: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ ne: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/nl.yml b/modules/meeting/config/locales/crowdin/nl.yml index 08e1f643738..1645de5cbb1 100644 --- a/modules/meeting/config/locales/crowdin/nl.yml +++ b/modules/meeting/config/locales/crowdin/nl.yml @@ -70,11 +70,9 @@ nl: invalid_time_format: "is geen geldige tijd. Vereist formaat: UH:MM" models: recurring_meeting: "Terugkerende vergadering" - structured_meeting: "Eenmalige vergadering" - meeting: "Vergadering" + meeting: "One-time meeting" meeting_agenda_item: "Agendapunt" meeting_agenda: "Agenda" - meeting_minutes: "Minuten" meeting_section: "Sectie" activity: filter: @@ -217,9 +215,6 @@ nl: new_date_time: "Nieuwe datum/tijd" label_mail_all_participants: "E-mail verzenden naar alle deelnemers" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Gestructureerd" one_time: "Eenmalig" recurring: "Terugkerend" recurring_text: "Maak vergaderreeksen met dynamische sjablonen voor elke gebeurtenis." diff --git a/modules/meeting/config/locales/crowdin/no.yml b/modules/meeting/config/locales/crowdin/no.yml index f8925a714ac..5ca9ee34ab5 100644 --- a/modules/meeting/config/locales/crowdin/no.yml +++ b/modules/meeting/config/locales/crowdin/no.yml @@ -70,11 +70,9 @@ invalid_time_format: "er ikke et gyldig tidpunkt. Formatet må være HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Møte" + meeting: "One-time meeting" meeting_agenda_item: "Dagsorden element" meeting_agenda: "Saksliste" - meeting_minutes: "Referat" meeting_section: "Seksjon" activity: filter: @@ -217,9 +215,6 @@ new_date_time: "Ny dato/tid" label_mail_all_participants: "Send e-post til alle deltagere" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/pl.yml b/modules/meeting/config/locales/crowdin/pl.yml index 3eecdac15bf..00613bb73c0 100644 --- a/modules/meeting/config/locales/crowdin/pl.yml +++ b/modules/meeting/config/locales/crowdin/pl.yml @@ -72,11 +72,9 @@ pl: invalid_time_format: "nie jest prawidłowy. Wymagany format to: GG:MM" models: recurring_meeting: "Spotkanie cykliczne" - structured_meeting: "Spotkanie jednorazowe" - meeting: "Spotkanie" + meeting: "Spotkanie jednorazowe" meeting_agenda_item: "Punkt programu" meeting_agenda: "Plan spotkania" - meeting_minutes: "Protokół ze spotkania" meeting_section: "Sekcja" activity: filter: @@ -225,9 +223,6 @@ pl: new_date_time: "Nowa data/godzina" label_mail_all_participants: "Wyślij wiadomość e-mail do wszystkich uczestników" types: - classic: "Klasyczne (nieobsługiwane)" - classic_text: "Uwaga: spotkania klasyczne zostaną usunięte w następnej wersji OpenProject." - structured: "Zorganizowane" one_time: "Jednorazowe" recurring: "Cykliczne" recurring_text: "Utwórz serię spotkań z dynamicznym szablonem dla każdego wystąpienia." diff --git a/modules/meeting/config/locales/crowdin/pt-BR.yml b/modules/meeting/config/locales/crowdin/pt-BR.yml index b00b25ca710..6190920d395 100644 --- a/modules/meeting/config/locales/crowdin/pt-BR.yml +++ b/modules/meeting/config/locales/crowdin/pt-BR.yml @@ -70,11 +70,9 @@ pt-BR: invalid_time_format: "não é um horário válido. Formato exigido: HH:MM" models: recurring_meeting: "Reunião recorrente" - structured_meeting: "Reunião única" - meeting: "Reunião" + meeting: "Reunião única" meeting_agenda_item: "Item da agenda" meeting_agenda: "Agenda" - meeting_minutes: "Atas" meeting_section: "Seção" activity: filter: @@ -217,9 +215,6 @@ pt-BR: new_date_time: "Nova data/hora" label_mail_all_participants: "Enviar e-mail para todos os participantes" types: - classic: "Clássica (não suportada)" - classic_text: "Nota: As reuniões clássicas serão removidas na próxima versão do OpenProject." - structured: "Estruturada" one_time: "Única" recurring: "Recorrente" recurring_text: "Criar série de reuniões com modelo dinâmico para cada evento." diff --git a/modules/meeting/config/locales/crowdin/pt-PT.yml b/modules/meeting/config/locales/crowdin/pt-PT.yml index 0285df0f715..c57e66bb0db 100644 --- a/modules/meeting/config/locales/crowdin/pt-PT.yml +++ b/modules/meeting/config/locales/crowdin/pt-PT.yml @@ -70,11 +70,9 @@ pt-PT: invalid_time_format: "não é uma hora válida. Formato exigido: HH:MM" models: recurring_meeting: "Reunião recorrente" - structured_meeting: "Reunião única" - meeting: "Reunião" + meeting: "Reunião única" meeting_agenda_item: "Pontos da ordem de trabalhos" meeting_agenda: "Agenda" - meeting_minutes: "Minutos" meeting_section: "Secção" activity: filter: @@ -217,9 +215,6 @@ pt-PT: new_date_time: "Nova data/hora" label_mail_all_participants: "Enviar e-mail a todos os participantes" types: - classic: "Clássica (não suportada)" - classic_text: "Nota: as reuniões clássicas serão removidas na próxima versão do OpenProject." - structured: "Estruturada" one_time: "Única" recurring: "Recorrente" recurring_text: "Crie séries de reuniões com um modelo dinâmico para cada ocorrência." diff --git a/modules/meeting/config/locales/crowdin/ro.yml b/modules/meeting/config/locales/crowdin/ro.yml index 7a95aab0bc3..aed764e2f86 100644 --- a/modules/meeting/config/locales/crowdin/ro.yml +++ b/modules/meeting/config/locales/crowdin/ro.yml @@ -71,11 +71,9 @@ ro: invalid_time_format: "nu este o oră valabilă. Format necesar: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "ID întâlnire" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agendă" - meeting_minutes: "Minute" meeting_section: "Secțiune" activity: filter: @@ -221,9 +219,6 @@ ro: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/ru.yml b/modules/meeting/config/locales/crowdin/ru.yml index 912433e5317..f44e03db954 100644 --- a/modules/meeting/config/locales/crowdin/ru.yml +++ b/modules/meeting/config/locales/crowdin/ru.yml @@ -72,11 +72,9 @@ ru: invalid_time_format: "недопустимое время. Требуемый формат: ЧЧ:ММ" models: recurring_meeting: "Повторяющееся совещание" - structured_meeting: "Однократное совещание" - meeting: "Совещание" + meeting: "Однократное совещание" meeting_agenda_item: "Пункт повестки" meeting_agenda: "Повестка дня" - meeting_minutes: "Протокол(-ы)" meeting_section: "Раздел" activity: filter: @@ -225,9 +223,6 @@ ru: new_date_time: "Новые дата/время" label_mail_all_participants: "Отправить письмо всем участникам" types: - classic: "Классический (не поддерживается)" - classic_text: "Примечание: совещания классического типа будут удалены в следующей версии OpenProject." - structured: "Структурированная" one_time: "Однократная" recurring: "Периодичность" recurring_text: "Создать серию совещаний с динамическим шаблоном для каждого события." diff --git a/modules/meeting/config/locales/crowdin/rw.yml b/modules/meeting/config/locales/crowdin/rw.yml index 2b5e431677e..55e3d0d1ee7 100644 --- a/modules/meeting/config/locales/crowdin/rw.yml +++ b/modules/meeting/config/locales/crowdin/rw.yml @@ -70,11 +70,9 @@ rw: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ rw: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/si.yml b/modules/meeting/config/locales/crowdin/si.yml index 53fc9f4dee1..6df003a0109 100644 --- a/modules/meeting/config/locales/crowdin/si.yml +++ b/modules/meeting/config/locales/crowdin/si.yml @@ -70,11 +70,9 @@ si: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ si: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/sk.yml b/modules/meeting/config/locales/crowdin/sk.yml index 66f97c01a54..ad369159d1b 100644 --- a/modules/meeting/config/locales/crowdin/sk.yml +++ b/modules/meeting/config/locales/crowdin/sk.yml @@ -72,11 +72,9 @@ sk: invalid_time_format: "nie je platný čas. Požadovaný formát: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Stretnutie" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Zápisnica" meeting_section: "Section" activity: filter: @@ -225,9 +223,6 @@ sk: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/sl.yml b/modules/meeting/config/locales/crowdin/sl.yml index 38824946f69..70d68214ad5 100644 --- a/modules/meeting/config/locales/crowdin/sl.yml +++ b/modules/meeting/config/locales/crowdin/sl.yml @@ -72,11 +72,9 @@ sl: invalid_time_format: "ni veljaven čas. Potreben format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Sestanek" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Dnevni red" - meeting_minutes: "Zapisnik" meeting_section: "Section" activity: filter: @@ -225,9 +223,6 @@ sl: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/sr.yml b/modules/meeting/config/locales/crowdin/sr.yml index 6b3ab5e994d..46ceaa82824 100644 --- a/modules/meeting/config/locales/crowdin/sr.yml +++ b/modules/meeting/config/locales/crowdin/sr.yml @@ -71,11 +71,9 @@ sr: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -221,9 +219,6 @@ sr: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/sv.yml b/modules/meeting/config/locales/crowdin/sv.yml index 419af360a8a..210b70a0bba 100644 --- a/modules/meeting/config/locales/crowdin/sv.yml +++ b/modules/meeting/config/locales/crowdin/sv.yml @@ -70,11 +70,9 @@ sv: invalid_time_format: "är inte en giltig tid. Använd formatet HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Möte" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Protokoll" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ sv: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/th.yml b/modules/meeting/config/locales/crowdin/th.yml index 61526065a19..5e7c14493b8 100644 --- a/modules/meeting/config/locales/crowdin/th.yml +++ b/modules/meeting/config/locales/crowdin/th.yml @@ -69,11 +69,9 @@ th: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "ประชุม" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -213,9 +211,6 @@ th: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/tr.yml b/modules/meeting/config/locales/crowdin/tr.yml index b778eae9028..3c245824539 100644 --- a/modules/meeting/config/locales/crowdin/tr.yml +++ b/modules/meeting/config/locales/crowdin/tr.yml @@ -70,11 +70,9 @@ tr: invalid_time_format: "geçerli bir zaman değil. Gerekli format: SS:DD" models: recurring_meeting: "Yinelenen toplantı" - structured_meeting: "Tek seferlik toplantı" - meeting: "Toplantı" + meeting: "Tek seferlik toplantı" meeting_agenda_item: "Gündem maddesi" meeting_agenda: "Gündem" - meeting_minutes: "Dakika" meeting_section: "Bölüm" activity: filter: @@ -217,9 +215,6 @@ tr: new_date_time: "Yeni tarih/saat" label_mail_all_participants: "Tüm katılımcılara e-posta gönder" types: - classic: "Klasik (desteklenmiyor)" - classic_text: "Not: Klasik toplantılar sonraki OpenProject sürümünde kaldırılacaktır." - structured: "Planlı" one_time: "Tek seferlik" recurring: "Yinelenen" recurring_text: "Her toplantı için kullanılabilecek dinamik bir şablonla toplantı serileri oluşturun." diff --git a/modules/meeting/config/locales/crowdin/uk.yml b/modules/meeting/config/locales/crowdin/uk.yml index b25560464f2..6516cc103ae 100644 --- a/modules/meeting/config/locales/crowdin/uk.yml +++ b/modules/meeting/config/locales/crowdin/uk.yml @@ -72,11 +72,9 @@ uk: invalid_time_format: "не є допустимим часом. Необхідний формат: HH:MM" models: recurring_meeting: "Повторювана нарада" - structured_meeting: "Одноразова нарада" - meeting: "Нарада" + meeting: "Одноразова нарада" meeting_agenda_item: "Порядок денний" meeting_agenda: "Порядок денний" - meeting_minutes: "Хвилини" meeting_section: "Розділ" activity: filter: @@ -225,9 +223,6 @@ uk: new_date_time: "Нова дата/час" label_mail_all_participants: "Надіслати лист усім учасникам" types: - classic: "Класична (не підтримується)" - classic_text: "Примітка: класичні наради буде вилучено в наступній версії OpenProject." - structured: "Структурована" one_time: "Одноразова" recurring: "Повторювана" recurring_text: "Створюйте серії нарад на основі динамічного шаблона для кожної події." diff --git a/modules/meeting/config/locales/crowdin/uz.yml b/modules/meeting/config/locales/crowdin/uz.yml index fed63d2302a..323453407e1 100644 --- a/modules/meeting/config/locales/crowdin/uz.yml +++ b/modules/meeting/config/locales/crowdin/uz.yml @@ -70,11 +70,9 @@ uz: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: filter: @@ -217,9 +215,6 @@ uz: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/config/locales/crowdin/vi.yml b/modules/meeting/config/locales/crowdin/vi.yml index 6f73d9214eb..2434208bc8d 100644 --- a/modules/meeting/config/locales/crowdin/vi.yml +++ b/modules/meeting/config/locales/crowdin/vi.yml @@ -69,11 +69,9 @@ vi: invalid_time_format: "không phải giờ hợp lệ. Yêu cầu định dạng: HH:MM" models: recurring_meeting: "Họp định kỳ" - structured_meeting: "Họp đột xuất" - meeting: "Cuộc họp" + meeting: "One-time meeting" meeting_agenda_item: "Mục chương trình nghị sự" meeting_agenda: "Các ý chính" - meeting_minutes: "Phút" meeting_section: "Phần" activity: filter: @@ -213,9 +211,6 @@ vi: new_date_time: "Ngày/giờ mới" label_mail_all_participants: "Gửi email đến tất cả người tham gia" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Đã cấu trúc" one_time: "Một lần" recurring: "Định kỳ" recurring_text: "Tạo chuỗi họp với mẫu động cho mỗi lần họp." diff --git a/modules/meeting/config/locales/crowdin/zh-CN.yml b/modules/meeting/config/locales/crowdin/zh-CN.yml index 43d626c87f2..9d0376db212 100644 --- a/modules/meeting/config/locales/crowdin/zh-CN.yml +++ b/modules/meeting/config/locales/crowdin/zh-CN.yml @@ -69,11 +69,9 @@ zh-CN: invalid_time_format: "不是有效时间。所需格式:HH:MM" models: recurring_meeting: "定期会议" - structured_meeting: "动态会议" - meeting: "会议" + meeting: "一次性会议" meeting_agenda_item: "议程项目" meeting_agenda: "议程" - meeting_minutes: "会议记录" meeting_section: "节" activity: filter: @@ -213,9 +211,6 @@ zh-CN: new_date_time: "新日期/时间" label_mail_all_participants: "发送电子邮件给所有参与者" types: - classic: "经典(不支持)" - classic_text: "注意:经典会议将在下一版 OpenProject 中删除。" - structured: "结构化" one_time: "动态" recurring: "定期" recurring_text: "为每次事件创建带有动态模板的会议系列。" diff --git a/modules/meeting/config/locales/crowdin/zh-TW.yml b/modules/meeting/config/locales/crowdin/zh-TW.yml index ac3a23dac4f..a4156e22383 100644 --- a/modules/meeting/config/locales/crowdin/zh-TW.yml +++ b/modules/meeting/config/locales/crowdin/zh-TW.yml @@ -69,11 +69,9 @@ zh-TW: invalid_time_format: "不是有效時間。所需格式: HH:MM" models: recurring_meeting: "重複性會議" - structured_meeting: "一次性會議" - meeting: "會議" + meeting: "一次性會議" meeting_agenda_item: "議程項目" meeting_agenda: "會議大綱" - meeting_minutes: "會議記錄" meeting_section: "區段" activity: filter: @@ -213,9 +211,6 @@ zh-TW: new_date_time: "新日期/時間" label_mail_all_participants: "發送電子郵件給所有參與者" types: - classic: "傳統(不支援)" - classic_text: "注意:傳統會議將在 OpenProject 的下一個版本中刪除。" - structured: "結構化" one_time: "一次性" recurring: "重覆" recurring_text: "為每次發生的事件建立具有動態範本的會議系列。" @@ -329,7 +324,7 @@ zh-TW: permission_create_meeting_agendas_explanation: "允許編輯傳統會議的議程。" permission_manage_agendas: "管理議程" permission_manage_agendas_explanation: "允許編輯動態會議的議程項目。" - permission_close_meeting_agendas: "結束會議大綱" + permission_close_meeting_agendas: "定案會議大綱" permission_send_meeting_agendas_notification: "傳送會議大綱審閱通知" permission_create_meeting_minutes: "管理會議記錄" permission_send_meeting_minutes_notification: "傳送會議記錄審閱通知" diff --git a/modules/meeting/config/locales/en.yml b/modules/meeting/config/locales/en.yml index 1ba523ae371..0bc0310920e 100644 --- a/modules/meeting/config/locales/en.yml +++ b/modules/meeting/config/locales/en.yml @@ -82,11 +82,9 @@ en: invalid_time_format: "is not a valid time. Required format: HH:MM" models: recurring_meeting: "Recurring meeting" - structured_meeting: "One-time meeting" - meeting: "Meeting" + meeting: "One-time meeting" meeting_agenda_item: "Agenda item" meeting_agenda: "Agenda" - meeting_minutes: "Minutes" meeting_section: "Section" activity: @@ -236,9 +234,6 @@ en: new_date_time: "New date/time" label_mail_all_participants: "Send email to all participants" types: - classic: "Classic (unsupported)" - classic_text: "Note: Classic meetings will be removed in the next version of OpenProject." - structured: "Structured" one_time: "One-time" recurring: "Recurring" recurring_text: "Create meeting series with dynamic template for each occurrence." diff --git a/modules/meeting/db/migrate/20240405131352_create_meeting_sections.rb b/modules/meeting/db/migrate/20240405131352_create_meeting_sections.rb index 0bf6a12d535..60e5145d6ca 100644 --- a/modules/meeting/db/migrate/20240405131352_create_meeting_sections.rb +++ b/modules/meeting/db/migrate/20240405131352_create_meeting_sections.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class CreateMeetingSections < ActiveRecord::Migration[7.1] def up create_table :meeting_sections do |t| @@ -24,7 +25,7 @@ class CreateMeetingSections < ActiveRecord::Migration[7.1] private def create_and_assign_default_section - StructuredMeeting.includes(:agenda_items).find_each do |meeting| + Meeting.includes(:agenda_items).find_each do |meeting| section = MeetingSection.create!( meeting:, title: "Untitled" diff --git a/modules/meeting/db/migrate/20250404060850_add_author_to_outcome.rb b/modules/meeting/db/migrate/20250404060850_add_author_to_outcome.rb new file mode 100644 index 00000000000..60900dcf601 --- /dev/null +++ b/modules/meeting/db/migrate/20250404060850_add_author_to_outcome.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AddAuthorToOutcome < ActiveRecord::Migration[8.0] + def change + add_reference :meeting_outcomes, + :author, + type: :bigint, + foreign_key: { to_table: :users }, + null: true, + index: true + end +end diff --git a/modules/meeting/db/migrate/20250404061503_migrate_classic_meetings.rb b/modules/meeting/db/migrate/20250404061503_migrate_classic_meetings.rb new file mode 100644 index 00000000000..e2e8dc18919 --- /dev/null +++ b/modules/meeting/db/migrate/20250404061503_migrate_classic_meetings.rb @@ -0,0 +1,82 @@ +# rubocop:disable Rails/SquishedSQLHeredocs +# frozen_string_literal: true + +class MigrateClassicMeetings < ActiveRecord::Migration[8.0] + def up + # Create default sections for meetings that don't have one + execute <<~SQL + INSERT INTO meeting_sections (meeting_id, title, position, created_at, updated_at) + SELECT m.id, '', 1, NOW(), NOW() + FROM meetings m + LEFT JOIN meeting_sections ms ON ms.meeting_id = m.id + WHERE ms.id IS NULL; + SQL + + # Migrate MeetingAgenda content to MeetingAgendaItem + execute <<~SQL.squish + INSERT INTO meeting_agenda_items ( + meeting_id, + meeting_section_id, + author_id, + presenter_id, + title, + notes, + position, + created_at, + updated_at + ) + SELECT + mc.meeting_id, + ms.id, + mc.author_id, + mc.author_id, + '#{I18n.t('activerecord.models.meeting_agenda')}', + mc.text, + 1, + mc.created_at, + mc.updated_at + FROM meeting_contents mc + INNER JOIN meetings m ON m.id = mc.meeting_id + INNER JOIN meeting_sections ms ON ms.meeting_id = m.id + WHERE mc.type = 'MeetingAgenda'; + SQL + + # Migrate MeetingMinutes to MeetingOutcome + execute <<~SQL.squish + INSERT INTO meeting_outcomes ( + meeting_agenda_item_id, + author_id, + notes, + created_at, + updated_at + ) + SELECT + mai.id, + mc.author_id, + mc.text, + mc.created_at, + mc.updated_at + FROM meeting_contents mc + INNER JOIN meetings m ON m.id = mc.meeting_id + INNER JOIN meeting_sections ms ON ms.meeting_id = m.id + INNER JOIN meeting_agenda_items mai ON mai.meeting_id = m.id + WHERE mc.type = 'MeetingMinutes'; + SQL + + # Close classic meetings that are in the past + execute <<~SQL.squish + UPDATE meetings + SET state = 5 + WHERE type = 'Meeting' + AND start_time < CURRENT_TIMESTAMP + SQL + + # Remove STI column + remove_column :meetings, :type + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end +# rubocop:enable Rails/SquishedSQLHeredocs diff --git a/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb index 4813568e815..66dc5bdbbb6 100644 --- a/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb +++ b/modules/meeting/lib/api/v3/attachments/attachments_by_meeting_content_api.rb @@ -32,23 +32,18 @@ module API module Attachments class AttachmentsByMeetingContentAPI < ::API::OpenProjectAPI resources :attachments do - helpers API::V3::Attachments::AttachmentsByContainerAPI::Helpers - - helpers do - def container - meeting_content - end - - def get_attachment_self_path - api_v3_paths.attachments_by_meeting_content container.id - end + get do + status :gone end - get &API::V3::Attachments::AttachmentsByContainerAPI.read - post &API::V3::Attachments::AttachmentsByContainerAPI.create + post do + status :gone + end namespace :prepare do - post &API::V3::Attachments::AttachmentsByContainerAPI.prepare + post do + status :gone + end end end end diff --git a/modules/meeting/lib/api/v3/meeting_agendas/meeting_agenda_representer.rb b/modules/meeting/lib/api/v3/meeting_agendas/meeting_agenda_representer.rb deleted file mode 100644 index f0d6571db92..00000000000 --- a/modules/meeting/lib/api/v3/meeting_agendas/meeting_agenda_representer.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module API - module V3 - module MeetingAgendas - class MeetingAgendaRepresenter < API::V3::MeetingContents::MeetingContentRepresenter - end - end - end -end diff --git a/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb b/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb index e46c74b1180..1d74caadca2 100644 --- a/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb +++ b/modules/meeting/lib/api/v3/meetings/meeting_contents_api.rb @@ -32,20 +32,10 @@ module API module Meetings class MeetingContentsAPI < ::API::OpenProjectAPI resources :meeting_contents do - helpers do - def meeting_content - MeetingContent.find params[:id] - end - end - route_param :id do get do - ::API::V3::MeetingContents::MeetingContentRepresenter.new( - meeting_content, current_user:, embed_links: true - ) + status :gone end - - mount ::API::V3::Attachments::AttachmentsByMeetingContentAPI end end end diff --git a/modules/meeting/lib/api/v3/meetings/meeting_representer.rb b/modules/meeting/lib/api/v3/meetings/meeting_representer.rb index e7528766f4b..21426d113e9 100644 --- a/modules/meeting/lib/api/v3/meetings/meeting_representer.rb +++ b/modules/meeting/lib/api/v3/meetings/meeting_representer.rb @@ -48,10 +48,6 @@ module API lock_version.to_i } - property :type, - as: :meeting_type, - getter: ->(*) { type } - date_time_property :start_time date_time_property :end_time diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index 25906f7bf53..a522fb332c7 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -189,6 +189,13 @@ module OpenProject::Meeting patches [:Project] patch_with_namespace :BasicData, :SettingSeeder + replace_principal_references "Meeting" => %i[author_id], + "MeetingAgenda" => %i[author_id], + "MeetingMinutes" => %i[author_id], + "MeetingAgendaItem" => %i[author_id presenter_id], + "MeetingParticipant" => :user_id, + "MeetingOutcome" => :author_id + extend_api_response(:v3, :work_packages, :work_package, &::OpenProject::Meeting::Patches::API::WorkPackageRepresenter.extension) diff --git a/modules/meeting/spec/components/meetings/delete_dialog_component_spec.rb b/modules/meeting/spec/components/meetings/delete_dialog_component_spec.rb index dd74e691069..31b721aea03 100644 --- a/modules/meeting/spec/components/meetings/delete_dialog_component_spec.rb +++ b/modules/meeting/spec/components/meetings/delete_dialog_component_spec.rb @@ -69,7 +69,7 @@ RSpec.describe Meetings::DeleteDialogComponent, type: :component do context "with an associated recurring/templated meeting" do let(:series) { build_stubbed(:recurring_meeting) } - let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting: series) } + let(:meeting) { build_stubbed(:meeting_template, recurring_meeting: series) } it "shows a heading" do expect(subject).to have_text "Cancel this meeting occurrence?" diff --git a/modules/meeting/spec/components/meetings/row_component_spec.rb b/modules/meeting/spec/components/meetings/row_component_spec.rb index 5fb0fd9088c..fc39200c080 100644 --- a/modules/meeting/spec/components/meetings/row_component_spec.rb +++ b/modules/meeting/spec/components/meetings/row_component_spec.rb @@ -61,7 +61,7 @@ RSpec.describe Meetings::RowComponent, type: :component do context "with an associated recurring/templated meeting" do let(:series) { build_stubbed(:recurring_meeting, project:) } - let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting: series, project:) } + let(:meeting) { build_stubbed(:meeting_template, recurring_meeting: series, project:) } it "shows default menu items" do expect(subject).to have_link "View meeting series" @@ -99,7 +99,7 @@ RSpec.describe Meetings::RowComponent, type: :component do context "with an associated recurring/templated meeting" do let(:series) { build_stubbed(:recurring_meeting, project:) } - let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting: series, project:) } + let(:meeting) { build_stubbed(:meeting_template, recurring_meeting: series, project:) } context "without a current project" do it "shows delete menu item with a back url" do diff --git a/modules/meeting/spec/components/meetings/side_panel/details_component_spec.rb b/modules/meeting/spec/components/meetings/side_panel/details_component_spec.rb index 709db948fd1..72fef088563 100644 --- a/modules/meeting/spec/components/meetings/side_panel/details_component_spec.rb +++ b/modules/meeting/spec/components/meetings/side_panel/details_component_spec.rb @@ -49,7 +49,7 @@ RSpec.describe Meetings::SidePanel::DetailsComponent, type: :component do frequency: "working_days") end let(:meeting) do - build_stubbed(:structured_meeting_template, + build_stubbed(:meeting_template, recurring_meeting: series) end @@ -65,7 +65,7 @@ RSpec.describe Meetings::SidePanel::DetailsComponent, type: :component do frequency: "weekly") end let(:meeting) do - build_stubbed(:structured_meeting_template, + build_stubbed(:meeting_template, recurring_meeting: series) end diff --git a/modules/meeting/spec/components/recurring_meetings/delete_dialog_component_spec.rb b/modules/meeting/spec/components/recurring_meetings/delete_dialog_component_spec.rb index 20614f6c262..a0d2ffb046b 100644 --- a/modules/meeting/spec/components/recurring_meetings/delete_dialog_component_spec.rb +++ b/modules/meeting/spec/components/recurring_meetings/delete_dialog_component_spec.rb @@ -35,7 +35,7 @@ RSpec.describe RecurringMeetings::DeleteDialogComponent, type: :component do let(:project) { build_stubbed(:project) } let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:, end_after: :iterations, iterations: 6) } - let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting:) } + let(:meeting) { build_stubbed(:meeting_template, recurring_meeting:) } let(:user) { build_stubbed(:user) } subject do diff --git a/modules/meeting/spec/components/recurring_meetings/delete_scheduled_dialog_component_spec.rb b/modules/meeting/spec/components/recurring_meetings/delete_scheduled_dialog_component_spec.rb index 1f47a657909..e595609bc07 100644 --- a/modules/meeting/spec/components/recurring_meetings/delete_scheduled_dialog_component_spec.rb +++ b/modules/meeting/spec/components/recurring_meetings/delete_scheduled_dialog_component_spec.rb @@ -37,7 +37,7 @@ RSpec.describe RecurringMeetings::DeleteScheduledDialogComponent, type: :compone let(:recurring_meeting) { build_stubbed(:recurring_meeting, project:, end_after: :iterations, iterations: 6) } let(:start_time) { 1.day.from_now } let(:scheduled_meeting) { recurring_meeting.scheduled_meetings.find_or_initialize_by(start_time: start_time) } - let(:meeting) { build_stubbed(:structured_meeting_template, recurring_meeting:) } + let(:meeting) { build_stubbed(:meeting_template, recurring_meeting:) } let(:user) { build_stubbed(:user) } subject do diff --git a/modules/meeting/spec/contracts/meeting_agenda_items/create_contract_spec.rb b/modules/meeting/spec/contracts/meeting_agenda_items/create_contract_spec.rb index e09213d85bc..cb5006e0feb 100644 --- a/modules/meeting/spec/contracts/meeting_agenda_items/create_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_agenda_items/create_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingAgendaItems::CreateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - let(:meeting) { create(:structured_meeting, project:) } + let(:meeting) { create(:meeting, project:) } let(:item) { build(:meeting_agenda_item, meeting:) } let(:contract) { described_class.new(item, user) } diff --git a/modules/meeting/spec/contracts/meeting_agenda_items/delete_contract_spec.rb b/modules/meeting/spec/contracts/meeting_agenda_items/delete_contract_spec.rb index 5afda8bd64e..ebe061d8aa5 100644 --- a/modules/meeting/spec/contracts/meeting_agenda_items/delete_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_agenda_items/delete_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingAgendaItems::DeleteContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - shared_let(:meeting) { create(:structured_meeting, project:) } + shared_let(:meeting) { create(:meeting, project:) } shared_let(:item) { create(:meeting_agenda_item, meeting:) } let(:contract) { described_class.new(item, user) } diff --git a/modules/meeting/spec/contracts/meeting_agenda_items/update_contract_spec.rb b/modules/meeting/spec/contracts/meeting_agenda_items/update_contract_spec.rb index fec471e6c51..43a85865509 100644 --- a/modules/meeting/spec/contracts/meeting_agenda_items/update_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_agenda_items/update_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingAgendaItems::UpdateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - shared_let(:meeting) { create(:structured_meeting, project:) } + shared_let(:meeting) { create(:meeting, project:) } shared_let(:item) { create(:meeting_agenda_item, meeting:) } let(:contract) { described_class.new(item, user) } diff --git a/modules/meeting/spec/contracts/meeting_outcomes/create_contract_spec.rb b/modules/meeting/spec/contracts/meeting_outcomes/create_contract_spec.rb index c85321449e9..e07e30c5243 100644 --- a/modules/meeting/spec/contracts/meeting_outcomes/create_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_outcomes/create_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingOutcomes::CreateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - let(:meeting) { create(:structured_meeting, project:) } + let(:meeting) { create(:meeting, project:) } let(:meeting_agenda_item) { create(:meeting_agenda_item, meeting:) } let(:outcome) { build(:meeting_outcome, meeting_agenda_item:) } let(:contract) { described_class.new(outcome, user) } diff --git a/modules/meeting/spec/contracts/meeting_outcomes/delete_contract_spec.rb b/modules/meeting/spec/contracts/meeting_outcomes/delete_contract_spec.rb index 999ce1800de..bcc473f8c3b 100644 --- a/modules/meeting/spec/contracts/meeting_outcomes/delete_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_outcomes/delete_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingOutcomes::DeleteContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - shared_let(:meeting) { create(:structured_meeting, project:) } + shared_let(:meeting) { create(:meeting, project:) } let(:meeting_agenda_item) { create(:meeting_agenda_item, meeting:) } let(:outcome) { build_stubbed(:meeting_outcome, meeting_agenda_item:) } let(:contract) { described_class.new(outcome, user) } diff --git a/modules/meeting/spec/contracts/meeting_outcomes/update_contract_spec.rb b/modules/meeting/spec/contracts/meeting_outcomes/update_contract_spec.rb index 0b6fd928cc8..a0c61195a8e 100644 --- a/modules/meeting/spec/contracts/meeting_outcomes/update_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_outcomes/update_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingOutcomes::UpdateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - let(:meeting) { create(:structured_meeting, project:) } + let(:meeting) { create(:meeting, project:) } let(:meeting_agenda_item) { create(:meeting_agenda_item, meeting:) } let(:outcome) { build(:meeting_outcome, meeting_agenda_item:) } let(:contract) { described_class.new(outcome, user) } diff --git a/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb b/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb index a702e623cff..e088c011516 100644 --- a/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_sections/create_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingSections::CreateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - let(:meeting) { create(:structured_meeting, project:) } + let(:meeting) { create(:meeting, project:) } let(:section) { build(:meeting_section, meeting:) } let(:contract) { described_class.new(section, user) } diff --git a/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb b/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb index c5c836c6de0..dfdcbcefa74 100644 --- a/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_sections/delete_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingSections::DeleteContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - shared_let(:meeting) { create(:structured_meeting, project:) } + shared_let(:meeting) { create(:meeting, project:) } let(:section) { create(:meeting_section, meeting:) } let(:contract) { described_class.new(section, user) } diff --git a/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb b/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb index 5bbac6a403c..84a3653fbea 100644 --- a/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb +++ b/modules/meeting/spec/contracts/meeting_sections/update_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe MeetingSections::UpdateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - shared_let(:meeting) { create(:structured_meeting, project:) } + shared_let(:meeting) { create(:meeting, project:) } shared_let(:section) { create(:meeting_section, meeting:) } let(:contract) { described_class.new(section, user) } diff --git a/modules/meeting/spec/contracts/meetings/create_contract_spec.rb b/modules/meeting/spec/contracts/meetings/create_contract_spec.rb index 6360f5a767b..be7a26635ae 100644 --- a/modules/meeting/spec/contracts/meetings/create_contract_spec.rb +++ b/modules/meeting/spec/contracts/meetings/create_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Meetings::CreateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - let(:meeting) { build(:structured_meeting, project:) } + let(:meeting) { build(:meeting, project:) } let(:contract) { described_class.new(meeting, user) } context "with permission" do diff --git a/modules/meeting/spec/contracts/meetings/update_contract_spec.rb b/modules/meeting/spec/contracts/meetings/update_contract_spec.rb index 850cf721181..29d95655bfd 100644 --- a/modules/meeting/spec/contracts/meetings/update_contract_spec.rb +++ b/modules/meeting/spec/contracts/meetings/update_contract_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Meetings::UpdateContract do include_context "ModelContract shared context" shared_let(:project) { create(:project) } - shared_let(:meeting) { create(:structured_meeting, project:) } + shared_let(:meeting) { create(:meeting, project:) } let(:contract) { described_class.new(meeting, user) } context "with permission" do diff --git a/modules/meeting/spec/controllers/meetings_controller_spec.rb b/modules/meeting/spec/controllers/meetings_controller_spec.rb deleted file mode 100644 index dd3c1878ae7..00000000000 --- a/modules/meeting/spec/controllers/meetings_controller_spec.rb +++ /dev/null @@ -1,221 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "#{File.dirname(__FILE__)}/../spec_helper" - -RSpec.describe MeetingsController do - shared_let(:user) { create(:admin) } - shared_let(:project) { create(:project) } - shared_let(:other_project) { create(:project) } - - current_user { user } - - describe "GET" do - describe "index" do - let(:meetings) do - [ - create(:meeting, project:), - create(:meeting, author: user, project:), - create(:meeting, author: user, project: other_project) - ] - end - end - - describe "show" do - let(:meeting) { create(:meeting, project:, agenda: nil) } - - describe "html" do - before do - get "show", params: { id: meeting.id } - end - - it { expect(response).to redirect_to(project_meeting_path(project, meeting)) } - it { expect(assigns(:meeting)).to eql meeting } - end - end - - describe "new" do - let(:meeting) { Meeting.new(project:) } - - before do - allow(Project).to receive(:find).and_return(project) - allow(Meeting).to receive(:new).and_return(meeting) - end - - shared_examples_for "new action" do |response_type:| - describe response_type do - context "when requesting the page without a project id" do - before do - get "new" - end - - it { expect(response).to be_successful } - it { expect(assigns(:meeting)).to eql meeting } - it { expect(assigns(:project)).to be_nil } - end - - context "when requesting the page with a project id" do - before do - get "new", params: { project_id: project.id } - end - - it { expect(response).to be_successful } - it { expect(assigns(:meeting)).to eql meeting } - it { expect(assigns(:project)).to eql project } - end - end - end - - it_behaves_like "new action", response_type: "html" - it_behaves_like "new action", response_type: "turbo_stream" - end - - describe "edit" do - let(:meeting) { create(:meeting, project:) } - - describe "html" do - before do - get "edit", params: { project_id: meeting.project_id, id: meeting.id } - end - - it { expect(response).to be_successful } - it { expect(assigns(:meeting)).to eql meeting } - end - end - end - - describe "POST" do - describe "create" do - render_views - - let(:base_params) do - { - project_id: project&.id, - meeting: meeting_params - } - end - - let(:base_meeting_params) do - { - title: "Foobar", - duration: "1.0", - start_date: "2015-06-01", - start_time_hour: "10:00" - } - end - - let(:params) { base_params } - let(:meeting_params) { base_meeting_params } - - before do - post :create, - params: - end - - context "with a project_id" do - context "and an invalid start_date with start_time_hour" do - let(:meeting_params) do - base_meeting_params.merge(start_date: "-") - end - - it "renders an error" do - expect(response).to have_http_status :unprocessable_entity - expect(response).to render_template :new - expect(response.body) - .to have_text("Date #{I18n.t('activerecord.errors.messages.not_an_iso_date')}") - end - end - - context "and an invalid start_time_hour with start_date" do - let(:meeting_params) do - base_meeting_params.merge(start_time_hour: "-") - end - - it "renders an error" do - expect(response).to have_http_status :unprocessable_entity - expect(response).to render_template :new - expect(response.body) - .to have_text("Start time #{I18n.t('activerecord.errors.messages.invalid_time_format')}") - end - end - end - - context "with a nil project_id" do - let(:project) { nil } - - it "renders an error" do - expect(response).to have_http_status :unprocessable_entity - expect(response).to render_template :new - expect(response.body) - .to have_text("Project #{I18n.t('activerecord.errors.messages.blank')}") - end - end - - context "without a project_id" do - let(:params) { base_params.except(:project_id) } - let(:project) { nil } - - it "renders an error" do - expect(response).to have_http_status :unprocessable_entity - expect(response).to render_template :new - expect(response.body) - .to have_text("Project #{I18n.t('activerecord.errors.messages.blank')}") - end - end - end - end - - describe "notify" do - let!(:meeting) { create(:meeting) } - let!(:participant) { create(:meeting_participant, meeting:, attended: true) } - - it "produces a background job for notification" do - post :notify, params: { project_id: meeting.project_id, id: meeting.id } - - perform_enqueued_jobs - expect(ActionMailer::Base.deliveries.count).to eq(1) - end - - context "with an error during deliver" do - before do - allow(MeetingMailer).to receive(:invited).and_raise(Net::SMTPError) - end - - it "produces a flash message containing the mail addresses raising the error" do - expect { post :notify, params: { project_id: meeting.project_id, id: meeting.id } }.not_to raise_error - meeting.participants.each do |participant| - expect(flash[:error]).to include(participant.name) - end - - perform_enqueued_jobs - expect(ActionMailer::Base.deliveries.count).to eq(0) - end - end - end -end diff --git a/modules/meeting/spec/factories/meeting_agenda_factory.rb b/modules/meeting/spec/factories/meeting_agenda_factory.rb deleted file mode 100644 index 22490f8aa58..00000000000 --- a/modules/meeting/spec/factories/meeting_agenda_factory.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -FactoryBot.define do - factory :meeting_agenda do |_a| - meeting - end -end diff --git a/modules/meeting/spec/factories/meeting_agenda_item_factory.rb b/modules/meeting/spec/factories/meeting_agenda_item_factory.rb index de44aec2961..f475af5f455 100644 --- a/modules/meeting/spec/factories/meeting_agenda_item_factory.rb +++ b/modules/meeting/spec/factories/meeting_agenda_item_factory.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,7 +30,7 @@ FactoryBot.define do factory :meeting_agenda_item do |m| - meeting factory: :structured_meeting + meeting factory: :meeting work_package { nil } author factory: :user duration_in_minutes { 10 } diff --git a/modules/meeting/spec/factories/meeting_content_journal_factory.rb b/modules/meeting/spec/factories/meeting_content_journal_factory.rb deleted file mode 100644 index 93f50409fe1..00000000000 --- a/modules/meeting/spec/factories/meeting_content_journal_factory.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -FactoryBot.define do - factory :journal_meeting_content_journal, class: "Journal::MeetingContentJournal" do - end -end diff --git a/modules/meeting/spec/factories/meeting_factory.rb b/modules/meeting/spec/factories/meeting_factory.rb index d807b97958f..085f174cee1 100644 --- a/modules/meeting/spec/factories/meeting_factory.rb +++ b/modules/meeting/spec/factories/meeting_factory.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -47,12 +48,8 @@ FactoryBot.define do meeting.project = evaluator.project if evaluator.project end - factory :structured_meeting, class: "StructuredMeeting" do |structured_meeting| - structured_meeting.sequence(:title) { |n| "Structured meeting #{n}" } - end - - factory :structured_meeting_template, class: "StructuredMeeting" do |structured_meeting| - structured_meeting.sequence(:title) { |n| "Structured meeting template #{n}" } + factory :meeting_template do |meeting| + meeting.sequence(:title) { |n| "Meeting template #{n}" } template { true } recurring_meeting diff --git a/modules/meeting/spec/factories/meeting_minutes_factory.rb b/modules/meeting/spec/factories/meeting_minutes_factory.rb deleted file mode 100644 index c9682b447bc..00000000000 --- a/modules/meeting/spec/factories/meeting_minutes_factory.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -FactoryBot.define do - factory :meeting_minutes do |_m| - meeting - end -end diff --git a/modules/meeting/spec/factories/meeting_section_factory.rb b/modules/meeting/spec/factories/meeting_section_factory.rb index 79bf3e3b30f..3c53cb8ec43 100644 --- a/modules/meeting/spec/factories/meeting_section_factory.rb +++ b/modules/meeting/spec/factories/meeting_section_factory.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,7 +30,7 @@ FactoryBot.define do factory :meeting_section do |m| - meeting factory: :structured_meeting + meeting factory: :meeting m.sequence(:title) { |n| "Section #{n}" } end diff --git a/modules/meeting/spec/factories/recurring_meeting_factory.rb b/modules/meeting/spec/factories/recurring_meeting_factory.rb index 1bafc93d571..94025c9446a 100644 --- a/modules/meeting/spec/factories/recurring_meeting_factory.rb +++ b/modules/meeting/spec/factories/recurring_meeting_factory.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -48,7 +49,7 @@ FactoryBot.define do recurring_meeting.project = project # create template - template = create(:structured_meeting_template, + template = create(:meeting_template, :author_participates, author: recurring_meeting.author, recurring_meeting:, diff --git a/modules/meeting/spec/features/meetings_activity_spec.rb b/modules/meeting/spec/features/meetings_activity_spec.rb index e2f099644fa..d7ea85968f5 100644 --- a/modules/meeting/spec/features/meetings_activity_spec.rb +++ b/modules/meeting/spec/features/meetings_activity_spec.rb @@ -34,8 +34,6 @@ RSpec.describe "Meetings", :js do let(:user) { create(:admin) } let!(:meeting) { create(:meeting, project:, title: "Awesome meeting!") } - let!(:agenda) { create(:meeting_agenda, meeting:, text: "foo") } - let!(:minutes) { create(:meeting_minutes, meeting:, text: "minutes") } before do login_as(user) @@ -47,9 +45,6 @@ RSpec.describe "Meetings", :js do check "Meetings" click_on "Apply" - - expect(page).to have_css(".op-activity-list--item-title", text: "Minutes: Awesome meeting!") - expect(page).to have_css(".op-activity-list--item-title", text: "Agenda: Awesome meeting!") expect(page).to have_css(".op-activity-list--item-title", text: "Meeting: Awesome meeting!") end end diff --git a/modules/meeting/spec/features/meetings_attachments_spec.rb b/modules/meeting/spec/features/meetings_attachments_spec.rb deleted file mode 100644 index 18289cbbe69..00000000000 --- a/modules/meeting/spec/features/meetings_attachments_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true -require "spec_helper" -require "features/page_objects/notification" - -RSpec.describe "Add an attachment to a meeting (agenda)", :js, :selenium do - let(:role) do - create(:project_role, permissions: %i[view_meetings edit_meetings create_meeting_agendas]) - end - - let(:dev) do - create(:user, member_with_roles: { project => role }) - end - - let(:project) { create(:project) } - - let(:meeting) do - create( - :meeting, - project:, - title: "Versammlung", - agenda: create(:meeting_agenda, text: "Versammlung") - ) - end - - let(:attachments) { Components::Attachments.new } - let(:image_fixture) { UploadedFile.load_from("spec/fixtures/files/image.png") } - let(:editor) { Components::WysiwygEditor.new } - let(:attachments_list) { Components::AttachmentsList.new } - - before do - login_as(dev) - - visit "/meetings/#{meeting.id}" - - within "#tab-content-agenda .toolbar" do - click_on "Edit" - end - end - - describe "wysiwyg editor" do - context "if on an existing page" do - it "can upload an image via drag & drop" do - find(".ck-content") - - editor.expect_button "Upload image from computer" - - editor.drag_attachment image_fixture.path, "Some image caption" - - click_on "Save" - - content = find_test_selector("op-meeting--meeting_agenda") - - expect(content).to have_css("img") - expect(content).to have_content("Some image caption") - end - end - end - - describe "attachment dropzone" do - it "can upload an image via attaching and drag & drop" do - editor.wait_until_loaded - attachments_list.wait_until_visible - - ## - # Attach file manually - editor.attachments_list.expect_empty - attachments.attach_file_on_input(image_fixture.path) - editor.wait_until_upload_progress_toaster_cleared - editor.attachments_list.expect_attached("image.png") - - ## - # and via drag & drop - editor.attachments_list.drag_enter - editor.attachments_list.drop(image_fixture) - editor.wait_until_upload_progress_toaster_cleared - editor.attachments_list.expect_attached("image.png", count: 2) - end - end -end diff --git a/modules/meeting/spec/features/meetings_close_spec.rb b/modules/meeting/spec/features/meetings_close_spec.rb deleted file mode 100644 index 534a51c779c..00000000000 --- a/modules/meeting/spec/features/meetings_close_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -RSpec.describe "Meetings close" do - let(:project) { create(:project, enabled_module_names: %w[meetings]) } - let(:user) do - create(:user, - member_with_permissions: { project => permissions }) - end - let(:other_user) do - create(:user, - member_with_permissions: { project => permissions }) - end - - let!(:meeting) { create(:meeting, :author_participates, project:, title: "Own awesome meeting!", author: user) } - let!(:meeting_agenda) { create(:meeting_agenda, meeting:, text: "asdf") } - - before do - login_as(user) - end - - context "with permission to close meetings", - :js do - let(:permissions) { %i[view_meetings close_meeting_agendas] } - - it "can delete own and other`s meetings" do - visit project_meetings_path(project) - - click_on meeting.title - - # Go to minutes, expect uneditable - find(".op-tab-row--link", text: "MINUTES").click - wait_for_network_idle - expect(page).to have_css(".button", text: "Close the agenda to begin the Minutes") - - # Close the meeting - find(".op-tab-row--link", text: "AGENDA").click - wait_for_network_idle - - accept_confirm do - find(".button", text: "Close").click - end - - # Expect to be on minutes - expect(page).to have_css(".op-tab-row--link_selected", text: "MINUTES") - - # Copies the text - expect(page).to have_css("#tab-content-minutes", text: "asdf") - - # Go back to agenda, expect we can open it again - find(".op-tab-row--link", text: "AGENDA").click - accept_confirm do - find(".button", text: "Open").click - end - expect(page).to have_css(".button", text: "Close") - end - end - - context "without permission to close meetings" do - let(:permissions) { %i[view_meetings] } - - it "cannot delete own and other`s meetings" do - visit project_meetings_path(project) - - expect(page) - .to have_no_link "Close" - end - end -end diff --git a/modules/meeting/spec/features/meetings_copy_spec.rb b/modules/meeting/spec/features/meetings_copy_spec.rb deleted file mode 100644 index 6d4183a628a..00000000000 --- a/modules/meeting/spec/features/meetings_copy_spec.rb +++ /dev/null @@ -1,133 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -RSpec.describe "Meetings copy", :js do - shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) } - shared_let(:permissions) { %i[view_meetings create_meetings] } - shared_let(:user) do - create(:user, - member_with_permissions: { project => permissions }).tap do |u| - u.pref[:time_zone] = "Etc/UTC" - - u.save! - end - end - shared_let(:other_user) do - create(:user, - member_with_permissions: { project => permissions }) - end - - shared_let(:start_time) { Time.current.next_day.at_noon } - shared_let(:duration) { 1.5 } - shared_let(:agenda_text) { "We will talk" } - shared_let(:meeting) do - create(:meeting, - :author_participates, - author: user, - project:, - title: "Awesome meeting!", - location: "Meeting room", - duration:, - start_time:).tap do |m| - create(:meeting_agenda, meeting: m, text: agenda_text) - create(:meeting_participant, :attendee, meeting: m, user: other_user) - end - end - - shared_let(:twelve_hour_format) { "%I:%M %p" } - shared_let(:copied_meeting_time_heading) do - date = (start_time + 1.day).strftime("%m/%d/%Y") - start_of_meeting = start_time.strftime(twelve_hour_format) - end_of_meeting = (start_time + meeting.duration.hours).strftime(twelve_hour_format) - - "Start time: #{date} #{start_of_meeting} - #{end_of_meeting} UTC+00:00" - end - - before do - login_as user - end - - it "copying a meeting" do - visit project_meetings_path(project) - - click_on meeting.title - - find_test_selector("meetings-more-dropdown-menu").click - page.within(".menu-drop-down-container") do - click_on "Copy" - end - - expect(page) - .to have_field "Title", with: meeting.title - expect(page) - .to have_field "Location", with: meeting.location - expect(page) - .to have_field "Duration", with: meeting.duration - expect(page) - .to have_field "Start date", with: (start_time + 1.day).strftime("%Y-%m-%d") - expect(page) - .to have_field "Time", with: start_time.strftime("%H:%M") - - click_on "Create" - - # Be on the new meeting's page with copied over attributes - expect(page).to have_no_current_path meeting_path(meeting.id) - - expect(page) - .to have_content("Added by #{user.name}") - expect(page) - .to have_content("Meeting: #{meeting.title}") - expect(page) - .to have_content(copied_meeting_time_heading) - expect(page) - .to have_content("Location: #{meeting.location}") - - # Copies the invitees - expect(page) - .to have_content "Invitees: #{other_user.name}" - - # Does not copy the attendees - expect(page) - .to have_no_content "Attendees: #{other_user.name}" - expect(page) - .to have_content "Attendees:" - - # Copies the agenda - click_on "Agenda" - expect(page) - .to have_content agenda_text - - # Adds an entry to the history - click_on "History" - expect(page) - .to have_content("Copied from Meeting ##{meeting.id}") - end -end diff --git a/modules/meeting/spec/features/meetings_delete_spec.rb b/modules/meeting/spec/features/meetings_delete_spec.rb deleted file mode 100644 index 17943bd6c6d..00000000000 --- a/modules/meeting/spec/features/meetings_delete_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -RSpec.describe "Meetings deletion" do - let(:project) { create(:project, enabled_module_names: %w[meetings]) } - let(:user) do - create(:user, - member_with_permissions: { project => permissions }) - end - let(:other_user) do - create(:user, - member_with_permissions: { project => permissions }) - end - - let!(:meeting) { create(:meeting, project:, title: "Own awesome meeting!", author: user) } - let!(:other_meeting) { create(:meeting, project:, title: "Other awesome meeting!", author: other_user) } - - let(:index_path) { project_meetings_path(project) } - - before do - create(:meeting_participant, :invitee, user:, meeting:) - create(:meeting_participant, :invitee, user:, meeting: other_meeting) - - login_as(user) - end - - context "with permission to delete meetings", :js do - let(:permissions) { %i[view_meetings delete_meetings] } - - it "can delete own and other's meetings" do - visit index_path - - click_on meeting.title - accept_confirm do - find_test_selector("meetings-more-dropdown-menu").click - click_on "Delete" - end - - expect(page) - .to have_current_path index_path - - click_on other_meeting.title - accept_confirm do - find_test_selector("meetings-more-dropdown-menu").click - click_on "Delete" - end - - expect(page) - .to have_content(I18n.t("meeting.blankslate.title")) - - expect(page) - .to have_current_path index_path - end - end - - context "without permission to delete meetings" do - let(:permissions) { %i[view_meetings] } - - it "cannot delete own and other's meetings" do - visit index_path - - click_on meeting.title - expect(page) - .to have_no_link "Delete" - - visit index_path - - click_on other_meeting.title - expect(page) - .to have_no_link "Delete" - end - end -end diff --git a/modules/meeting/spec/features/meetings_new_spec.rb b/modules/meeting/spec/features/meetings_new_spec.rb deleted file mode 100644 index fd6393051d7..00000000000 --- a/modules/meeting/spec/features/meetings_new_spec.rb +++ /dev/null @@ -1,349 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -require_relative "../support/pages/meetings/index" - -RSpec.describe "Meetings new", :js do - shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) } - shared_let(:admin) { create(:admin) } - let(:time_zone) { "Etc/UTC" } - let(:user) do - create(:user, - lastname: "First", - member_with_permissions: { project => permissions }).tap do |u| - u.pref[:time_zone] = time_zone - - u.save! - end - end - let(:other_user) do - create(:user, - lastname: "Second", - member_with_permissions: { project => permissions }) - end - let(:permissions) { %i[view_meetings create_meetings] } - let(:current_user) { user } - - before do - login_as current_user - end - - context "when creating a meeting from the global page" do - before do - other_user - project - end - - let(:index_page) { Pages::Meetings::Index.new(project: nil) } - let(:new_page) { Pages::Meetings::New.new(nil) } - - context "with permission to create meetings" do - it "does not render menus" do - new_page.visit! - new_page.expect_no_main_menu - end - - describe "clicking on the create new meeting button" do - it "navigates to the global create form" do - index_page.visit! - index_page.click_create_new - expect(page).to have_current_path(new_page.path) - end - end - - ["CET", "UTC", "", "Pacific Time (US & Canada)"].each do |zone| - let(:time_zone) { zone } - - it "allows creating a project and handles errors in time zone #{zone}" do - new_page.visit! - - expect_angular_frontend_initialized # Wait for project dropdown to be ready - - new_page.set_title "Some title" - new_page.set_project project - - new_page.set_start_date "2013-03-28" - new_page.set_start_time "13:30" - new_page.set_duration "1.5" - new_page.invite(other_user) - - show_page = new_page.click_create - - expect_flash(message: "Successful creation.") - - show_page.expect_invited(user, other_user) - - show_page.expect_date_time "03/28/2013 01:30 PM - 03:00 PM" - end - end - - context "without a title set" do - before do - new_page.visit! - - # Wait for project dropdown to be initialized - expect_angular_frontend_initialized - - new_page.set_project project - - new_page.set_start_date "2013-03-28" - new_page.set_start_time "13:30" - new_page.set_duration "1.5" - new_page.invite(other_user) - end - - it "renders a validation error" do - expect do - new_page.click_create - end.not_to change(Query, :count) - - # HTML required attribute validation error - expect(page).to have_current_path(new_page.path) - end - end - - context "without a project set" do - before do - new_page.visit! - new_page.set_title "Some title" - new_page.set_start_date "2013-03-28" - new_page.set_start_time "13:30" - new_page.set_duration "1.5" - end - - it "renders a validation error" do - new_page.click_create - - expect_flash(type: :error, - message: "#{Project.model_name.human} #{I18n.t('activerecord.errors.messages.blank')}") - - new_page.expect_project_dropdown - end - end - end - - context "without permission to create meetings" do - let(:permissions) { %i[view_meetings] } - - it "shows no edit link" do - index_page.visit! - - index_page.expect_no_create_new_button - end - end - - context "as an admin" do - let(:current_user) { admin } - - it "allows creating meeting in a project without members" do - new_page.visit! - - expect_angular_frontend_initialized # Wait for project dropdown to be ready - - new_page.set_title "Some title" - - new_page.set_project project - - wait_for_network_idle # Wait for participant section to be fetched - - show_page = new_page.click_create - - expect_flash(message: "Successful creation.") - - # Not sure if that is then intended behaviour but that is what is currently programmed - show_page.expect_invited(admin) - end - - context "without a project set" do - before do - new_page.visit! - new_page.set_title "Some title" - end - - it "renders a validation error" do - new_page.click_create - - expect(page).to have_text "#{Project.model_name.human} #{I18n.t('activerecord.errors.messages.blank')}" - new_page.expect_project_dropdown - end - end - - context "without a title set" do - before do - new_page.visit! - - # Wait for project dropdown to be initialized - expect_angular_frontend_initialized - - new_page.set_project project - end - - it "renders a validation error" do - expect do - new_page.click_create - end.not_to change(Query, :count) - - # HTML required attribute validation error - expect(page).to have_current_path(new_page.path) - end - end - end - end - - context "when creating a meeting from the project-specific page" do - let(:index_page) { Pages::Meetings::Index.new(project:) } - let(:new_page) { Pages::Meetings::New.new(project) } - - context "with permission to create meetings" do - before do - other_user - end - - describe "clicking on the create new meeting button" do - it "navigates to the project-specific create form" do - index_page.visit! - index_page.click_create_new - expect(page).to have_current_path(new_page.path) - end - end - - ["CET", "UTC", "", "Pacific Time (US & Canada)"].each do |zone| - let(:time_zone) { zone } - - it "allows creating a project and handles errors in time zone #{zone}" do - new_page.visit! - - new_page.set_title "Some title" - new_page.set_start_date "2013-03-28" - new_page.set_start_time "13:30" - new_page.set_duration "1.5" - new_page.invite(other_user) - - show_page = new_page.click_create - - expect_flash(message: "Successful creation.") - - show_page.expect_invited(user, other_user) - - show_page.expect_date_time "03/28/2013 01:30 PM - 03:00 PM" - end - end - - context "without a title set" do - before do - new_page.visit! - new_page.set_start_date "2013-03-28" - new_page.set_start_time "13:30" - new_page.set_duration "1.5" - new_page.invite(other_user) - end - - it "renders a validation error" do - expect do - new_page.click_create - end.not_to change(Query, :count) - - # HTML required attribute validation error - expect(page).to have_current_path(new_page.path) - end - end - end - - context "without permission to create meetings" do - let(:permissions) { %i[view_meetings] } - - it "shows no edit link" do - index_page.visit! - - index_page.expect_no_create_new_button - end - end - - context "as an admin" do - let(:current_user) { admin } - let(:field) do - TextEditorField.new(page, - "", - selector: test_selector("op-meeting--meeting_agenda")) - end - - it "allows creating meeting in a project without members" do - new_page.visit! - - new_page.set_title "Some title" - - show_page = new_page.click_create - - expect_flash(message: "Successful creation.") - - # Not sure if that is then intended behaviour but that is what is currently programmed - show_page.expect_invited(admin) - end - - context "without a title set" do - before do - new_page.visit! - end - - it "renders a validation error" do - expect do - new_page.click_create - end.not_to change(Query, :count) - - # HTML required attribute validation error - expect(page).to have_current_path(new_page.path) - end - end - - it "can save the meeting agenda via cmd+Enter" do - new_page.visit! - - new_page.set_title "Some title" - - new_page.click_create - - expect_flash(message: "Successful creation.") - - meeting = Meeting.last - - field.set_value("My new meeting text") - - field.submit_by_enter - - expect_flash(message: "Successful update") - - meeting.reload - - expect(meeting.agenda.text).to eq "My new meeting text" - end - end - end -end diff --git a/modules/meeting/spec/features/meetings_participants_spec.rb b/modules/meeting/spec/features/meetings_participants_spec.rb deleted file mode 100644 index 16ec0cd6d2d..00000000000 --- a/modules/meeting/spec/features/meetings_participants_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -require_relative "../support/pages/meetings/edit" - -RSpec.describe "Meetings participants" do - let(:project) { create(:project, enabled_module_names: %w[meetings]) } - let!(:user) do - create(:user, - firstname: "Current", - member_with_permissions: { project => %i[view_meetings edit_meetings] }) - end - let!(:viewer_user) do - create(:user, - firstname: "Viewer", - member_with_permissions: { project => %i[view_meetings] }) - end - let!(:non_viewer_user) do - create(:user, - firstname: "Nonviewer", - member_with_permissions: { project => %i[] }) - end - let(:edit_page) { Pages::Meetings::Edit.new(meeting) } - let!(:meeting) { create(:meeting, project:, title: "Awesome meeting!") } - - before do - login_as(user) - end - - it "allows setting members to participants which are allowed to view the meeting" do - edit_page.visit! - - edit_page.expect_available_participant(user) - edit_page.expect_available_participant(viewer_user) - - edit_page.expect_not_available_participant(non_viewer_user) - - edit_page.invite(viewer_user) - show_page = edit_page.click_save - expect_flash(message: "Successful update") - - show_page.expect_invited(viewer_user) - - show_page.click_edit - - edit_page.uninvite(viewer_user) - show_page = edit_page.click_save - - expect_flash(message: "Successful update") - - show_page.expect_uninvited(viewer_user) - end - - context "with an invalid user reference" do - let(:show_page) { Pages::Meetings::Show.new(meeting) } - let(:meeting_participant) { create(:meeting_participant, user: viewer_user, meeting:) } - - before do - meeting_participant.update_column(:user_id, 12341234) - end - - it "still allows to view the meeting" do - show_page.visit! - - show_page.expect_invited meeting.author - show_page.expect_uninvited viewer_user - end - end -end diff --git a/modules/meeting/spec/features/meetings_search_spec.rb b/modules/meeting/spec/features/meetings_search_spec.rb index 48a9188823d..a8f96b18584 100644 --- a/modules/meeting/spec/features/meetings_search_spec.rb +++ b/modules/meeting/spec/features/meetings_search_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -35,7 +36,7 @@ RSpec.describe "Meeting search", :js do let(:role) { create(:project_role, permissions: %i(view_meetings view_work_packages)) } let(:user) { create(:user, member_with_roles: { project => role }) } - let!(:meeting) { create(:structured_meeting, project:) } + let!(:meeting) { create(:meeting, project:) } let!(:agenda_item) { create(:meeting_agenda_item, meeting:) } before do diff --git a/modules/meeting/spec/features/meetings_show_spec.rb b/modules/meeting/spec/features/meetings_show_spec.rb deleted file mode 100644 index 93ec40796b8..00000000000 --- a/modules/meeting/spec/features/meetings_show_spec.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" -require_relative "../support/pages/meetings/show" - -RSpec.describe "Meetings", :js do - let(:project) { create(:project, enabled_module_names: %w[meetings]) } - let(:role) { create(:project_role, permissions:) } - let(:user) do - create(:user, - member_with_roles: { project => role }) - end - - let!(:meeting) { create(:meeting, project:, title: "Awesome meeting!") } - let(:show_page) { Pages::Meetings::Show.new(meeting) } - - current_user { user } - - describe "navigate to meeting page" do - before do - create(:meeting_participant, :invitee, user:, meeting:) - end - - let(:permissions) { %i[view_meetings] } - - it "can visit the meeting" do - visit meetings_path(project) - - find("div.title a", text: "Awesome meeting!", wait: 10).click - expect(page).to have_css("h2", text: "Meeting: Awesome meeting!") - - expect(page).to have_test_selector("op-meeting--meeting_agenda", - text: "There is currently nothing to display") - end - - context "with a location" do - context "as a valid url" do - it "renders a link to the meeting location" do - show_page.visit! - - show_page.expect_link_to_location(meeting.location) - end - end - - context "as an invalid url" do - before do - meeting.update!(location: "badurl") - end - - it "renders the meeting location as plaintext" do - show_page.visit! - - show_page.expect_plaintext_location(meeting.location) - end - end - end - - context "with an open agenda" do - let!(:agenda) { create(:meeting_agenda, meeting:, text: "foo") } - let(:agenda_update) { create(:meeting_agenda, meeting:, text: "bla") } - - it "shows the agenda" do - visit meeting_path(meeting) - expect(page).to have_test_selector("op-meeting--meeting_agenda", - text: "foo") - - # May not edit - expect(page).to have_no_css(".button--edit-agenda") - expect(page).not_to have_test_selector("op-meeting--meeting_agenda", - text: "Edit") - end - - it "can view history" do - agenda_update - - visit meeting_path(meeting) - - click_on "History" - - find_by_id("version-1").click - expect(page).to have_test_selector("op-meeting--meeting_agenda", text: "foo") - end - - context "and edit permissions" do - let(:permissions) { %i[view_meetings create_meeting_agendas] } - let(:field) do - TextEditorField.new(page, - "", - selector: test_selector("op-meeting--meeting_agenda")) - end - - it "can edit the agenda" do - visit meeting_path(meeting) - - find(".toolbar-item", text: "Edit").click - - field.expect_value("foo") - - field.set_value("My new meeting text") - - field.submit_by_enter - - expect_and_dismiss_flash(message: "Successful update") - - meeting.reload - - expect(meeting.agenda.text).to eq "My new meeting text" - end - end - - context "and edit minutes permissions" do - let(:permissions) { %i[view_meetings create_meeting_minutes] } - - it "can not edit the minutes" do - visit meeting_path(meeting) - click_on "Minutes" - expect(page).not_to have_test_selector("op-meeting--meeting_minutes", text: "Edit") - expect(page).to have_test_selector("op-meeting--meeting_minutes", - text: "There is currently nothing to display") - end - end - end - - context "with a locked agenda" do - let!(:agenda) { create(:meeting_agenda, meeting:, text: "foo", locked: true) } - - it "shows the minutes when visiting" do - visit meeting_path(meeting) - expect(page).to have_no_css("h2", text: "Agenda") - expect(page).to have_no_css("#meeting_minutes_text") - expect(page).to have_css("h2", text: "Minutes") - end - - context "and edit permissions" do - let(:permissions) { %i[view_meetings create_meeting_minutes] } - let(:field) do - TextEditorField.new(page, - "", - selector: test_selector("op-meeting--meeting_minutes")) - end - - it "can edit the minutes" do - visit meeting_path(meeting) - - field.set_value("This is what we talked about") - - click_on "Save" - - expect(page) - .to have_css(".op-uc-container", - text: "This is what we talked about") - end - end - end - end -end diff --git a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_create_spec.rb b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_create_spec.rb index 721149f006c..77d87002176 100644 --- a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_create_spec.rb +++ b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_create_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,8 +30,7 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/recurring_meeting/show" require_relative "../../support/pages/meetings/index" @@ -61,7 +61,7 @@ RSpec.describe "Recurring meetings creation", let(:current_user) { user } let(:meeting) { RecurringMeeting.last } let(:show_page) { Pages::RecurringMeeting::Show.new(meeting) } - let(:template_page) { Pages::StructuredMeeting::Show.new(meeting.template) } + let(:template_page) { Pages::Meetings::Show.new(meeting.template) } let(:meetings_page) { Pages::Meetings::Index.new(project:) } before do diff --git a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_crud_spec.rb b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_crud_spec.rb index aed1150fd41..362a047985a 100644 --- a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_crud_spec.rb +++ b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_crud_spec.rb @@ -30,8 +30,7 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/recurring_meeting/show" require_relative "../../support/pages/meetings/index" diff --git a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_end_series_spec.rb b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_end_series_spec.rb index a54d08eb525..ca45180dc36 100644 --- a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_end_series_spec.rb +++ b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_end_series_spec.rb @@ -30,8 +30,7 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/recurring_meeting/show" require_relative "../../support/pages/meetings/index" diff --git a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_create_spec.rb b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_create_spec.rb index 795a56f15c0..cc9d629044f 100644 --- a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_create_spec.rb +++ b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_create_spec.rb @@ -29,8 +29,7 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/recurring_meeting/show" require_relative "../../support/pages/meetings/index" diff --git a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_crud_spec.rb b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_crud_spec.rb index da7583bf956..6f03deb0517 100644 --- a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_crud_spec.rb +++ b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_global_crud_spec.rb @@ -30,8 +30,7 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/recurring_meeting/show" require_relative "../../support/pages/meetings/index" diff --git a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_move_to_next_spec.rb b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_move_to_next_spec.rb index 860a5808317..80e4c362586 100644 --- a/modules/meeting/spec/features/recurring_meetings/recurring_meeting_move_to_next_spec.rb +++ b/modules/meeting/spec/features/recurring_meetings/recurring_meeting_move_to_next_spec.rb @@ -30,8 +30,7 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/recurring_meeting/show" require_relative "../../support/pages/meetings/index" @@ -60,8 +59,8 @@ RSpec.describe "Recurring meetings move to next meeting", :js do end_after: "never", author: user_with_manage_permissions end - shared_let(:structured_meeting) do - create :structured_meeting, + shared_let(:meeting) do + create :meeting, project:, start_time: DateTime.parse("2025-01-28T10:30:00Z"), duration: 1, @@ -75,10 +74,8 @@ RSpec.describe "Recurring meetings move to next meeting", :js do series.meetings.not_templated.first end - let!(:agenda_item) { create(:meeting_agenda_item, meeting: structured_meeting, title: "Test notes") } - let!(:series_agenda_item) { create(:meeting_agenda_item, meeting: recurring_meeting, title: "Test notes") } - - let(:meeting_page) { Pages::StructuredMeeting::Show.new(meeting) } + let!(:agenda_item) { create(:meeting_agenda_item, meeting:, title: "Test notes") } + let(:meeting_page) { Pages::Meetings::Show.new(meeting) } before do login_as current_user @@ -94,12 +91,13 @@ RSpec.describe "Recurring meetings move to next meeting", :js do it "shows the move to next meeting option" do meeting_page.expect_agenda_item(title: "Test notes") - meeting_page.expect_agenda_action_menu(series_agenda_item) accept_confirm do - meeting_page.select_action(series_agenda_item, "Move to next meeting") + meeting_page.select_action(agenda_item, "Move to next meeting") end + expect_and_dismiss_flash(message: "Agenda item moved to the next meeting") + meeting_page.expect_no_agenda_item(title: "Test notes") end end @@ -115,10 +113,9 @@ RSpec.describe "Recurring meetings move to next meeting", :js do it "shows the move to next meeting option" do meeting_page.expect_agenda_item(title: "Test notes") - meeting_page.expect_agenda_action_menu(series_agenda_item) accept_confirm do - meeting_page.select_action(series_agenda_item, "Move to next meeting") + meeting_page.select_action(agenda_item, "Move to next meeting") end expect(page).to have_text "Unable to move to the next meeting since it has been cancelled." @@ -136,8 +133,7 @@ RSpec.describe "Recurring meetings move to next meeting", :js do end end - context "when viewing a structured meeting" do - let(:meeting) { structured_meeting } + context "when viewing a one-time meeting" do let(:current_user) { user_with_manage_permissions } it "does not show the move to next meeting option" do diff --git a/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb b/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb index 8e2e41f26c7..5ff6d968bfd 100644 --- a/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/attachment_upload_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,7 +30,7 @@ require "spec_helper" require "features/page_objects/notification" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" RSpec.describe "Upload attachment to meetings", :js do let(:user) do @@ -43,8 +44,8 @@ RSpec.describe "Upload attachment to meetings", :js do let(:wiki_page_content) { project.wiki.pages.first.text } let(:attachment_list) { Components::AttachmentsList.new("#content") } - let(:meeting) { create(:structured_meeting, project:) } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let(:meeting) { create(:meeting, project:) } + let(:show_page) { Pages::Meetings::Show.new(meeting) } before do login_as(user) diff --git a/modules/meeting/spec/features/structured_meetings/history_spec.rb b/modules/meeting/spec/features/structured_meetings/history_spec.rb index feb6a9d28f8..78d9296e73d 100644 --- a/modules/meeting/spec/features/structured_meetings/history_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/history_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,9 +30,8 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" -require_relative "../../support/pages/structured_meeting/history" +require_relative "../../support/pages/meetings/show" +require_relative "../../support/pages/meetings/history" RSpec.describe "history", :js do @@ -72,7 +72,7 @@ RSpec.describe "history", end shared_let(:meeting) do User.execute_as(user) do - create(:structured_meeting, + create(:meeting, project:, start_time: DateTime.parse("2024-03-28T13:30:00Z"), title: "Some title", @@ -82,8 +82,8 @@ RSpec.describe "history", end end - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } - let(:history_page) { Pages::StructuredMeeting::History.new(meeting) } + let(:show_page) { Pages::Meetings::Show.new(meeting) } + let(:history_page) { Pages::Meeting::History.new(meeting) } let(:editor) { Components::WysiwygEditor.new "#content", "opce-ckeditor-augmented-textarea" } it "allows browsing the history", with_settings: { journal_aggregation_time_minutes: 0 } do diff --git a/modules/meeting/spec/features/structured_meetings/meeting_outcomes/meeting_outcomes_crud_spec.rb b/modules/meeting/spec/features/structured_meetings/meeting_outcomes/meeting_outcomes_crud_spec.rb index 5c11ad33be1..0f5eee55cce 100644 --- a/modules/meeting/spec/features/structured_meetings/meeting_outcomes/meeting_outcomes_crud_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/meeting_outcomes/meeting_outcomes_crud_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" -require_relative "../../../support/pages/structured_meeting/show" +require_relative "../../../support/pages/meetings/show" RSpec.describe "Meeting Outcomes CRUD", :js do shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) } @@ -46,7 +46,7 @@ RSpec.describe "Meeting Outcomes CRUD", :js do member_with_permissions: { project => %i[view_meetings manage_agendas close_meeting_agendas] } end shared_let(:meeting) do - create :structured_meeting, + create :meeting, project:, start_time: "2024-12-31T13:30:00Z", duration: 1.5, @@ -59,7 +59,7 @@ RSpec.describe "Meeting Outcomes CRUD", :js do let(:current_user) { user } let(:state) { :in_progress } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let(:show_page) { Pages::Meetings::Show.new(meeting) } let(:field) do TextEditorField.new(page, "Outcome", selector: test_selector("meeting-outcome-input")) end diff --git a/modules/meeting/spec/features/structured_meetings/mobile_structure_meeting_spec.rb b/modules/meeting/spec/features/structured_meetings/mobile_structure_meeting_spec.rb index 997873fbc0a..8a758188e16 100644 --- a/modules/meeting/spec/features/structured_meetings/mobile_structure_meeting_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/mobile_structure_meeting_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,9 +30,9 @@ require "spec_helper" -require_relative "../../support/pages/structured_meeting//mobile/show" +require_relative "../../support/pages/meetings/mobile/show" -RSpec.describe "Structured meetings CRUD", +RSpec.describe "Meetings CRUD", :js do include Components::Autocompleter::NgSelectAutocompleteHelpers @@ -57,14 +58,14 @@ RSpec.describe "Structured meetings CRUD", end shared_let(:meeting) do - create(:structured_meeting, + create(:meeting, :author_participates, project:, author: user) end let(:current_user) { user } - let(:show_page) { Pages::StructuredMeeting::Mobile::Show.new(StructuredMeeting.last) } + let(:show_page) { Pages::Meetings::Mobile::Show.new(Meeting.last) } include_context "with mobile screen size" @@ -73,7 +74,7 @@ RSpec.describe "Structured meetings CRUD", show_page.visit! end - it "can edit participants of a structured meeting" do + it "can edit participants of a meeting" do expect(page).to have_current_path(show_page.path) show_page.expect_participants(count: 1) diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb index f9ba5e27b9f..2046909f019 100644 --- a/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb @@ -30,11 +30,10 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/meetings/index" -RSpec.describe "Structured meetings CRUD", +RSpec.describe "Meetings CRUD", :js do include Components::Autocompleter::NgSelectAutocompleteHelpers @@ -63,9 +62,8 @@ RSpec.describe "Structured meetings CRUD", end let(:current_user) { user } - let(:new_page) { Pages::Meetings::New.new(project) } - let(:meeting) { StructuredMeeting.last } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let(:meeting) { Meeting.last } + let(:show_page) { Pages::Meetings::Show.new(meeting) } let(:meetings_page) { Pages::Meetings::Index.new(project:) } before do |test| @@ -88,7 +86,7 @@ RSpec.describe "Structured meetings CRUD", meetings_page.click_create end - it "can create a structured meeting and add agenda items" do + it "can create a meeting and add agenda items" do expect_and_dismiss_flash(type: :success, message: "Successful creation") # Does not send invitation mails by default @@ -312,7 +310,7 @@ RSpec.describe "Structured meetings CRUD", fill_in "Title", with: "Some title" click_on "Create meeting" - new_meeting = StructuredMeeting.last + new_meeting = Meeting.last expect(page).to have_current_path "/projects/#{project.identifier}/meetings/#{new_meeting.id}" # check for copied agenda items @@ -330,13 +328,13 @@ RSpec.describe "Structured meetings CRUD", end context "with a work package reference to another" do - let!(:meeting) { create(:structured_meeting, project:, author: current_user) } + let!(:meeting) { create(:meeting, project:, author: current_user) } let!(:other_project) { create(:project) } let!(:other_wp) { create(:work_package, project: other_project, author: current_user, subject: "Private task") } let!(:role) { create(:project_role, permissions: %w[view_work_packages]) } let!(:membership) { create(:member, principal: user, project: other_project, roles: [role]) } let!(:agenda_item) { create(:wp_meeting_agenda_item, meeting:, author: current_user, work_package: other_wp) } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let(:show_page) { Pages::Meetings::Show.new(meeting) } it "shows correctly for author, but returns an unresolved reference for the second user" do show_page.visit! @@ -352,8 +350,8 @@ RSpec.describe "Structured meetings CRUD", end context "with sections" do - let!(:meeting) { create(:structured_meeting, project:, author: current_user) } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let!(:meeting) { create(:meeting, project:, author: current_user) } + let(:show_page) { Pages::Meetings::Show.new(meeting) } context "when starting with empty sections" do it "can add, edit and delete sections" do diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_delete_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_delete_spec.rb index fc8a25344d4..6779b75c08d 100644 --- a/modules/meeting/spec/features/structured_meetings/structured_meeting_delete_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_delete_spec.rb @@ -30,11 +30,10 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/meetings/index" -RSpec.describe "Structured meetings deletion", +RSpec.describe "Meetings deletion", :js do include Components::Autocompleter::NgSelectAutocompleteHelpers diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_global_crud_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_global_crud_spec.rb index 64cb70ac8e2..78866d68202 100644 --- a/modules/meeting/spec/features/structured_meetings/structured_meeting_global_crud_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_global_crud_spec.rb @@ -30,11 +30,10 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" require_relative "../../support/pages/meetings/index" -RSpec.describe "Structured meetings global CRUD", :js do +RSpec.describe "Meetings global CRUD", :js do include Components::Autocompleter::NgSelectAutocompleteHelpers shared_let(:project) { create(:project, enabled_module_names: %w[meetings work_package_tracking]) } @@ -62,9 +61,8 @@ RSpec.describe "Structured meetings global CRUD", :js do end let(:current_user) { user } - let(:new_page) { Pages::Meetings::New.new(project) } - let(:meeting) { StructuredMeeting.last } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let(:meeting) { Meeting.last } + let(:show_page) { Pages::Meetings::Show.new(meeting) } let(:meetings_page) { Pages::Meetings::Index.new(project: nil) } before do diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_participant_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_participant_spec.rb index 6df140abe18..86091b124c2 100644 --- a/modules/meeting/spec/features/structured_meetings/structured_meeting_participant_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_participant_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,9 +30,9 @@ require "spec_helper" -require_relative "../../support/pages/structured_meeting//mobile/show" +require_relative "../../support/pages/meetings/mobile/show" -RSpec.describe "Structured meetings participants", +RSpec.describe "Meetings participants", :js do include Components::Autocompleter::NgSelectAutocompleteHelpers @@ -57,21 +58,21 @@ RSpec.describe "Structured meetings participants", end shared_let(:meeting) do - create(:structured_meeting, + create(:meeting, :author_participates, project:, author: user) end let(:current_user) { user } - let(:show_page) { Pages::StructuredMeeting::Show.new(StructuredMeeting.last) } + let(:show_page) { Pages::Meetings::Show.new(Meeting.last) } before do login_as current_user show_page.visit! end - it "can edit participants of a structured meeting" do + it "can edit participants of a meeting" do expect(page).to have_current_path(show_page.path) show_page.open_participant_form diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_update_flash_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_update_flash_spec.rb index ed037ac0320..2db7b6b2951 100644 --- a/modules/meeting/spec/features/structured_meetings/structured_meeting_update_flash_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_update_flash_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,10 +30,9 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" -RSpec.describe "Structured meetings CRUD", +RSpec.describe "Meetings CRUD", :js, :selenium do include Components::Autocompleter::NgSelectAutocompleteHelpers @@ -41,9 +41,8 @@ RSpec.describe "Structured meetings CRUD", shared_let(:user) { create(:admin) } current_user { user } - let(:new_page) { Pages::Meetings::New.new(project) } - let(:meeting) { create(:structured_meeting, project:, author: current_user) } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting) } + let(:meeting) { create(:meeting, project:, author: current_user) } + let(:show_page) { Pages::Meetings::Show.new(meeting) } describe "meeting update flash" do before do @@ -134,7 +133,7 @@ RSpec.describe "Structured meetings CRUD", ## Edit meeting details within_window(first_window) do find_test_selector("edit-meeting-details-button").click - fill_in "structured_meeting_duration", with: "2.5" + fill_in "meeting_duration", with: "2.5" click_link_or_button "Save" # Expect updated duration diff --git a/modules/meeting/spec/features/structured_meetings/turbo_links_spec.rb b/modules/meeting/spec/features/structured_meetings/turbo_links_spec.rb index 935958f11a5..fa027164a9c 100644 --- a/modules/meeting/spec/features/structured_meetings/turbo_links_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/turbo_links_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,10 +30,9 @@ require "spec_helper" -require_relative "../../support/pages/meetings/new" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" -RSpec.describe "Structured meetings links caught by turbo", +RSpec.describe "Meetings links caught by turbo", :js do include Rails.application.routes.url_helpers @@ -47,8 +47,8 @@ RSpec.describe "Structured meetings links caught by turbo", u.save! end end - shared_let(:meeting1) { create(:structured_meeting, title: "First meeting", project:) } - shared_let(:meeting2) { create(:structured_meeting, title: "Other meeting", project:) } + shared_let(:meeting1) { create(:meeting, title: "First meeting", project:) } + shared_let(:meeting2) { create(:meeting, title: "Other meeting", project:) } let(:notes) do <<~NOTES @@ -56,7 +56,7 @@ RSpec.describe "Structured meetings links caught by turbo", NOTES end let!(:agenda_item) { create(:meeting_agenda_item, meeting: meeting1, notes:) } - let(:show_page) { Pages::StructuredMeeting::Show.new(meeting1) } + let(:show_page) { Pages::Meetings::Show.new(meeting1) } before do login_as user diff --git a/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb b/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb index 8ce9d2e8deb..510735987ee 100644 --- a/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/work_package_meetings_tab_spec.rb @@ -30,7 +30,7 @@ require "spec_helper" require_relative "../../support/pages/work_package_meetings_tab" -require_relative "../../support/pages/structured_meeting/show" +require_relative "../../support/pages/meetings/show" RSpec.describe "Open the Meetings tab", :js do @@ -98,8 +98,8 @@ RSpec.describe "Open the Meetings tab", context "when the user has the permission to see the tab, but the work package is linked in two projects" do let(:other_project) { create(:project, enabled_module_names: %w[meetings]) } - let!(:visible_meeting) { create(:structured_meeting, project:) } - let!(:invisible_meeting) { create(:structured_meeting, project: other_project) } + let!(:visible_meeting) { create(:meeting, project:) } + let!(:invisible_meeting) { create(:meeting, project: other_project) } let!(:meeting_agenda_item_of_visible_meeting) do create(:meeting_agenda_item, meeting: visible_meeting, work_package:, notes: "Public note!") @@ -131,7 +131,7 @@ RSpec.describe "Open the Meetings tab", end context "with another past meeting" do - let!(:past_meeting) { create(:structured_meeting, project:, start_time: 1.week.ago) } + let!(:past_meeting) { create(:meeting, project:, start_time: 1.week.ago) } let!(:past_agenda_item) do create(:meeting_agenda_item, meeting: past_meeting, work_package:, notes: "Public note!") @@ -225,8 +225,8 @@ RSpec.describe "Open the Meetings tab", end context "when the work_package is already referenced in upcoming meetings" do - let!(:first_meeting) { create(:structured_meeting, project:) } - let!(:second_meeting) { create(:structured_meeting, project:) } + let!(:first_meeting) { create(:meeting, project:) } + let!(:second_meeting) { create(:meeting, project:) } let!(:first_meeting_agenda_item_of_first_meeting) do create(:meeting_agenda_item, meeting: first_meeting, work_package:, notes: "A very important note in first meeting!") @@ -267,7 +267,7 @@ RSpec.describe "Open the Meetings tab", end context "when the work_package is referenced and has an outcome" do - let!(:meeting) { create(:structured_meeting, project:) } + let!(:meeting) { create(:meeting, project:) } let!(:meeting_agenda_item) do create(:meeting_agenda_item, meeting:, work_package:, notes: "A very important note in first meeting!") @@ -291,8 +291,8 @@ RSpec.describe "Open the Meetings tab", end context "when the work_package was already referenced in past meetings" do - let!(:first_past_meeting) { create(:structured_meeting, project:, start_time: Date.yesterday - 11.hours) } - let!(:second_past_meeting) { create(:structured_meeting, project:, start_time: Date.yesterday - 10.hours) } + let!(:first_past_meeting) { create(:meeting, project:, start_time: Date.yesterday - 11.hours) } + let!(:second_past_meeting) { create(:meeting, project:, start_time: Date.yesterday - 10.hours) } let!(:first_meeting_agenda_item_of_first_past_meeting) do create(:meeting_agenda_item, meeting: first_past_meeting, work_package:, notes: "A very important note in first meeting!") @@ -347,15 +347,15 @@ RSpec.describe "Open the Meetings tab", end context "when open, upcoming meetings are visible for the user" do - shared_let(:past_meeting) { create(:structured_meeting, project:, start_time: Date.yesterday - 10.hours) } - shared_let(:first_upcoming_meeting) { create(:structured_meeting, project:) } - shared_let(:second_upcoming_meeting) { create(:structured_meeting, project:) } - shared_let(:closed_upcoming_meeting) { create(:structured_meeting, project:, state: :closed) } + shared_let(:past_meeting) { create(:meeting, project:, start_time: Date.yesterday - 10.hours) } + shared_let(:first_upcoming_meeting) { create(:meeting, project:) } + shared_let(:second_upcoming_meeting) { create(:meeting, project:) } + shared_let(:closed_upcoming_meeting) { create(:meeting, project:, state: :closed) } shared_let(:ongoing_meeting) do - create(:structured_meeting, title: "Ongoing", project:, start_time: 1.hour.ago, duration: 4.0) + create(:meeting, title: "Ongoing", project:, start_time: 1.hour.ago, duration: 4.0) end - let(:meeting_page) { Pages::StructuredMeeting::Show.new(first_upcoming_meeting) } + let(:meeting_page) { Pages::Meetings::Show.new(first_upcoming_meeting) } it "enables the user to add the work package to multiple open, upcoming meetings" do work_package_page.visit! diff --git a/modules/meeting/spec/mailers/meeting_mailer_spec.rb b/modules/meeting/spec/mailers/meeting_mailer_spec.rb index 7a5c058144b..e17ea369ceb 100644 --- a/modules/meeting/spec/mailers/meeting_mailer_spec.rb +++ b/modules/meeting/spec/mailers/meeting_mailer_spec.rb @@ -45,9 +45,6 @@ RSpec.describe MeetingMailer do author:, project:) end - let(:meeting_agenda) do - create(:meeting_agenda, meeting:) - end let(:tokyo_offset) { "UTC#{ActiveSupport::TimeZone['Asia/Tokyo'].now.formatted_offset}" } let(:berlin_offset) { "UTC#{ActiveSupport::TimeZone['Europe/Berlin'].now.formatted_offset}" } diff --git a/modules/meeting/spec/models/meeting_acts_as_journalized_spec.rb b/modules/meeting/spec/models/meeting_acts_as_journalized_spec.rb index 6f834a756e5..5d57eda6008 100644 --- a/modules/meeting/spec/models/meeting_acts_as_journalized_spec.rb +++ b/modules/meeting/spec/models/meeting_acts_as_journalized_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -35,7 +36,7 @@ RSpec.describe Meeting do let!(:meeting) do User.execute_as current_user do - create(:structured_meeting, author: user) + create(:meeting, author: user) end end diff --git a/modules/meeting/spec/models/meeting_agenda_item_spec.rb b/modules/meeting/spec/models/meeting_agenda_item_spec.rb index 00e6651aebc..78183c4cfeb 100644 --- a/modules/meeting/spec/models/meeting_agenda_item_spec.rb +++ b/modules/meeting/spec/models/meeting_agenda_item_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -31,7 +32,7 @@ require_relative "../spec_helper" RSpec.describe MeetingAgendaItem do let(:meeting_attributes) { {} } - let(:meeting) { build_stubbed(:structured_meeting, **meeting_attributes) } + let(:meeting) { build_stubbed(:meeting, **meeting_attributes) } let(:attributes) { {} } let(:meeting_agenda_item) { described_class.new(meeting:, **attributes) } diff --git a/modules/meeting/spec/models/meeting_section_spec.rb b/modules/meeting/spec/models/meeting_section_spec.rb index e34aaddd831..5c3cea18ee0 100644 --- a/modules/meeting/spec/models/meeting_section_spec.rb +++ b/modules/meeting/spec/models/meeting_section_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -31,7 +32,7 @@ require_relative "../spec_helper" RSpec.describe MeetingSection do let(:meeting_attributes) { {} } - let(:meeting) { build_stubbed(:structured_meeting, **meeting_attributes) } + let(:meeting) { build_stubbed(:meeting, **meeting_attributes) } let(:attributes) { {} } let(:meeting_section) { described_class.new(meeting:, **attributes) } @@ -73,7 +74,7 @@ RSpec.describe MeetingSection do describe "#agenda_items_sum_duration_in_minutes" do subject { meeting_section.agenda_items_sum_duration_in_minutes } - let(:meeting) { create(:structured_meeting, **meeting_attributes) } + let(:meeting) { create(:meeting, **meeting_attributes) } let(:meeting_section) { create(:meeting_section, meeting:) } context "when there are no agenda items" do diff --git a/modules/meeting/spec/models/meeting_spec.rb b/modules/meeting/spec/models/meeting_spec.rb index 8e45a28c4eb..c2ebc8d7b30 100644 --- a/modules/meeting/spec/models/meeting_spec.rb +++ b/modules/meeting/spec/models/meeting_spec.rb @@ -32,12 +32,9 @@ require_relative "../spec_helper" RSpec.describe Meeting do shared_let (:user1) { create(:user) } shared_let (:user2) { create(:user) } + let(:project) { create(:project, members: project_members) } let(:meeting) { create(:meeting, :author_participates, project:, author: user1) } - let(:agenda) do - meeting.create_agenda text: "Meeting Agenda text" - meeting.reload_agenda # avoiding stale object errors - end let(:project_members) { {} } let(:role) { create(:project_role, permissions: [:view_meetings]) } @@ -152,22 +149,6 @@ RSpec.describe Meeting do it { expect(meeting.watchers.collect(&:user)).to contain_exactly(user1, user2) } end - describe "#close_agenda_and_copy_to_minutes" do - before do - agenda # creating it - - meeting.close_agenda_and_copy_to_minutes! - end - - it "creates a meeting with the agenda's text" do - expect(meeting.minutes.text).to eq(meeting.agenda.text) - end - - it "closes the agenda" do - expect(meeting.agenda).to be_locked - end - end - describe "Timezones" do shared_examples "uses that zone" do |zone| it do @@ -203,10 +184,6 @@ RSpec.describe Meeting do it "uses the :view_meetings permission" do expect(described_class.acts_as_watchable_permission).to eq(:view_meetings) end - - it "uses the :view_meetings permission in STI classes" do - expect(StructuredMeeting.acts_as_watchable_permission).to eq(:view_meetings) - end end describe "duration" do diff --git a/modules/meeting/spec/requests/api/v3/attachments/meeting_agenda_spec.rb b/modules/meeting/spec/requests/api/v3/attachments/meeting_agenda_spec.rb deleted file mode 100644 index 32cce374d07..00000000000 --- a/modules/meeting/spec/requests/api/v3/attachments/meeting_agenda_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" -require "requests/api/v3/attachments/attachment_resource_shared_examples" - -RSpec.describe "meeting agenda attachments" do - it_behaves_like "an APIv3 attachment resource" do - let(:attachment_type) { :meeting_content } - - let(:create_permission) { :create_meetings } - let(:read_permission) { :view_meetings } - let(:update_permission) { :edit_meetings } - - let(:meeting_content) { create(:meeting_agenda, meeting:) } - let(:meeting) { create(:meeting, project:) } - end -end diff --git a/modules/meeting/spec/requests/meetings_index_spec.rb b/modules/meeting/spec/requests/meetings_index_spec.rb index ed4786a3072..cfedbd2032a 100644 --- a/modules/meeting/spec/requests/meetings_index_spec.rb +++ b/modules/meeting/spec/requests/meetings_index_spec.rb @@ -37,7 +37,7 @@ RSpec.describe "Meeting index", shared_let(:user) { create(:user, member_with_permissions: { project => %i[view_meetings] }) } shared_let(:past) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "an earlier meeting", start_time: DateTime.parse("2025-01-29T06:00:00Z"), @@ -46,7 +46,7 @@ RSpec.describe "Meeting index", end shared_let(:today) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "meeting starting soon", start_time: DateTime.parse("2025-01-29T10:00:00Z"), @@ -55,7 +55,7 @@ RSpec.describe "Meeting index", end shared_let(:tomorrow) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "meeting starting tomorrow", start_time: DateTime.parse("2025-01-30T10:00:00Z"), @@ -64,7 +64,7 @@ RSpec.describe "Meeting index", end shared_let(:saturday) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "weekend meeting on saturday", start_time: DateTime.parse("2025-02-01T10:00:00Z"), @@ -73,7 +73,7 @@ RSpec.describe "Meeting index", end shared_let(:sunday) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "weekend meeting on sunday", start_time: DateTime.parse("2025-02-02T10:00:00Z"), @@ -82,7 +82,7 @@ RSpec.describe "Meeting index", end shared_let(:next_monday) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "meeting on next monday", start_time: DateTime.parse("2025-02-03T10:00:00Z"), @@ -91,7 +91,7 @@ RSpec.describe "Meeting index", end shared_let(:next_friday) do - create(:structured_meeting, + create(:meeting, :author_participates, title: "meeting on next friday", start_time: DateTime.parse("2025-02-07T10:00:00Z"), diff --git a/modules/meeting/spec/requests/meetings_spec.rb b/modules/meeting/spec/requests/meetings_spec.rb index 1fd3a5a4a15..f748a749818 100644 --- a/modules/meeting/spec/requests/meetings_spec.rb +++ b/modules/meeting/spec/requests/meetings_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -34,7 +35,7 @@ RSpec.describe "Meeting requests", type: :rails_request do shared_let(:project) { create(:project, enabled_module_names: %i[meetings]) } shared_let(:user) { create(:user, member_with_permissions: { project => %i[view_meetings create_meetings edit_meetings] }) } - shared_let(:meeting) { create(:structured_meeting, project:, author: user) } + shared_let(:meeting) { create(:meeting, project:, author: user) } before do meeting.participants.delete_all @@ -51,55 +52,6 @@ RSpec.describe "Meeting requests", end end - describe "update with a new particpant" do - let(:other_user) { create(:user, member_with_permissions: { project => %i[view_meetings] }) } - let(:params) do - { - send_notifications:, - title: "Updated meeting", - meeting: { - participants_attributes: [ - { user_id: user.id, invited: true }, - { user_id: other_user.id, invited: true } - ] - } - - } - end - - subject do - meeting.reload - end - - context "with send_notifications" do - let(:send_notifications) { "1" } - - it "sends an invitation mail to the invited users" do - patch(project_meeting_path(project, meeting), params:) - - expect(subject.participants.count).to eq(2) - - perform_enqueued_jobs - - expect(ActionMailer::Base.deliveries.size).to eq(2) - end - end - - context "with send_notifications false" do - let(:send_notifications) { "0" } - - it "sends an invitation mail to the invited users" do - patch(project_meeting_path(project, meeting), params:) - - expect(subject.participants.count).to eq(2) - - perform_enqueued_jobs - - expect(ActionMailer::Base.deliveries.size).to eq(0) - end - end - end - describe "copy" do let(:base_params) do { @@ -107,7 +59,7 @@ RSpec.describe "Meeting requests", project_id: project.id, meeting: { title: "Copied meeting", - type: "StructuredMeeting" + type: "Meeting" } } end diff --git a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_end_series_spec.rb b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_end_series_spec.rb index 206f0d2afe5..cc0775a0233 100644 --- a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_end_series_spec.rb +++ b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_end_series_spec.rb @@ -62,7 +62,7 @@ RSpec.describe "Recurring meetings complete template", end context "when past occurrence is already created" do - let!(:meeting) { create(:structured_meeting, recurring_meeting:, start_time: recurring_meeting.start_time) } + let!(:meeting) { create(:meeting, recurring_meeting:, start_time: recurring_meeting.start_time) } let!(:schedule) do create :scheduled_meeting, meeting:, @@ -81,7 +81,7 @@ RSpec.describe "Recurring meetings complete template", end context "when start_time < current time" do - let!(:meeting) { create(:structured_meeting, recurring_meeting:, start_time: recurring_meeting.start_time) } + let!(:meeting) { create(:meeting, recurring_meeting:, start_time: recurring_meeting.start_time) } let!(:schedule) do create :scheduled_meeting, meeting:, diff --git a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb index 10c351e0acb..6e7fc910753 100644 --- a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb +++ b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_show_spec.rb @@ -94,7 +94,7 @@ RSpec.describe "Recurring meetings show", end describe "past quick filter" do - let!(:past_instance) { create(:structured_meeting, recurring_meeting:, start_time: 1.day.ago + 10.hours) } + let!(:past_instance) { create(:meeting, recurring_meeting:, start_time: 1.day.ago + 10.hours) } let!(:past_schedule) do create :scheduled_meeting, meeting: past_instance, @@ -131,7 +131,7 @@ RSpec.describe "Recurring meetings show", describe "upcoming tab" do let!(:upcoming_open_meeting) do - create(:structured_meeting, recurring_meeting:, start_time: Time.zone.today + 1.day + 10.hours, state: :open) + create(:meeting, recurring_meeting:, start_time: Time.zone.today + 1.day + 10.hours, state: :open) end let!(:open_meeting) do create :scheduled_meeting, @@ -179,7 +179,7 @@ RSpec.describe "Recurring meetings show", end let!(:ongoing_meeting) do - create(:structured_meeting, recurring_meeting:, start_time: Time.zone.today + 10.hours, state: :open) + create(:meeting, recurring_meeting:, start_time: Time.zone.today + 10.hours, state: :open) end let!(:ongoing_schedule) do create :scheduled_meeting, @@ -210,7 +210,7 @@ RSpec.describe "Recurring meetings show", describe "upcoming quick filter" do context "with a rescheduled meeting" do let!(:rescheduled_instance) do - create :structured_meeting, + create :meeting, recurring_meeting:, start_time: Time.zone.today + 2.days + 10.hours end @@ -242,7 +242,7 @@ RSpec.describe "Recurring meetings show", iterations: 2 end let!(:first_instance) do - create :structured_meeting, + create :meeting, recurring_meeting:, start_time: Time.zone.today + 2.days + 10.hours, state: :open @@ -254,7 +254,7 @@ RSpec.describe "Recurring meetings show", start_time: Time.zone.today + 2.days + 10.hours end let!(:second_instance) do - create :structured_meeting, + create :meeting, recurring_meeting:, start_time: Time.zone.today + 3.days + 10.hours, state: :open @@ -323,7 +323,7 @@ RSpec.describe "Recurring meetings show", iterations: 5 end let!(:ongoing_instance) do - create :structured_meeting, + create :meeting, recurring_meeting:, start_time: Time.zone.today + 10.hours - 10.minutes end diff --git a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_template_completed_spec.rb b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_template_completed_spec.rb index f01a2603d7a..2f3794e7ed5 100644 --- a/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_template_completed_spec.rb +++ b/modules/meeting/spec/requests/recurring_meetings/recurring_meetings_template_completed_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -81,7 +82,7 @@ RSpec.describe "Recurring meetings complete template", end context "when first occurrence is already created" do - let!(:meeting) { create(:structured_meeting, recurring_meeting:, start_time: recurring_meeting.start_time) } + let!(:meeting) { create(:meeting, recurring_meeting:, start_time: recurring_meeting.start_time) } let!(:schedule) do create :scheduled_meeting, meeting:, diff --git a/modules/meeting/spec/routing/previews_routing_spec.rb b/modules/meeting/spec/routing/previews_routing_spec.rb deleted file mode 100644 index 3c8fedad40e..00000000000 --- a/modules/meeting/spec/routing/previews_routing_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -RSpec.describe "preview" do - it "connects POST /meetings/:meeting_id/agenda/preview to meeting_agendas#preview" do - expect(post("/meetings/1/agenda/preview")).to route_to(controller: "meeting_agendas", - meeting_id: "1", - action: "preview") - end - - it "connects POST /meetings/:meeting_id/agenda/preview to meeting_minutes#preview" do - expect(post("/meetings/1/minutes/preview")).to route_to(controller: "meeting_minutes", - meeting_id: "1", - action: "preview") - end -end diff --git a/modules/meeting/spec/seeders/demo_data/meeting_agenda_items_seeder_spec.rb b/modules/meeting/spec/seeders/demo_data/meeting_agenda_items_seeder_spec.rb index 85d788aebda..b6f67607449 100644 --- a/modules/meeting/spec/seeders/demo_data/meeting_agenda_items_seeder_spec.rb +++ b/modules/meeting/spec/seeders/demo_data/meeting_agenda_items_seeder_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -34,7 +35,7 @@ RSpec.describe Meetings::DemoData::MeetingAgendaItemsSeeder do shared_let(:alice) { create(:user, firstname: "Alice") } shared_let(:bob) { create(:user, firstname: "Bob") } - shared_let(:meeting) { create(:structured_meeting, title: "Weekly meeting") } + shared_let(:meeting) { create(:meeting, title: "Weekly meeting") } shared_let(:work_package) { create(:work_package, subject: "Some important task") } subject(:seeder) { described_class.new("_project", seed_data) } diff --git a/modules/meeting/spec/services/meeting_contents/update_service_spec.rb b/modules/meeting/spec/services/meeting_contents/update_service_spec.rb deleted file mode 100644 index 296e8a4a960..00000000000 --- a/modules/meeting/spec/services/meeting_contents/update_service_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" -require "services/base_services/behaves_like_update_service" - -RSpec.describe MeetingContents::UpdateService, type: :model do - it_behaves_like "BaseServices update service" do - let(:factory) { :meeting_agenda } - end -end diff --git a/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb b/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb index 00bddb71226..d71b3a9d7f5 100644 --- a/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb +++ b/modules/meeting/spec/services/meetings/copy_service_integration_spec.rb @@ -35,7 +35,7 @@ RSpec.describe Meetings::CopyService, "integration", type: :model do shared_let(:user) do create(:user, member_with_permissions: { project => %i(view_meetings create_meetings) }) end - shared_let(:meeting) { create(:structured_meeting, project:, start_time: Time.parse("2013-03-27T15:35:00Z")) } + shared_let(:meeting) { create(:meeting, project:, start_time: Time.parse("2013-03-27T15:35:00Z")) } let(:instance) { described_class.new(model: meeting, user:) } let(:attributes) { {} } diff --git a/modules/meeting/spec/services/meetings/update_service_integration_spec.rb b/modules/meeting/spec/services/meetings/update_service_integration_spec.rb index 2197c9f486c..23d13048196 100644 --- a/modules/meeting/spec/services/meetings/update_service_integration_spec.rb +++ b/modules/meeting/spec/services/meetings/update_service_integration_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Meetings::UpdateService, "integration", type: :model do context "when meeting is in a series and scheduled to the future" do shared_let(:recurring_meeting, refind: true) { create(:recurring_meeting, project:, frequency: "daily") } shared_let(:meeting, refind: true) do - create(:structured_meeting, + create(:meeting, recurring_meeting:, project:, start_time: Time.zone.today + 2.days + 10.hours) @@ -140,7 +140,7 @@ RSpec.describe Meetings::UpdateService, "integration", type: :model do context "when previous schedule exists tomorrow at 10:00" do shared_let(:previous_meeting) do - create(:structured_meeting, recurring_meeting:, project:, start_time: Time.zone.tomorrow + 10.hours) + create(:meeting, recurring_meeting:, project:, start_time: Time.zone.tomorrow + 10.hours) end shared_let(:previous_schedule) do create(:scheduled_meeting, diff --git a/modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb b/modules/meeting/spec/services/principals/delete_job_integration_spec.rb similarity index 63% rename from modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb rename to modules/meeting/spec/services/principals/delete_job_integration_spec.rb index 0bfbaaa38fd..83e2911de42 100644 --- a/modules/meeting/spec/contracts/meeting_contents/update_contract_spec.rb +++ b/modules/meeting/spec/services/principals/delete_job_integration_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -28,29 +29,28 @@ #++ require "spec_helper" -require "contracts/shared/model_contract_shared_context" -RSpec.describe MeetingContents::UpdateContract do - include_context "ModelContract shared context" - let(:agenda) { build_stubbed(:meeting_agenda) } - let(:current_user) { build_stubbed(:user) } - let(:contract) { described_class.new(agenda, current_user) } +RSpec.describe Principals::DeleteJob, "Meetings", type: :model do + subject(:job) { described_class.perform_now(principal) } - context "when not editable" do - before do - allow(agenda).to receive(:editable?).and_return false - end - - it_behaves_like "contract is invalid", base: :error_readonly + shared_let(:deleted_user) do + create(:deleted_user) + end + let(:principal) do + create(:user) end - context "when editable" do - before do - allow(agenda).to receive(:editable?).and_return true + context "with a meeting" do + let!(:meeting) { create(:meeting, author: principal) } + let!(:meeting_agenda_item) { create(:meeting_agenda_item, presenter: principal) } + let!(:meeting_outcome) { create(:meeting_outcome, meeting_agenda_item:, author: principal) } + + it "rewrites the references" do + job + + expect(meeting.reload.author).to eq deleted_user + expect(meeting_agenda_item.reload.presenter).to eq deleted_user + expect(meeting_outcome.reload.author).to eq deleted_user end - - it_behaves_like "contract is valid" end - - include_examples "contract reuses the model errors" end diff --git a/modules/meeting/spec/services/principals/replace_references_service_call_integration_spec.rb b/modules/meeting/spec/services/principals/replace_references_service_call_integration_spec.rb index a3156bf03e5..456596b5dbd 100644 --- a/modules/meeting/spec/services/principals/replace_references_service_call_integration_spec.rb +++ b/modules/meeting/spec/services/principals/replace_references_service_call_integration_spec.rb @@ -53,6 +53,12 @@ RSpec.describe Principals::ReplaceReferencesService, "#call", type: :model do :presenter_id end + context "with MeetingOutcome" do + it_behaves_like "rewritten record", + :meeting_outcome, + :author_id + end + context "with Journal::MeetingAgendaItemJournal" do it_behaves_like "rewritten record", :journal_meeting_agenda_item_journal, diff --git a/modules/meeting/spec/services/recurring_meetings/create_service_integration_spec.rb b/modules/meeting/spec/services/recurring_meetings/create_service_integration_spec.rb index 714c8aea39a..d9707c8db01 100644 --- a/modules/meeting/spec/services/recurring_meetings/create_service_integration_spec.rb +++ b/modules/meeting/spec/services/recurring_meetings/create_service_integration_spec.rb @@ -47,7 +47,7 @@ RSpec.describe RecurringMeetings::CreateService, "integration", type: :model do expect(service_result).to be_success expect(series).to be_persisted - expect(series.template).to be_a(StructuredMeeting) + expect(series.template).to be_a(Meeting) expect(series.template).to be_template expect(series.meetings.count).to eq(1) @@ -84,10 +84,10 @@ RSpec.describe RecurringMeetings::CreateService, "integration", type: :model do it_behaves_like "creates the series" context "when the template cannot be saved" do - let(:template) { StructuredMeeting.new } + let(:template) { Meeting.new } before do - allow(StructuredMeeting).to receive(:new).and_return(template) + allow(Meeting).to receive(:new).and_return(template) allow(template).to receive(:save).and_return(false) end diff --git a/modules/meeting/spec/support/pages/structured_meeting/history.rb b/modules/meeting/spec/support/pages/meetings/history.rb similarity index 98% rename from modules/meeting/spec/support/pages/structured_meeting/history.rb rename to modules/meeting/spec/support/pages/meetings/history.rb index c47ceff18c6..01aafdb3d27 100644 --- a/modules/meeting/spec/support/pages/structured_meeting/history.rb +++ b/modules/meeting/spec/support/pages/meetings/history.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module Pages::StructuredMeeting +module Pages::Meeting class History < ::Pages::Page def initialize(meeting) super() diff --git a/modules/meeting/spec/support/pages/meetings/index.rb b/modules/meeting/spec/support/pages/meetings/index.rb index ecd4830e110..e737a97258c 100644 --- a/modules/meeting/spec/support/pages/meetings/index.rb +++ b/modules/meeting/spec/support/pages/meetings/index.rb @@ -28,8 +28,6 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require_relative "new" - module Pages::Meetings class Index < Pages::Page include Components::Common::Filters @@ -43,13 +41,6 @@ module Pages::Meetings self.project = project end - def click_create_new - click_on("add-meeting-button") - click_on("Classic") - - New.new(project) - end - def set_title(text) fill_in "Title", with: text end @@ -88,16 +79,7 @@ module Pages::Meetings def click_create click_on "Create meeting" - wait_for_network_idle - - meeting = Meeting.last - - if meeting - Pages::Meetings::Show.new(meeting) - else - self - end end def expect_no_main_menu @@ -115,7 +97,7 @@ module Pages::Meetings def expect_create_new_types click_on("add-meeting-button") - expect(page).to have_link("Classic") + expect(page).to have_link("Recurring") expect(page).to have_link("One-time") end diff --git a/modules/meeting/spec/support/pages/structured_meeting/mobile/show.rb b/modules/meeting/spec/support/pages/meetings/mobile/show.rb similarity index 95% rename from modules/meeting/spec/support/pages/structured_meeting/mobile/show.rb rename to modules/meeting/spec/support/pages/meetings/mobile/show.rb index a149a2d0a1a..0b81b0f9323 100644 --- a/modules/meeting/spec/support/pages/structured_meeting/mobile/show.rb +++ b/modules/meeting/spec/support/pages/meetings/mobile/show.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -29,8 +30,8 @@ require_relative "../show" -module Pages::StructuredMeeting::Mobile - class Show < ::Pages::StructuredMeeting::Show +module Pages::Meetings::Mobile + class Show < ::Pages::Meetings::Show def expect_participants(count: 1) within(meeting_details_container) do expect(page).to have_text(Meeting.human_attribute_name(:participant, count:)) diff --git a/modules/meeting/spec/support/pages/meetings/new.rb b/modules/meeting/spec/support/pages/meetings/new.rb deleted file mode 100644 index fed8cb47804..00000000000 --- a/modules/meeting/spec/support/pages/meetings/new.rb +++ /dev/null @@ -1,98 +0,0 @@ -# frozen_string_literal: true -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require_relative "base" -require_relative "show" - -module Pages::Meetings - class New < Base - include Components::Autocompleter::NgSelectAutocompleteHelpers - - def expect_no_main_menu - expect(page).to have_no_css "#main-menu" - end - - def click_create - click_on "Create" - - wait_for_network_idle - - meeting = Meeting.last - - if meeting - Pages::Meetings::Show.new(meeting) - else - self - end - end - - def set_type(type) - choose type, match: :first - end - - def set_title(text) - fill_in "Title", with: text - end - - def expect_project_dropdown - find "[data-test-selector='project_id']" - end - - def set_project(project) - select_autocomplete find("[data-test-selector='project_id']"), - query: project.name, - results_selector: "body" - end - - def set_start_date(date) - if using_cuprite? - fill_in "Start date", with: date - else - fill_in "Start date", with: date, fill_options: { clear: :backspace } - end - end - - def set_start_time(time) - input = page.find_by_id("meeting-form-start-time") - page.execute_script("arguments[0].value = arguments[1]", input.native, time) - end - - def set_duration(duration) - fill_in "Duration", with: duration - end - - def invite(user) - check "#{user.name} invited" - end - - def path - polymorphic_path([:new, project, :meeting]) - end - end -end diff --git a/modules/meeting/spec/support/pages/meetings/show.rb b/modules/meeting/spec/support/pages/meetings/show.rb index 1c50f32f3a8..3e0239e2ac5 100644 --- a/modules/meeting/spec/support/pages/meetings/show.rb +++ b/modules/meeting/spec/support/pages/meetings/show.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -31,10 +32,13 @@ require_relative "base" module Pages::Meetings class Show < Base + include ::Components::Autocompleter::NgSelectAutocompleteHelpers attr_accessor :meeting def initialize(meeting) self.meeting = meeting + + super(meeting.project) end def expect_no_invited @@ -83,18 +87,359 @@ module Pages::Meetings end end - def meeting_details_container - find(".meeting.details") - end - - def click_edit - within ".meeting--main-toolbar .toolbar-items" do - click_on "Edit" - end - end - def path project_meeting_path(meeting.project, meeting) end + + def expect_empty + expect(page).to have_no_css('[id^="meeting-agenda-items-item-component"]') + end + + def trigger_dropdown_menu_item(name) + click_link_or_button "op-meetings-header-action-trigger" + click_link_or_button name + end + + def trigger_change_poll + script = <<~JS + var target = document.querySelector('#content-wrapper'); + var controller = window.Stimulus.getControllerForElementAndIdentifier(target, 'poll-for-changes') + controller.triggerTurboStream(); + JS + + page.execute_script(script) + end + + def add_agenda_item(type: MeetingAgendaItem, save: true, &) + page.within("#meeting-agenda-items-new-button-component") do + click_on I18n.t(:button_add) + click_on type.model_name.human + end + + in_agenda_form do + yield + click_on("Save") if save + end + end + + def expect_modal(...) + expect(page).to have_modal(...) + end + + def expect_no_add_form + expect(page).not_to have_test_selector("#meeting-agenda-items-form-component") + end + + def add_agenda_item_to_section(section:, type: MeetingAgendaItem, save: true, &) + select_section_action(section, type.model_name.human) + + within("#meeting-sections-show-component-#{section.id}") do + in_agenda_form do + yield + click_on("Save") if save + end + end + end + + def cancel_edit_form(item) + in_edit_form(item) do + click_on I18n.t(:button_cancel) + expect(page).to have_no_link I18n.t(:button_cancel) + end + end + + def in_edit_form(item, &) + page.within("#meeting-agenda-items-item-component-#{item.id}", &) + end + + def in_agenda_form(&) + page.within("#meeting-agenda-items-form-component", &) + end + + def assert_agenda_order!(*titles) + retry_block do + found = page.all(:test_id, "op-meeting-agenda-title").map(&:text) + raise "Expected order of agenda items #{titles.inspect}, but found #{found.inspect}" if titles != found + end + end + + def remove_agenda_item(item) + accept_confirm(I18n.t("text_are_you_sure")) do + action = item.work_package ? I18n.t(:label_agenda_item_remove) : I18n.t(:button_delete) + select_action(item, action) + end + + title = item.work_package ? item.work_package.subject : item.title + expect_no_agenda_item(title:) + end + + def expect_agenda_item(title:) + expect(page).to have_test_selector("op-meeting-agenda-title", text: title) + end + + def expect_agenda_item_in_section(title:, section:) + within("#meeting-sections-show-component-#{section.id}") do + expect_agenda_item(title:) + end + end + + def expect_agenda_link(item) + if item.is_a?(WorkPackage) + expect(page).to have_css("[id^='meeting-agenda-items-item-component-']", text: item.subject) + else + expect(page).to have_css("#meeting-agenda-items-item-component-#{item.id}", text: item.work_package.subject) + end + end + + def expect_agenda_author(name) + expect(page).to have_test_selector("op-principal", text: name) + end + + def expect_undisclosed_agenda_link(item) + expect(page).to have_css("#meeting-agenda-items-item-component-#{item.id}", + text: I18n.t(:label_agenda_item_undisclosed_wp, id: item.work_package_id)) + end + + def expect_no_agenda_item(title:) + expect(page).not_to have_test_selector("op-meeting-agenda-title", text: title) + end + + def expect_agenda_action_menu(item) + expect(page) + .to have_css("#meeting-agenda-items-item-component-#{item.id} #{test_selector('op-meeting-agenda-actions')}") + end + + def expect_no_agenda_action_menu(item) + expect(page) + .to have_no_css("#meeting-agenda-items-item-component-#{item.id} #{test_selector('op-meeting-agenda-actions')}") + end + + def select_action(item, action) + open_menu(item) do + click_on action + end + end + + def open_menu(item, &) + retry_block do + page.within("#meeting-agenda-items-item-component-#{item.id}") do + page.find_test_selector("op-meeting-agenda-actions").click + end + page.find(".Overlay") + end + + page.within(".Overlay", &) + end + + def select_outcome_action(action) + retry_block do + page.find_test_selector("op-meeting-outcome-actions").click + page.find(".Overlay") + end + + page.within(".Overlay") do + click_on action + end + end + + def expect_no_outcome_actions + expect(page).to have_no_css("op-meeting-outcome-actions") + end + + def expect_no_outcome_action(item) + retry_block do + page.within("#meeting-agenda-items-item-component-#{item.id}") do + page.find_test_selector("op-meeting-agenda-actions").trigger("click") + end + page.find(".Overlay") + end + + page.within(".Overlay") do + expect(page).to have_no_text("Add outcome") + end + end + + def select_section_action(section, action) + retry_block do + click_on_section_menu(section) + page.find(".Overlay") + end + + page.within(".Overlay") do + click_on action + end + end + + def click_on_section_menu(section) + page.within_test_selector("meeting-section-header-container-#{section.id}") do + page.find_test_selector("meeting-section-action-menu").click + end + end + + def in_outcome_component(item, &) + page.within("#meeting-agenda-items-outcomes-base-component-#{item.id}", &) + end + + def add_outcome(item, &) + page.within("#meeting-agenda-items-outcomes-base-component-#{item.id}") do + click_link_or_button "Outcome" + end + expect_outcome_form(item) + page.within("#meeting-agenda-items-outcomes-input-component-#{item.id}", &) + end + + def add_outcome_from_menu(item, &) + select_action item, "Add outcome" + expect_outcome_form(item) + page.within("#meeting-agenda-items-outcomes-input-component-#{item.id}", &) + end + + def expect_outcome_form(item) + expect(page) + .to have_css("#meeting-agenda-items-outcomes-input-component-#{item.id}") + end + + def expect_outcome(text) + expect(page).to have_css("#meeting-agenda-items-outcomes-show-notes-component", text:) + end + + def expect_no_outcome(text) + expect(page).to have_no_css("#meeting-agenda-items-outcomes-show-notes-component", text:) + end + + def expect_no_outcome_button + expect(page).to have_no_css("op-meeting-outcome--button") + end + + def edit_agenda_item(item, &) + select_action item, "Edit" + expect_item_edit_form(item) + page.within("#meeting-agenda-items-form-component-#{item.id}", &) + end + + def expect_item_edit_form(item, visible: true) + expect(page) + .to have_conditional_selector( + visible, + "#meeting-agenda-items-form-component-#{item.id}" + ) + end + + def expect_item_edit_title(item, value) + page.within("#meeting-agenda-items-form-component-#{item.id}") do + find_field("Title", with: value) + end + end + + def expect_item_edit_field_error(item, text) + page.within("#meeting-agenda-items-form-component-#{item.id}") do + expect(page).to have_css(".FormControl-inlineValidation", text:) + end + end + + def clear_item_edit_work_package_title + ng_select_clear page.find(".op-meeting-agenda-item-form--title") + expect(page).to have_css(".ng-input ", value: nil) + end + + def open_participant_form + page.find_test_selector("manage-participants-button").click + expect_modal("Participants") + end + + def in_participant_form(&) + page.within_modal("Participants", &) + end + + def expect_participant(participant, invited: false, attended: false, editable: true) + expect(page).to have_text(participant.name) + expect(page).to have_field(id: "checkbox_invited_#{participant.id}", checked: invited, disabled: !editable) + expect(page).to have_field(id: "checkbox_attended_#{participant.id}", checked: attended, disabled: !editable) + end + + def expect_participant_invited(participant, invited: true) + expect(page).to have_text(participant.name) + expect(page).to have_field(id: "checkbox_invited_#{participant.id}", checked: invited) + end + + def invite_participant(participant) + id = "checkbox_invited_#{participant.id}" + retry_block do + check(id:) + raise "Expected #{participant.id} to be invited now" unless page.has_checked_field?(id:) + end + end + + def expect_available_participants(count:) + expect(page).to have_link(class: "op-principal--name", count:) + end + + def close_meeting + retry_block do + click_on("Open") + page.find(".Overlay") + end + + page.within(".Overlay") do + click_on("Closed") + end + expect(page).to have_link("Reopen meeting") + end + + def reopen_meeting + click_on("Reopen meeting") + expect(page).to have_link("Start meeting") + end + + def close_dialog + click_on(class: "Overlay-closeButton") + end + + def meeting_details_container + find_by_id("meetings-side-panel-details-component") + end + + def in_latest_section_form(&) + page.within(all(".op-meeting-section-container").last, &) + end + + def add_section(&) + retry_block do + page.within("#meeting-agenda-items-new-button-component") do + click_on I18n.t(:button_add) + click_on "Section" + # wait for the disabled button, indicating the turbo streams are applied + expect(page).to have_css("#meeting-agenda-items-new-button-component button[disabled='disabled']") + end + end + + in_latest_section_form(&) + end + + def expect_section(title:) + expect(page).to have_css(".op-meeting-section-container", text: title) + end + + def expect_no_section(title:) + expect(page).to have_no_css(".op-meeting-section-container", text: title) + end + + def expect_section_duration(section:, duration_text:) + page.within_test_selector("meeting-section-header-container-#{section.id}") do + expect(page).to have_text(duration_text) + end + end + + def edit_section(section, &) + select_section_action(section, "Edit") + + page.within_test_selector("meeting-section-header-container-#{section.id}", &) + end + + def remove_section(section) + accept_confirm do + select_section_action(section, "Delete") + end + end end end diff --git a/modules/meeting/spec/support/pages/structured_meeting/show.rb b/modules/meeting/spec/support/pages/structured_meeting/show.rb deleted file mode 100644 index dbbbe0ef758..00000000000 --- a/modules/meeting/spec/support/pages/structured_meeting/show.rb +++ /dev/null @@ -1,388 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require_relative "../meetings/show" - -module Pages::StructuredMeeting - class Show < ::Pages::Meetings::Show - include ::Components::Autocompleter::NgSelectAutocompleteHelpers - - def expect_empty - expect(page).to have_no_css('[id^="meeting-agenda-items-item-component"]') - end - - def trigger_dropdown_menu_item(name) - click_link_or_button "op-meetings-header-action-trigger" - click_link_or_button name - end - - def trigger_change_poll - script = <<~JS - var target = document.querySelector('#content-wrapper'); - var controller = window.Stimulus.getControllerForElementAndIdentifier(target, 'poll-for-changes') - controller.triggerTurboStream(); - JS - - page.execute_script(script) - end - - def add_agenda_item(type: MeetingAgendaItem, save: true, &) - page.within("#meeting-agenda-items-new-button-component") do - click_on I18n.t(:button_add) - click_on type.model_name.human - end - - in_agenda_form do - yield - click_on("Save") if save - end - end - - def expect_modal(...) - expect(page).to have_modal(...) - end - - def expect_no_add_form - expect(page).not_to have_test_selector("#meeting-agenda-items-form-component") - end - - def add_agenda_item_to_section(section:, type: MeetingAgendaItem, save: true, &) - select_section_action(section, type.model_name.human) - - within("#meeting-sections-show-component-#{section.id}") do - in_agenda_form do - yield - click_on("Save") if save - end - end - end - - def cancel_edit_form(item) - in_edit_form(item) do - click_on I18n.t(:button_cancel) - expect(page).to have_no_link I18n.t(:button_cancel) - end - end - - def in_edit_form(item, &) - page.within("#meeting-agenda-items-item-component-#{item.id}", &) - end - - def in_agenda_form(&) - page.within("#meeting-agenda-items-form-component", &) - end - - def assert_agenda_order!(*titles) - retry_block do - found = page.all(:test_id, "op-meeting-agenda-title").map(&:text) - raise "Expected order of agenda items #{titles.inspect}, but found #{found.inspect}" if titles != found - end - end - - def remove_agenda_item(item) - accept_confirm(I18n.t("text_are_you_sure")) do - action = item.work_package ? I18n.t(:label_agenda_item_remove) : I18n.t(:button_delete) - select_action(item, action) - end - - title = item.work_package ? item.work_package.subject : item.title - expect_no_agenda_item(title:) - end - - def expect_agenda_item(title:) - expect(page).to have_test_selector("op-meeting-agenda-title", text: title) - end - - def expect_agenda_item_in_section(title:, section:) - within("#meeting-sections-show-component-#{section.id}") do - expect_agenda_item(title:) - end - end - - def expect_agenda_link(item) - if item.is_a?(WorkPackage) - expect(page).to have_css("[id^='meeting-agenda-items-item-component-']", text: item.subject) - else - expect(page).to have_css("#meeting-agenda-items-item-component-#{item.id}", text: item.work_package.subject) - end - end - - def expect_agenda_author(name) - expect(page).to have_test_selector("op-principal", text: name) - end - - def expect_undisclosed_agenda_link(item) - expect(page).to have_css("#meeting-agenda-items-item-component-#{item.id}", - text: I18n.t(:label_agenda_item_undisclosed_wp, id: item.work_package_id)) - end - - def expect_no_agenda_item(title:) - expect(page).not_to have_test_selector("op-meeting-agenda-title", text: title) - end - - def expect_agenda_action_menu(item) - expect(page) - .to have_css("#meeting-agenda-items-item-component-#{item.id} #{test_selector('op-meeting-agenda-actions')}") - end - - def expect_no_agenda_action_menu(item) - expect(page) - .to have_no_css("#meeting-agenda-items-item-component-#{item.id} #{test_selector('op-meeting-agenda-actions')}") - end - - def select_action(item, action) - open_menu(item) do - click_on action - end - end - - def open_menu(item, &) - retry_block do - page.within("#meeting-agenda-items-item-component-#{item.id}") do - page.find_test_selector("op-meeting-agenda-actions").click - end - page.find(".Overlay") - end - - page.within(".Overlay", &) - end - - def select_outcome_action(action) - retry_block do - page.find_test_selector("op-meeting-outcome-actions").click - page.find(".Overlay") - end - - page.within(".Overlay") do - click_on action - end - end - - def expect_no_outcome_actions - expect(page).to have_no_css("op-meeting-outcome-actions") - end - - def expect_no_outcome_action(item) - retry_block do - page.within("#meeting-agenda-items-item-component-#{item.id}") do - page.find_test_selector("op-meeting-agenda-actions").trigger("click") - end - page.find(".Overlay") - end - - page.within(".Overlay") do - expect(page).to have_no_text("Add outcome") - end - end - - def select_section_action(section, action) - retry_block do - click_on_section_menu(section) - page.find(".Overlay") - end - - page.within(".Overlay") do - click_on action - end - end - - def click_on_section_menu(section) - page.within_test_selector("meeting-section-header-container-#{section.id}") do - page.find_test_selector("meeting-section-action-menu").click - end - end - - def in_outcome_component(item, &) - page.within("#meeting-agenda-items-outcomes-base-component-#{item.id}", &) - end - - def add_outcome(item, &) - page.within("#meeting-agenda-items-outcomes-base-component-#{item.id}") do - click_link_or_button "Outcome" - end - expect_outcome_form(item) - page.within("#meeting-agenda-items-outcomes-input-component-#{item.id}", &) - end - - def add_outcome_from_menu(item, &) - select_action item, "Add outcome" - expect_outcome_form(item) - page.within("#meeting-agenda-items-outcomes-input-component-#{item.id}", &) - end - - def expect_outcome_form(item) - expect(page) - .to have_css("#meeting-agenda-items-outcomes-input-component-#{item.id}") - end - - def expect_outcome(text) - expect(page).to have_css("#meeting-agenda-items-outcomes-show-notes-component", text:) - end - - def expect_no_outcome(text) - expect(page).to have_no_css("#meeting-agenda-items-outcomes-show-notes-component", text:) - end - - def expect_no_outcome_button - expect(page).to have_no_css("op-meeting-outcome--button") - end - - def edit_agenda_item(item, &) - select_action item, "Edit" - expect_item_edit_form(item) - page.within("#meeting-agenda-items-form-component-#{item.id}", &) - end - - def expect_item_edit_form(item, visible: true) - expect(page) - .to have_conditional_selector( - visible, - "#meeting-agenda-items-form-component-#{item.id}" - ) - end - - def expect_item_edit_title(item, value) - page.within("#meeting-agenda-items-form-component-#{item.id}") do - find_field("Title", with: value) - end - end - - def expect_item_edit_field_error(item, text) - page.within("#meeting-agenda-items-form-component-#{item.id}") do - expect(page).to have_css(".FormControl-inlineValidation", text:) - end - end - - def clear_item_edit_work_package_title - ng_select_clear page.find(".op-meeting-agenda-item-form--title") - expect(page).to have_css(".ng-input ", value: nil) - end - - def open_participant_form - page.find_test_selector("manage-participants-button").click - expect_modal("Participants") - end - - def in_participant_form(&) - page.within_modal("Participants", &) - end - - def expect_participant(participant, invited: false, attended: false, editable: true) - expect(page).to have_text(participant.name) - expect(page).to have_field(id: "checkbox_invited_#{participant.id}", checked: invited, disabled: !editable) - expect(page).to have_field(id: "checkbox_attended_#{participant.id}", checked: attended, disabled: !editable) - end - - def expect_participant_invited(participant, invited: true) - expect(page).to have_text(participant.name) - expect(page).to have_field(id: "checkbox_invited_#{participant.id}", checked: invited) - end - - def invite_participant(participant) - id = "checkbox_invited_#{participant.id}" - retry_block do - check(id:) - raise "Expected #{participant.id} to be invited now" unless page.has_checked_field?(id:) - end - end - - def expect_available_participants(count:) - expect(page).to have_link(class: "op-principal--name", count:) - end - - def close_meeting - retry_block do - click_on("Open") - page.find(".Overlay") - end - - page.within(".Overlay") do - click_on("Closed") - end - expect(page).to have_link("Reopen meeting") - end - - def reopen_meeting - click_on("Reopen meeting") - expect(page).to have_link("Start meeting") - end - - def close_dialog - click_on(class: "Overlay-closeButton") - end - - def meeting_details_container - find_by_id("meetings-side-panel-details-component") - end - - def in_latest_section_form(&) - page.within(all(".op-meeting-section-container").last, &) - end - - def add_section(&) - retry_block do - page.within("#meeting-agenda-items-new-button-component") do - click_on I18n.t(:button_add) - click_on "Section" - # wait for the disabled button, indicating the turbo streams are applied - expect(page).to have_css("#meeting-agenda-items-new-button-component button[disabled='disabled']") - end - end - - in_latest_section_form(&) - end - - def expect_section(title:) - expect(page).to have_css(".op-meeting-section-container", text: title) - end - - def expect_no_section(title:) - expect(page).to have_no_css(".op-meeting-section-container", text: title) - end - - def expect_section_duration(section:, duration_text:) - page.within_test_selector("meeting-section-header-container-#{section.id}") do - expect(page).to have_text(duration_text) - end - end - - def edit_section(section, &) - select_section_action(section, "Edit") - - page.within_test_selector("meeting-section-header-container-#{section.id}", &) - end - - def remove_section(section) - accept_confirm do - select_section_action(section, "Delete") - end - end - end -end diff --git a/modules/meeting/spec/workers/recurring_meetings/init_next_occurrence_job_spec.rb b/modules/meeting/spec/workers/recurring_meetings/init_next_occurrence_job_spec.rb index 698b77c1668..59a08813cae 100644 --- a/modules/meeting/spec/workers/recurring_meetings/init_next_occurrence_job_spec.rb +++ b/modules/meeting/spec/workers/recurring_meetings/init_next_occurrence_job_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -46,7 +47,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do subject { described_class.perform_now(series, scheduled_time) } it "schedules the first occurrence" do - expect { subject }.to change(StructuredMeeting, :count).by(1) + expect { subject }.to change(Meeting, :count).by(1) expect(subject).to be_success created_meeting = subject.result @@ -62,7 +63,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do end it "does not instantiate anything, but schedules the next job" do - expect { subject }.not_to change(StructuredMeeting, :count) + expect { subject }.not_to change(Meeting, :count) expect(subject).to be_nil expect(described_class) @@ -73,7 +74,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do context "when next occurrence is already instantiated" do let!(:instance) do - create(:structured_meeting, + create(:meeting, recurring_meeting: series, start_time: Time.zone.tomorrow + 10.hours) end @@ -88,7 +89,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do let(:next_occurrence) { Time.zone.tomorrow + 1.day + 10.hours } it "does not instantiate anything, but schedules the next job" do - expect { subject }.not_to change(StructuredMeeting, :count) + expect { subject }.not_to change(Meeting, :count) expect(subject).to be_nil expect(described_class) @@ -99,7 +100,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do context "when next occurrence is already instantiated, and moved" do let!(:instance) do - create(:structured_meeting, + create(:meeting, recurring_meeting: series, start_time: Time.zone.tomorrow + 1.day + 10.hours) end @@ -114,7 +115,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do let(:next_occurrence) { Time.zone.tomorrow + 1.day + 10.hours } it "does not instantiate anything, but schedules the next one" do - expect { subject }.not_to change(StructuredMeeting, :count) + expect { subject }.not_to change(Meeting, :count) expect(subject).to be_nil expect(described_class) .to have_been_enqueued.with(series, next_occurrence) @@ -124,7 +125,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do context "when later occurrence is already instantiated" do let!(:instance) do - create(:structured_meeting, + create(:meeting, recurring_meeting: series, start_time: Time.zone.tomorrow + 1.day + 10.hours) end @@ -139,7 +140,7 @@ RSpec.describe RecurringMeetings::InitNextOccurrenceJob, type: :model do let(:next_occurrence) { Time.zone.tomorrow + 1.day + 10.hours } it "schedules the one for tomorrow" do - expect { subject }.to change(StructuredMeeting, :count).by(1) + expect { subject }.to change(Meeting, :count).by(1) expect(subject).to be_success created_meeting = subject.result diff --git a/modules/openid_connect/app/controllers/openid_connect/providers_controller.rb b/modules/openid_connect/app/controllers/openid_connect/providers_controller.rb index a4aadf58f8b..88d6c36c5f8 100644 --- a/modules/openid_connect/app/controllers/openid_connect/providers_controller.rb +++ b/modules/openid_connect/app/controllers/openid_connect/providers_controller.rb @@ -36,7 +36,7 @@ module OpenIDConnect menu_item :plugin_openid_connect before_action :require_admin - before_action :check_ee + before_action :check_ee, except: %i[index] before_action :find_provider, only: %i[edit show update confirm_destroy destroy] before_action :set_edit_state, only: %i[create edit update] @@ -123,16 +123,11 @@ module OpenIDConnect private def check_ee - unless EnterpriseToken.allows_to?(:sso_auth_providers) - render template: "/openid_connect/providers/upsale" - false - end + redirect_to action: :index unless EnterpriseToken.allows_to?(:sso_auth_providers) end def find_provider @provider = OpenIDConnect::Provider.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 end def default_breadcrumb; end diff --git a/modules/openid_connect/app/views/openid_connect/providers/index.html.erb b/modules/openid_connect/app/views/openid_connect/providers/index.html.erb index e912d3269d4..33ae7abd753 100644 --- a/modules/openid_connect/app/views/openid_connect/providers/index.html.erb +++ b/modules/openid_connect/app/views/openid_connect/providers/index.html.erb @@ -21,6 +21,7 @@ ) do |menu| menu.with_show_button( scheme: :primary, + disabled: !EnterpriseToken.allows_to?(:sso_auth_providers), aria: { label: I18n.t("openid_connect.providers.label_add_new") } ) do |button| button.with_leading_visual_icon(icon: :plus) @@ -39,4 +40,5 @@ end %> +<%= render EnterpriseEdition::BannerComponent.new(:sso_auth_providers, i18n_scope: "openid_connect.providers.upsale") %> <%= render ::OpenIDConnect::Providers::TableComponent.new(rows: @providers) %> diff --git a/modules/openid_connect/app/views/openid_connect/providers/upsale.html.erb b/modules/openid_connect/app/views/openid_connect/providers/upsale.html.erb deleted file mode 100644 index 6b6063a75b6..00000000000 --- a/modules/openid_connect/app/views/openid_connect/providers/upsale.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% html_title(t(:label_administration), t("openid_connect.providers.plural")) -%> - -<%= render template: "common/upsale", - locals: { - feature_title: t("openid_connect.providers.plural"), - feature_description: t("openid_connect.providers.upsale.description"), - feature_reference: "enterprise-openid-connect", - feature_image: "enterprise/open-id-providers.jpg" - } %> diff --git a/modules/openid_connect/config/locales/crowdin/af.yml b/modules/openid_connect/config/locales/crowdin/af.yml index b161587c33e..3f8e6e7873c 100644 --- a/modules/openid_connect/config/locales/crowdin/af.yml +++ b/modules/openid_connect/config/locales/crowdin/af.yml @@ -92,6 +92,7 @@ af: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ar.yml b/modules/openid_connect/config/locales/crowdin/ar.yml index 568250486a9..882e3e06d57 100644 --- a/modules/openid_connect/config/locales/crowdin/ar.yml +++ b/modules/openid_connect/config/locales/crowdin/ar.yml @@ -92,6 +92,7 @@ ar: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: إضافة مزود OpenID جديد label_edit: تعديل موفر OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/az.yml b/modules/openid_connect/config/locales/crowdin/az.yml index 5d52883594f..280764a568f 100644 --- a/modules/openid_connect/config/locales/crowdin/az.yml +++ b/modules/openid_connect/config/locales/crowdin/az.yml @@ -92,6 +92,7 @@ az: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/be.yml b/modules/openid_connect/config/locales/crowdin/be.yml index 98a664732bb..4c9196c327a 100644 --- a/modules/openid_connect/config/locales/crowdin/be.yml +++ b/modules/openid_connect/config/locales/crowdin/be.yml @@ -92,6 +92,7 @@ be: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/bg.yml b/modules/openid_connect/config/locales/crowdin/bg.yml index 3c236401f6e..39b8edbd0f4 100644 --- a/modules/openid_connect/config/locales/crowdin/bg.yml +++ b/modules/openid_connect/config/locales/crowdin/bg.yml @@ -92,6 +92,7 @@ bg: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Добавяне на нов OpenID доставчик label_edit: Редактиране на OpenID доставчика %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ca.yml b/modules/openid_connect/config/locales/crowdin/ca.yml index de128f75ca6..d50f6e5185f 100644 --- a/modules/openid_connect/config/locales/crowdin/ca.yml +++ b/modules/openid_connect/config/locales/crowdin/ca.yml @@ -92,6 +92,7 @@ ca: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Afegeix un proveïdor d'OpenID label_edit: Edita el proveïdor d'OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ckb-IR.yml b/modules/openid_connect/config/locales/crowdin/ckb-IR.yml index e052c58f964..1d78f8ebb1f 100644 --- a/modules/openid_connect/config/locales/crowdin/ckb-IR.yml +++ b/modules/openid_connect/config/locales/crowdin/ckb-IR.yml @@ -92,6 +92,7 @@ ckb-IR: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/cs.yml b/modules/openid_connect/config/locales/crowdin/cs.yml index a9117b5b380..84b50a1ab49 100644 --- a/modules/openid_connect/config/locales/crowdin/cs.yml +++ b/modules/openid_connect/config/locales/crowdin/cs.yml @@ -92,6 +92,7 @@ cs: custom: name: Vlastní upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Připojit OpenProject k poskytovateli OpenID připojení identity label_add_new: Přidat nového poskytovatele OpenID label_edit: Upravit poskytovatele OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/da.yml b/modules/openid_connect/config/locales/crowdin/da.yml index 633d28bb36b..6484b3b0036 100644 --- a/modules/openid_connect/config/locales/crowdin/da.yml +++ b/modules/openid_connect/config/locales/crowdin/da.yml @@ -92,6 +92,7 @@ da: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/de.yml b/modules/openid_connect/config/locales/crowdin/de.yml index ce881348b12..0f5c0fd41fc 100644 --- a/modules/openid_connect/config/locales/crowdin/de.yml +++ b/modules/openid_connect/config/locales/crowdin/de.yml @@ -92,6 +92,7 @@ de: custom: name: Benutzerdefiniert upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: OpenProject mit einem OpenID Connect SSO-Anbieter verbinden label_add_new: Neuen OpenID-Anbieter hinzufügen label_edit: OpenID-Provider %{name} bearbeiten diff --git a/modules/openid_connect/config/locales/crowdin/el.yml b/modules/openid_connect/config/locales/crowdin/el.yml index f50ad0aa2e3..2e54c262bd5 100644 --- a/modules/openid_connect/config/locales/crowdin/el.yml +++ b/modules/openid_connect/config/locales/crowdin/el.yml @@ -92,6 +92,7 @@ el: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Προσθήκη νέου παρόχου OpenID label_edit: Επεξεργασία παρόχου OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/eo.yml b/modules/openid_connect/config/locales/crowdin/eo.yml index a74f2d4950c..a10251070ea 100644 --- a/modules/openid_connect/config/locales/crowdin/eo.yml +++ b/modules/openid_connect/config/locales/crowdin/eo.yml @@ -92,6 +92,7 @@ eo: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/es.yml b/modules/openid_connect/config/locales/crowdin/es.yml index a13592e204b..87ffa8f3ffe 100644 --- a/modules/openid_connect/config/locales/crowdin/es.yml +++ b/modules/openid_connect/config/locales/crowdin/es.yml @@ -92,6 +92,7 @@ es: custom: name: Personalizado upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Conectar OpenProject a un proveedor de identidad OpenID Connect label_add_new: Agregar nuevo proveedor de OpenID label_edit: Editar el proveedor de OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/et.yml b/modules/openid_connect/config/locales/crowdin/et.yml index ab61a976b02..252ba07fe9d 100644 --- a/modules/openid_connect/config/locales/crowdin/et.yml +++ b/modules/openid_connect/config/locales/crowdin/et.yml @@ -92,6 +92,7 @@ et: custom: name: Kohandatud upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/eu.yml b/modules/openid_connect/config/locales/crowdin/eu.yml index 42ef7366e07..842e25768bf 100644 --- a/modules/openid_connect/config/locales/crowdin/eu.yml +++ b/modules/openid_connect/config/locales/crowdin/eu.yml @@ -92,6 +92,7 @@ eu: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/fa.yml b/modules/openid_connect/config/locales/crowdin/fa.yml index 7ef18e612a9..2da44818e32 100644 --- a/modules/openid_connect/config/locales/crowdin/fa.yml +++ b/modules/openid_connect/config/locales/crowdin/fa.yml @@ -92,6 +92,7 @@ fa: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/fi.yml b/modules/openid_connect/config/locales/crowdin/fi.yml index 13597410e5c..ce996ef0f01 100644 --- a/modules/openid_connect/config/locales/crowdin/fi.yml +++ b/modules/openid_connect/config/locales/crowdin/fi.yml @@ -92,6 +92,7 @@ fi: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/fil.yml b/modules/openid_connect/config/locales/crowdin/fil.yml index cc07c5fa911..0210762ebf0 100644 --- a/modules/openid_connect/config/locales/crowdin/fil.yml +++ b/modules/openid_connect/config/locales/crowdin/fil.yml @@ -92,6 +92,7 @@ fil: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/fr.yml b/modules/openid_connect/config/locales/crowdin/fr.yml index fdebf25ca4f..001a1a97b66 100644 --- a/modules/openid_connect/config/locales/crowdin/fr.yml +++ b/modules/openid_connect/config/locales/crowdin/fr.yml @@ -92,6 +92,7 @@ fr: custom: name: Personnalisé upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connecter OpenProject à un fournisseur d'identité OpenID Connect label_add_new: Ajouter un nouveau fournisseur OpenID label_edit: Modifier le fournisseur OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/he.yml b/modules/openid_connect/config/locales/crowdin/he.yml index 261a7ca24d6..1de4c6ffed3 100644 --- a/modules/openid_connect/config/locales/crowdin/he.yml +++ b/modules/openid_connect/config/locales/crowdin/he.yml @@ -92,6 +92,7 @@ he: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/hi.yml b/modules/openid_connect/config/locales/crowdin/hi.yml index b6ae65eea6f..f34f9b0f129 100644 --- a/modules/openid_connect/config/locales/crowdin/hi.yml +++ b/modules/openid_connect/config/locales/crowdin/hi.yml @@ -92,6 +92,7 @@ hi: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/hr.yml b/modules/openid_connect/config/locales/crowdin/hr.yml index cf312943608..70d4a59cd5b 100644 --- a/modules/openid_connect/config/locales/crowdin/hr.yml +++ b/modules/openid_connect/config/locales/crowdin/hr.yml @@ -92,6 +92,7 @@ hr: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/hu.yml b/modules/openid_connect/config/locales/crowdin/hu.yml index 2f3ba184fb5..0aa9cc02a3b 100644 --- a/modules/openid_connect/config/locales/crowdin/hu.yml +++ b/modules/openid_connect/config/locales/crowdin/hu.yml @@ -92,6 +92,7 @@ hu: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: OpenID szolgáltató hozzáadása label_edit: OpenID szolgáltató %{name} szerkesztése diff --git a/modules/openid_connect/config/locales/crowdin/id.yml b/modules/openid_connect/config/locales/crowdin/id.yml index fa74a9908f0..e83802001e8 100644 --- a/modules/openid_connect/config/locales/crowdin/id.yml +++ b/modules/openid_connect/config/locales/crowdin/id.yml @@ -92,6 +92,7 @@ id: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Tambahkan penyedia OpenID baru label_edit: Edit penyedia OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/it.yml b/modules/openid_connect/config/locales/crowdin/it.yml index aff4d8b4c55..ff223cb5516 100644 --- a/modules/openid_connect/config/locales/crowdin/it.yml +++ b/modules/openid_connect/config/locales/crowdin/it.yml @@ -92,6 +92,7 @@ it: custom: name: Personalizzato upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connetti OpenProject a un fornitore di identità OpenID connect label_add_new: Aggiungi un nuovo provider OpenID label_edit: Modifica provider OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ja.yml b/modules/openid_connect/config/locales/crowdin/ja.yml index a732620e3d9..ea1cb9db368 100644 --- a/modules/openid_connect/config/locales/crowdin/ja.yml +++ b/modules/openid_connect/config/locales/crowdin/ja.yml @@ -92,6 +92,7 @@ ja: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: 新しい OpenID プロバイダーを追加 label_edit: OpenID プロバイダー %{name} を編集 diff --git a/modules/openid_connect/config/locales/crowdin/ka.yml b/modules/openid_connect/config/locales/crowdin/ka.yml index aac0dd5e001..610bc0bd86f 100644 --- a/modules/openid_connect/config/locales/crowdin/ka.yml +++ b/modules/openid_connect/config/locales/crowdin/ka.yml @@ -92,6 +92,7 @@ ka: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/kk.yml b/modules/openid_connect/config/locales/crowdin/kk.yml index 1541745c73a..d7f65270c98 100644 --- a/modules/openid_connect/config/locales/crowdin/kk.yml +++ b/modules/openid_connect/config/locales/crowdin/kk.yml @@ -92,6 +92,7 @@ kk: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ko.yml b/modules/openid_connect/config/locales/crowdin/ko.yml index b55c2f6698b..3adcc86fc55 100644 --- a/modules/openid_connect/config/locales/crowdin/ko.yml +++ b/modules/openid_connect/config/locales/crowdin/ko.yml @@ -92,6 +92,7 @@ ko: custom: name: 사용자 지정 upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: OpenID Connect ID 공급자에 OpenProject 연결 label_add_new: 새로운 OpenID 공급자 추가 label_edit: OpenID 공급자 %{name} 편집 diff --git a/modules/openid_connect/config/locales/crowdin/lt.yml b/modules/openid_connect/config/locales/crowdin/lt.yml index 6953579d5ff..048c479a0ef 100644 --- a/modules/openid_connect/config/locales/crowdin/lt.yml +++ b/modules/openid_connect/config/locales/crowdin/lt.yml @@ -92,6 +92,7 @@ lt: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Pridėti naują OpenID tiekėją label_edit: Keisti OpenID tiekėją %{name} diff --git a/modules/openid_connect/config/locales/crowdin/lv.yml b/modules/openid_connect/config/locales/crowdin/lv.yml index cfc8976b9a6..76fc2f6f77e 100644 --- a/modules/openid_connect/config/locales/crowdin/lv.yml +++ b/modules/openid_connect/config/locales/crowdin/lv.yml @@ -92,6 +92,7 @@ lv: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/mn.yml b/modules/openid_connect/config/locales/crowdin/mn.yml index 4a1de552294..fa15043ee20 100644 --- a/modules/openid_connect/config/locales/crowdin/mn.yml +++ b/modules/openid_connect/config/locales/crowdin/mn.yml @@ -92,6 +92,7 @@ mn: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ms.yml b/modules/openid_connect/config/locales/crowdin/ms.yml index 38ca0fe23f8..8a0e7b6701e 100644 --- a/modules/openid_connect/config/locales/crowdin/ms.yml +++ b/modules/openid_connect/config/locales/crowdin/ms.yml @@ -92,6 +92,7 @@ ms: custom: name: Adat upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Sambungkan OpenProject kepada pembekal identiti sambungan OpenID label_add_new: Tambah penyedia OpenID baharu label_edit: Edit penyedia OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ne.yml b/modules/openid_connect/config/locales/crowdin/ne.yml index acd30aa9918..41cb3c16aef 100644 --- a/modules/openid_connect/config/locales/crowdin/ne.yml +++ b/modules/openid_connect/config/locales/crowdin/ne.yml @@ -92,6 +92,7 @@ ne: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/nl.yml b/modules/openid_connect/config/locales/crowdin/nl.yml index 632a5b3f7cd..b368398834e 100644 --- a/modules/openid_connect/config/locales/crowdin/nl.yml +++ b/modules/openid_connect/config/locales/crowdin/nl.yml @@ -92,6 +92,7 @@ nl: custom: name: Aangepast upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: OpenProject verbinden met een OpenID connect identity provider label_add_new: Voeg een nieuwe OpenID provider toe label_edit: OpenID provider %{name} bewerken diff --git a/modules/openid_connect/config/locales/crowdin/no.yml b/modules/openid_connect/config/locales/crowdin/no.yml index ee45b8e5cac..92be6b2600e 100644 --- a/modules/openid_connect/config/locales/crowdin/no.yml +++ b/modules/openid_connect/config/locales/crowdin/no.yml @@ -92,6 +92,7 @@ custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Legg til ny OpenID-leverandør label_edit: Rediger OpenID-leverandør %{name} diff --git a/modules/openid_connect/config/locales/crowdin/pl.yml b/modules/openid_connect/config/locales/crowdin/pl.yml index cd9a8dde0c6..b8224f59695 100644 --- a/modules/openid_connect/config/locales/crowdin/pl.yml +++ b/modules/openid_connect/config/locales/crowdin/pl.yml @@ -92,6 +92,7 @@ pl: custom: name: Niestandardowy upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Połącz OpenProject z dostawcą tożsamości OpenID Connect label_add_new: Dodaj nowego dostawcę OpenID label_edit: Edytuj dostawcę OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/pt-BR.yml b/modules/openid_connect/config/locales/crowdin/pt-BR.yml index e9521ee3c09..76d3c4dffed 100644 --- a/modules/openid_connect/config/locales/crowdin/pt-BR.yml +++ b/modules/openid_connect/config/locales/crowdin/pt-BR.yml @@ -92,6 +92,7 @@ pt-BR: custom: name: Personalizar upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Conecte o OpenProject a um provedor de identidade OpenID Connect label_add_new: Adicionar um novo provedor OpenID label_edit: Editar provedor OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/pt-PT.yml b/modules/openid_connect/config/locales/crowdin/pt-PT.yml index 7a7f038a9fb..f01989bb22b 100644 --- a/modules/openid_connect/config/locales/crowdin/pt-PT.yml +++ b/modules/openid_connect/config/locales/crowdin/pt-PT.yml @@ -92,6 +92,7 @@ pt-PT: custom: name: Personalizado upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Associar o OpenProject a um fornecedor de identidade OpenID Connect label_add_new: Adicionar um novo fornecedor OpenID label_edit: Editar fornecedor de OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ro.yml b/modules/openid_connect/config/locales/crowdin/ro.yml index 1ae193110c2..cdf2d963f76 100644 --- a/modules/openid_connect/config/locales/crowdin/ro.yml +++ b/modules/openid_connect/config/locales/crowdin/ro.yml @@ -92,6 +92,7 @@ ro: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Adaugă stare nouă label_edit: Editați furnizorul OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/ru.yml b/modules/openid_connect/config/locales/crowdin/ru.yml index 80ac36ac536..382c97c942a 100644 --- a/modules/openid_connect/config/locales/crowdin/ru.yml +++ b/modules/openid_connect/config/locales/crowdin/ru.yml @@ -92,6 +92,7 @@ ru: custom: name: Пользовательский upsale: + title: "Single Sign-On (SSO) с подключением OpenID" description: Подключить OpenProject к провайдеру идентификации OpenID connect label_add_new: Добавить нового провайдера OpenID label_edit: Редактировать провайдера OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/rw.yml b/modules/openid_connect/config/locales/crowdin/rw.yml index 06419d2835e..6846f5c5b38 100644 --- a/modules/openid_connect/config/locales/crowdin/rw.yml +++ b/modules/openid_connect/config/locales/crowdin/rw.yml @@ -92,6 +92,7 @@ rw: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/si.yml b/modules/openid_connect/config/locales/crowdin/si.yml index c9b38005b3e..26c318fb2b8 100644 --- a/modules/openid_connect/config/locales/crowdin/si.yml +++ b/modules/openid_connect/config/locales/crowdin/si.yml @@ -92,6 +92,7 @@ si: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/sk.yml b/modules/openid_connect/config/locales/crowdin/sk.yml index 745d6822c33..1325bda00b2 100644 --- a/modules/openid_connect/config/locales/crowdin/sk.yml +++ b/modules/openid_connect/config/locales/crowdin/sk.yml @@ -92,6 +92,7 @@ sk: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/sl.yml b/modules/openid_connect/config/locales/crowdin/sl.yml index d62b6a4dae3..18d98dbe3e4 100644 --- a/modules/openid_connect/config/locales/crowdin/sl.yml +++ b/modules/openid_connect/config/locales/crowdin/sl.yml @@ -92,6 +92,7 @@ sl: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Dodaj novega OpenID ponudnika label_edit: Uredi OpenID ponudnik %{name} diff --git a/modules/openid_connect/config/locales/crowdin/sr.yml b/modules/openid_connect/config/locales/crowdin/sr.yml index 69653198741..e4d88508f06 100644 --- a/modules/openid_connect/config/locales/crowdin/sr.yml +++ b/modules/openid_connect/config/locales/crowdin/sr.yml @@ -92,6 +92,7 @@ sr: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/sv.yml b/modules/openid_connect/config/locales/crowdin/sv.yml index ba4aaf94619..88a6f9acbd0 100644 --- a/modules/openid_connect/config/locales/crowdin/sv.yml +++ b/modules/openid_connect/config/locales/crowdin/sv.yml @@ -92,6 +92,7 @@ sv: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Lägg till en ny OpenID-leverantör label_edit: Redigera OpenID-leverantör %{name} diff --git a/modules/openid_connect/config/locales/crowdin/th.yml b/modules/openid_connect/config/locales/crowdin/th.yml index 3ec4653df12..632a0fd8ec8 100644 --- a/modules/openid_connect/config/locales/crowdin/th.yml +++ b/modules/openid_connect/config/locales/crowdin/th.yml @@ -92,6 +92,7 @@ th: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/tr.yml b/modules/openid_connect/config/locales/crowdin/tr.yml index b2a667f94a5..058853aa056 100644 --- a/modules/openid_connect/config/locales/crowdin/tr.yml +++ b/modules/openid_connect/config/locales/crowdin/tr.yml @@ -92,6 +92,7 @@ tr: custom: name: Özel upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Yeni bir OpenID sağlayıcı ekle label_edit: OpenID sağlayıcıyı düzenle %{name} diff --git a/modules/openid_connect/config/locales/crowdin/uk.yml b/modules/openid_connect/config/locales/crowdin/uk.yml index 4b2366eac97..3afc93e5449 100644 --- a/modules/openid_connect/config/locales/crowdin/uk.yml +++ b/modules/openid_connect/config/locales/crowdin/uk.yml @@ -92,6 +92,7 @@ uk: custom: name: Власний upsale: + title: "Єдиний вхід (SSO) з OpenID Connect" description: Підключіть OpenProject до постачальника ідентифікаційних даних OpenID label_add_new: Додати нового постачальника OpenID label_edit: Редагувати OpenID провайдера %{name} diff --git a/modules/openid_connect/config/locales/crowdin/uz.yml b/modules/openid_connect/config/locales/crowdin/uz.yml index 4474b9b1f06..baac338feac 100644 --- a/modules/openid_connect/config/locales/crowdin/uz.yml +++ b/modules/openid_connect/config/locales/crowdin/uz.yml @@ -92,6 +92,7 @@ uz: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/config/locales/crowdin/vi.yml b/modules/openid_connect/config/locales/crowdin/vi.yml index b175ce0c00b..815e62ae382 100644 --- a/modules/openid_connect/config/locales/crowdin/vi.yml +++ b/modules/openid_connect/config/locales/crowdin/vi.yml @@ -92,6 +92,7 @@ vi: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Thêm nhà cung cấp OpenID mới label_edit: Chỉnh sửa nhà cung cấp OpenID %{name} diff --git a/modules/openid_connect/config/locales/crowdin/zh-CN.yml b/modules/openid_connect/config/locales/crowdin/zh-CN.yml index 52a3771898e..a916e965e4f 100644 --- a/modules/openid_connect/config/locales/crowdin/zh-CN.yml +++ b/modules/openid_connect/config/locales/crowdin/zh-CN.yml @@ -92,6 +92,7 @@ zh-CN: custom: name: 自定义 upsale: + title: "与 OpenID 连接的单点登录 (SSO)" description: 将 OpenProject 连接到 OpenID connect 身份提供商 label_add_new: 添加一个新的 OpenID 提供商 label_edit: 编辑 OpenID 提供商 %{name} diff --git a/modules/openid_connect/config/locales/crowdin/zh-TW.yml b/modules/openid_connect/config/locales/crowdin/zh-TW.yml index 0e5889b4100..7c746c432ce 100644 --- a/modules/openid_connect/config/locales/crowdin/zh-TW.yml +++ b/modules/openid_connect/config/locales/crowdin/zh-TW.yml @@ -92,6 +92,7 @@ zh-TW: custom: name: 自訂 upsale: + title: "使用 OpenID 連線的單一登入 (SSO)" description: 將 OpenProject 連接到 OpenID connect 身分提供者 label_add_new: 新增 OpenID 提供商 label_edit: 編輯 OpenID 提供商:%{name} diff --git a/modules/openid_connect/config/locales/en.yml b/modules/openid_connect/config/locales/en.yml index 9b0f42e7842..d4869fc40cf 100644 --- a/modules/openid_connect/config/locales/en.yml +++ b/modules/openid_connect/config/locales/en.yml @@ -97,6 +97,7 @@ en: custom: name: Custom upsale: + title: "Single Sign-On (SSO) with OpenID connect" description: Connect OpenProject to an OpenID connect identity provider label_add_new: Add a new OpenID provider label_edit: Edit OpenID provider %{name} diff --git a/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb b/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb index 9b5a5608986..60a993f3aff 100644 --- a/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb +++ b/modules/openid_connect/spec/features/administration/oidc_custom_crud_spec.rb @@ -175,7 +175,7 @@ RSpec.describe "OIDC administration CRUD", context "without EE", without_ee: %i[sso_auth_providers] do it "renders the upsale page" do visit "/admin/openid_connect/providers" - expect(page).to have_text "OpenID providers is an Enterprise add-on" + expect(page).to have_enterprise_banner(:premium) end end end diff --git a/modules/overviews/app/components/_index.sass b/modules/overviews/app/components/_index.sass index fc1fdcf84e0..3ffde8d69d3 100644 --- a/modules/overviews/app/components/_index.sass +++ b/modules/overviews/app/components/_index.sass @@ -1 +1 @@ -@import "./project_custom_fields/sections/project_custom_fields/show_component.sass" +@import "./overviews/project_custom_fields/item_component.sass" diff --git a/modules/overviews/app/components/project_custom_fields/sections/edit_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/edit_component.html.erb similarity index 100% rename from modules/overviews/app/components/project_custom_fields/sections/edit_component.html.erb rename to modules/overviews/app/components/overviews/project_custom_fields/edit_component.html.erb diff --git a/modules/overviews/app/components/project_custom_fields/sections/edit_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/edit_component.rb similarity index 95% rename from modules/overviews/app/components/project_custom_fields/sections/edit_component.rb rename to modules/overviews/app/components/overviews/project_custom_fields/edit_component.rb index ebf29c10644..7317293ba46 100644 --- a/modules/overviews/app/components/project_custom_fields/sections/edit_component.rb +++ b/modules/overviews/app/components/overviews/project_custom_fields/edit_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectCustomFields - module Sections +module Overviews + module ProjectCustomFields class EditComponent < ApplicationComponent include ApplicationHelper include OpTurbo::Streamable diff --git a/modules/overviews/app/components/project_custom_fields/sections/edit_dialog_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/edit_dialog_component.html.erb similarity index 81% rename from modules/overviews/app/components/project_custom_fields/sections/edit_dialog_component.html.erb rename to modules/overviews/app/components/overviews/project_custom_fields/edit_dialog_component.html.erb index 1c4859b33cb..2ffb36b5c94 100644 --- a/modules/overviews/app/components/project_custom_fields/sections/edit_dialog_component.html.erb +++ b/modules/overviews/app/components/overviews/project_custom_fields/edit_dialog_component.html.erb @@ -8,12 +8,12 @@ ) do |d| d.with_header(variant: :large) d.with_body(classes: "Overlay-body_autocomplete_height") do - render(::ProjectCustomFields::Sections::EditComponent.new(project_custom_field_section: @project_custom_field_section, project: @project)) + render(Overviews::ProjectCustomFields::EditComponent.new(project_custom_field_section: @project_custom_field_section, project: @project)) end d.with_footer do component_collection do |footer_collection| footer_collection.with_component( - Primer::ButtonComponent.new( + Primer::Beta::Button.new( data: { "close-dialog-id": "edit-project-custom-fields-dialog-#{@project_custom_field_section.id}" } @@ -22,7 +22,7 @@ t("button_cancel") end footer_collection.with_component( - Primer::ButtonComponent.new( + Primer::Beta::Button.new( scheme: :primary, type: :submit, form: "project-section-edit-form", diff --git a/modules/overviews/app/components/project_custom_fields/sections/edit_dialog_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/edit_dialog_component.rb similarity index 95% rename from modules/overviews/app/components/project_custom_fields/sections/edit_dialog_component.rb rename to modules/overviews/app/components/overviews/project_custom_fields/edit_dialog_component.rb index 102d7b04746..6099f6ee07b 100644 --- a/modules/overviews/app/components/project_custom_fields/sections/edit_dialog_component.rb +++ b/modules/overviews/app/components/overviews/project_custom_fields/edit_dialog_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectCustomFields - module Sections +module Overviews + module ProjectCustomFields class EditDialogComponent < ApplicationComponent include ApplicationHelper include OpTurbo::Streamable diff --git a/modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/item_component.html.erb similarity index 100% rename from modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.html.erb rename to modules/overviews/app/components/overviews/project_custom_fields/item_component.html.erb diff --git a/modules/overviews/app/components/overviews/project_custom_fields/item_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/item_component.rb new file mode 100644 index 00000000000..954fecc3a3b --- /dev/null +++ b/modules/overviews/app/components/overviews/project_custom_fields/item_component.rb @@ -0,0 +1,105 @@ +# 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 Overviews + module ProjectCustomFields + class ItemComponent < ApplicationComponent + include ApplicationHelper + include CustomFieldsHelper + include OpPrimer::ComponentHelpers + + def initialize(project_custom_field:, project_custom_field_values:) + super + + @project_custom_field = project_custom_field + @project_custom_field_values = project_custom_field_values + end + + private + + def not_set? + @project_custom_field_values.empty? || @project_custom_field_values.all? { |cf_value| cf_value.value.blank? } + end + + def render_value + case @project_custom_field.field_format + when "link" + render_link + when "text" + render_long_text + when "user" + render_user + else + render(Primer::Beta::Text.new) do + @project_custom_field_values&.map do |cf_value| + format_value(cf_value.value, @project_custom_field) + end&.join(", ") + end + end + end + + def render_long_text + render OpenProject::Common::AttributeComponent.new("dialog-cf-#{@project_custom_field.id}", + @project_custom_field.name, + @project_custom_field_values&.first&.value, + lines: 3) + end + + def render_user + if @project_custom_field.multi_value? + flex_layout do |avatar_container| + @project_custom_field_values&.each do |cf_value| + avatar_container.with_row do + render_avatar(cf_value.typed_value) + end + end + end + else + render_avatar(@project_custom_field_values&.first&.typed_value) + end + end + + def render_avatar(user) + render(Users::AvatarComponent.new(user:, size: :mini)) + end + + def render_link + href = @project_custom_field_values&.first&.value + link = Addressable::URI.parse(href) + return href unless link + + target = link.host == Setting.host_without_protocol ? "_top" : "_blank" + render(Primer::Beta::Link.new(href:, rel: "noopener noreferrer", target:)) do + href + end + end + end + end +end diff --git a/modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.sass b/modules/overviews/app/components/overviews/project_custom_fields/item_component.sass similarity index 100% rename from modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.sass rename to modules/overviews/app/components/overviews/project_custom_fields/item_component.sass diff --git a/modules/overviews/app/components/project_custom_fields/sections/show_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb similarity index 93% rename from modules/overviews/app/components/project_custom_fields/sections/show_component.html.erb rename to modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb index db29d31d191..bdcaadcbde3 100644 --- a/modules/overviews/app/components/project_custom_fields/sections/show_component.html.erb +++ b/modules/overviews/app/components/overviews/project_custom_fields/show_component.html.erb @@ -26,7 +26,7 @@ margin = i == @project_custom_fields.size - 1 ? 0 : 3 details_container.with_row(mb: margin) do render( - ProjectCustomFields::Sections::ProjectCustomFields::ShowComponent.new( + Overviews::ProjectCustomFields::ItemComponent.new( project_custom_field:, project_custom_field_values: get_eager_loaded_project_custom_field_values_for(project_custom_field.id) ) diff --git a/modules/overviews/app/components/project_custom_fields/sections/show_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/show_component.rb similarity index 96% rename from modules/overviews/app/components/project_custom_fields/sections/show_component.rb rename to modules/overviews/app/components/overviews/project_custom_fields/show_component.rb index 78137a19d77..341176dc930 100644 --- a/modules/overviews/app/components/project_custom_fields/sections/show_component.rb +++ b/modules/overviews/app/components/overviews/project_custom_fields/show_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectCustomFields - module Sections +module Overviews + module ProjectCustomFields class ShowComponent < ApplicationComponent include ApplicationHelper include OpPrimer::ComponentHelpers diff --git a/modules/overviews/app/components/project_custom_fields/side_panel_component.html.erb b/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.html.erb similarity index 90% rename from modules/overviews/app/components/project_custom_fields/side_panel_component.html.erb rename to modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.html.erb index b6aa482acd6..2eecef3a612 100644 --- a/modules/overviews/app/components/project_custom_fields/side_panel_component.html.erb +++ b/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.html.erb @@ -9,7 +9,7 @@ ) do |panel| available_project_custom_fields_grouped_by_section.each do |project_custom_field_section, project_custom_fields| panel.with_section( - ProjectCustomFields::Sections::ShowComponent.new( + Overviews::ProjectCustomFields::ShowComponent.new( project: @project, project_custom_field_section:, project_custom_fields: project_custom_fields diff --git a/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb b/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb new file mode 100644 index 00000000000..2f531e43142 --- /dev/null +++ b/modules/overviews/app/components/overviews/project_custom_fields/side_panel_component.rb @@ -0,0 +1,52 @@ +# 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 Overviews + module ProjectCustomFields + class SidePanelComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(project:) + super + + @project = project + 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) + end + end + end +end diff --git a/modules/overviews/app/components/project_life_cycles/sections/edit_component.html.erb b/modules/overviews/app/components/overviews/project_phases/edit_component.html.erb similarity index 52% rename from modules/overviews/app/components/project_life_cycles/sections/edit_component.html.erb rename to modules/overviews/app/components/overviews/project_phases/edit_component.html.erb index 3ba10a71d01..4d7e766733b 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/edit_component.html.erb +++ b/modules/overviews/app/components/overviews/project_phases/edit_component.html.erb @@ -7,19 +7,14 @@ method: :put, data: { controller: "overview--project-life-cycles-form", - "overview--project-life-cycles-form-url-value": project_life_cycles_form_path(project_id: model.id), + "overview--project-life-cycles-form-url-value": preview_project_phase_path(model), "application-target": "dynamic", turbo: true, turbo_stream: true, "test-selector": "async-dialog-content" - }, - url: update_project_life_cycles_path(project_id: model.id) + } ) do |f| - render(Primer::Forms::SpacingWrapper.new) do - f.fields_for(:available_phases) do |life_cycle_form| - render(Projects::LifeCycles::Form.new(life_cycle_form)) - end - end + render(Projects::LifeCycles::Form.new(f)) end end %> diff --git a/modules/overviews/app/components/project_life_cycles/sections/edit_component.rb b/modules/overviews/app/components/overviews/project_phases/edit_component.rb similarity index 95% rename from modules/overviews/app/components/project_life_cycles/sections/edit_component.rb rename to modules/overviews/app/components/overviews/project_phases/edit_component.rb index 69e395d772f..f49d49dd713 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/edit_component.rb +++ b/modules/overviews/app/components/overviews/project_phases/edit_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectLifeCycles - module Sections +module Overviews + module ProjectPhases class EditComponent < ApplicationComponent include ApplicationHelper include OpTurbo::Streamable diff --git a/modules/overviews/app/components/project_life_cycles/sections/edit_dialog_component.html.erb b/modules/overviews/app/components/overviews/project_phases/edit_dialog_component.html.erb similarity index 85% rename from modules/overviews/app/components/project_life_cycles/sections/edit_dialog_component.html.erb rename to modules/overviews/app/components/overviews/project_phases/edit_dialog_component.html.erb index 1a027271f46..4c5725836bd 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/edit_dialog_component.html.erb +++ b/modules/overviews/app/components/overviews/project_phases/edit_dialog_component.html.erb @@ -8,12 +8,12 @@ ) do |d| d.with_header(variant: :large) d.with_body do - render(::ProjectLifeCycles::Sections::EditComponent.new(model)) + render(Overviews::ProjectPhases::EditComponent.new(model)) end d.with_footer do component_collection do |footer_collection| footer_collection.with_component( - Primer::ButtonComponent.new( + Primer::Beta::Button.new( data: { "close-dialog-id": DIALOG_ID } @@ -22,7 +22,7 @@ t("button_cancel") end footer_collection.with_component( - Primer::ButtonComponent.new( + Primer::Beta::Button.new( scheme: :primary, type: :submit, form: "project-life-cycles-edit-form", diff --git a/modules/overviews/app/components/project_life_cycles/sections/edit_dialog_component.rb b/modules/overviews/app/components/overviews/project_phases/edit_dialog_component.rb similarity index 97% rename from modules/overviews/app/components/project_life_cycles/sections/edit_dialog_component.rb rename to modules/overviews/app/components/overviews/project_phases/edit_dialog_component.rb index 9d9a248eda1..3e916d9b167 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/edit_dialog_component.rb +++ b/modules/overviews/app/components/overviews/project_phases/edit_dialog_component.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectLifeCycles - module Sections +module Overviews + module ProjectPhases class EditDialogComponent < ApplicationComponent include ApplicationHelper include OpTurbo::Streamable diff --git a/modules/overviews/app/components/project_life_cycles/sections/project_life_cycles/show_component.html.erb b/modules/overviews/app/components/overviews/project_phases/item_component.html.erb similarity index 74% rename from modules/overviews/app/components/project_life_cycles/sections/project_life_cycles/show_component.html.erb rename to modules/overviews/app/components/overviews/project_phases/item_component.html.erb index 14c08c4f9a6..ced74188556 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/project_life_cycles/show_component.html.erb +++ b/modules/overviews/app/components/overviews/project_phases/item_component.html.erb @@ -20,10 +20,12 @@ end life_cycle_container.with_row(w: :full) do - if not_set? - render(Primer::Beta::Text.new) { t("placeholders.default") } - else - render Projects::PhaseComponent.new(phase: model) + render(authorized_edit_link) do + if not_set? + render(Primer::Beta::Text.new) { t("placeholders.default") } + else + render Projects::PhaseComponent.new(phase: model) + end end end end diff --git a/modules/overviews/app/components/overviews/project_phases/item_component.rb b/modules/overviews/app/components/overviews/project_phases/item_component.rb new file mode 100644 index 00000000000..2e8baef8755 --- /dev/null +++ b/modules/overviews/app/components/overviews/project_phases/item_component.rb @@ -0,0 +1,74 @@ +# 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 Overviews + module ProjectPhases + class ItemComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + + private + + def not_set? + model.not_set? + end + + def icon + :"op-phase" + end + + def icon_color_class + helpers.hl_inline_class("project_phase_definition", model.definition) + end + + def text + model.name + end + + def authorized_edit_link + if allowed_to_edit? + Primer::Beta::Link.new( + href: edit_project_phase_path(model), + data: { controller: "async-dialog" }, + aria: { label: I18n.t(:label_edit) }, + test_selector: "project-life-cycle-edit-button-#{model.id}", + underline: false + ) + else + Primer::Content.new + end + end + + def allowed_to_edit? + User.current.allowed_in_project?(:edit_project_phases, model.project) + end + end + end +end diff --git a/modules/overviews/app/components/overviews/project_phases/show_component.html.erb b/modules/overviews/app/components/overviews/project_phases/show_component.html.erb new file mode 100644 index 00000000000..180acce00ae --- /dev/null +++ b/modules/overviews/app/components/overviews/project_phases/show_component.html.erb @@ -0,0 +1,25 @@ +<%= + component_wrapper do + render( + Primer::OpenProject::SidePanel::Section.new( + classes: "op-project-life-cyle-section-container", + test_selector: "project-life-cycle-section" + ) + ) do |section| + section.with_title { t("label_life_cycle_step_plural") } + + flex_layout do |details_container| + @phases.each_with_index do |life_cycle_step, i| + margin = i == @phases.size - 1 ? 0 : 3 + details_container.with_row(mb: margin) do + render( + Overviews::ProjectPhases::ItemComponent.new( + life_cycle_step + ) + ) + end + end + end + end + end +%> diff --git a/modules/overviews/app/components/project_life_cycles/sections/show_component.rb b/modules/overviews/app/components/overviews/project_phases/show_component.rb similarity index 86% rename from modules/overviews/app/components/project_life_cycles/sections/show_component.rb rename to modules/overviews/app/components/overviews/project_phases/show_component.rb index ff8bfff92a1..2f2561282d9 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/show_component.rb +++ b/modules/overviews/app/components/overviews/project_phases/show_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectLifeCycles - module Sections +module Overviews + module ProjectPhases class ShowComponent < ApplicationComponent include ApplicationHelper include OpPrimer::ComponentHelpers @@ -37,13 +39,7 @@ module ProjectLifeCycles super @project = project - @life_cycle_steps = @project.available_phases - end - - private - - def allowed_to_edit? - User.current.allowed_in_project?(:edit_project_phases, @project) + @phases = @project.available_phases end end end diff --git a/modules/overviews/app/components/project_life_cycles/side_panel_component.html.erb b/modules/overviews/app/components/overviews/project_phases/side_panel_component.html.erb similarity index 69% rename from modules/overviews/app/components/project_life_cycles/side_panel_component.html.erb rename to modules/overviews/app/components/overviews/project_phases/side_panel_component.html.erb index 230767e0bc9..61a3e308f65 100644 --- a/modules/overviews/app/components/project_life_cycles/side_panel_component.html.erb +++ b/modules/overviews/app/components/overviews/project_phases/side_panel_component.html.erb @@ -6,7 +6,7 @@ test_selector: "project-life-cycles-sidebar-async-content" ) ) do |panel| - panel.with_section(ProjectLifeCycles::Sections::ShowComponent.new(project: @project)) + panel.with_section(Overviews::ProjectPhases::ShowComponent.new(project: @project)) end end %> diff --git a/modules/overviews/app/components/overviews/project_phases/side_panel_component.rb b/modules/overviews/app/components/overviews/project_phases/side_panel_component.rb new file mode 100644 index 00000000000..4929ccba078 --- /dev/null +++ b/modules/overviews/app/components/overviews/project_phases/side_panel_component.rb @@ -0,0 +1,45 @@ +# 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 Overviews + module ProjectPhases + class SidePanelComponent < ApplicationComponent + include ApplicationHelper + include OpPrimer::ComponentHelpers + include OpTurbo::Streamable + + def initialize(project:) + super + + @project = project + end + end + end +end diff --git a/modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.rb b/modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.rb deleted file mode 100644 index a8681d715e5..00000000000 --- a/modules/overviews/app/components/project_custom_fields/sections/project_custom_fields/show_component.rb +++ /dev/null @@ -1,105 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module ProjectCustomFields - module Sections - module ProjectCustomFields - class ShowComponent < ApplicationComponent - include ApplicationHelper - include CustomFieldsHelper - include OpPrimer::ComponentHelpers - - def initialize(project_custom_field:, project_custom_field_values:) - super - - @project_custom_field = project_custom_field - @project_custom_field_values = project_custom_field_values - end - - private - - def not_set? - @project_custom_field_values.empty? || @project_custom_field_values.all? { |cf_value| cf_value.value.blank? } - end - - def render_value - case @project_custom_field.field_format - when "link" - render_link - when "text" - render_long_text - when "user" - render_user - else - render(Primer::Beta::Text.new) do - @project_custom_field_values&.map do |cf_value| - format_value(cf_value.value, @project_custom_field) - end&.join(", ") - end - end - end - - def render_long_text - render OpenProject::Common::AttributeComponent.new("dialog-cf-#{@project_custom_field.id}", - @project_custom_field.name, - @project_custom_field_values&.first&.value, - lines: 3) - end - - def render_user - if @project_custom_field.multi_value? - flex_layout do |avatar_container| - @project_custom_field_values&.each do |cf_value| - avatar_container.with_row do - render_avatar(cf_value.typed_value) - end - end - end - else - render_avatar(@project_custom_field_values&.first&.typed_value) - end - end - - def render_avatar(user) - render(Users::AvatarComponent.new(user:, size: :mini)) - end - - def render_link - href = @project_custom_field_values&.first&.value - link = Addressable::URI.parse(href) - return href unless link - - target = link.host == Setting.host_without_protocol ? "_top" : "_blank" - render(Primer::Beta::Link.new(href:, rel: "noopener noreferrer", target:)) do - href - end - end - end - end - end -end diff --git a/modules/overviews/app/components/project_life_cycles/sections/show_component.html.erb b/modules/overviews/app/components/project_life_cycles/sections/show_component.html.erb deleted file mode 100644 index 4823da14107..00000000000 --- a/modules/overviews/app/components/project_life_cycles/sections/show_component.html.erb +++ /dev/null @@ -1,38 +0,0 @@ -<%= - component_wrapper do - render( - Primer::OpenProject::SidePanel::Section.new( - classes: "op-project-life-cyle-section-container", - test_selector: "project-life-cycle-section" - ) - ) do |section| - section.with_title { t("label_life_cycle_step_plural") } - - if allowed_to_edit? - section.with_action_icon( - icon: :pencil, - tag: :a, - href: project_life_cycles_dialog_path(project_id: @project.id), - data: { - controller: "async-dialog" - }, - test_selector: "project-life-cycles-edit-button", - aria: { label: I18n.t(:label_edit) } - ) - end - - flex_layout do |details_container| - @life_cycle_steps.each_with_index do |life_cycle_step, i| - margin = i == @life_cycle_steps.size - 1 ? 0 : 3 - details_container.with_row(mb: margin) do - render( - ProjectLifeCycles::Sections::ProjectLifeCycles::ShowComponent.new( - life_cycle_step - ) - ) - end - end - end - end - end -%> diff --git a/modules/overviews/app/components/project_life_cycles/side_panel_component.rb b/modules/overviews/app/components/project_life_cycles/side_panel_component.rb deleted file mode 100644 index d837969ffa3..00000000000 --- a/modules/overviews/app/components/project_life_cycles/side_panel_component.rb +++ /dev/null @@ -1,41 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -module ProjectLifeCycles - class SidePanelComponent < ApplicationComponent - include ApplicationHelper - include OpPrimer::ComponentHelpers - include OpTurbo::Streamable - - def initialize(project:) - super - - @project = project - end - end -end diff --git a/modules/overviews/app/controllers/overviews/overviews_controller.rb b/modules/overviews/app/controllers/overviews/overviews_controller.rb index 976d38d23ef..bc08e436faf 100644 --- a/modules/overviews/app/controllers/overviews/overviews_controller.rb +++ b/modules/overviews/app/controllers/overviews/overviews_controller.rb @@ -46,7 +46,7 @@ module ::Overviews def project_custom_field_section_dialog respond_with_dialog( - ProjectCustomFields::Sections::EditDialogComponent.new( + Overviews::ProjectCustomFields::EditDialogComponent.new( project: @project, project_custom_field_section: find_project_custom_field_section ) @@ -80,46 +80,6 @@ module ::Overviews render :project_life_cycles_sidebar, layout: false end - def project_life_cycles_dialog - respond_with_dialog( - ProjectLifeCycles::Sections::EditDialogComponent.new(@project) - ) - end - - def project_life_cycles_form - service_call = ::ProjectLifeCycleSteps::PreviewAttributesService - .new(user: current_user, - model: @project, - contract_class: ProjectLifeCycleSteps::UpdateContract) - .call(permitted_params.project_phases) - - update_via_turbo_stream( - component: ProjectLifeCycles::Sections::EditComponent.new(service_call.result), - method: "morph" - ) - # TODO: :unprocessable_entity is not nice, change the dialog logic to accept :ok - # without dismissing the dialog, alternatively use turbo frames instead of streams. - respond_to_with_turbo_streams(status: :unprocessable_entity) - end - - def update_project_life_cycles - service_call = ::ProjectLifeCycleSteps::UpdateService - .new(user: current_user, model: @project) - .call(permitted_params.project_phases) - - if service_call.success? - update_via_turbo_stream( - component: ProjectLifeCycles::SidePanelComponent.new(project: @project) - ) - else - update_via_turbo_stream( - component: ProjectLifeCycles::Sections::EditComponent.new(service_call.result) - ) - end - - respond_to_with_turbo_streams(status: service_call.success? ? :ok : :unprocessable_entity) - end - def jump_to_project_menu_item if params[:jump] # try to redirect to the requested menu item @@ -145,7 +105,7 @@ module ::Overviews def handle_errors(project_with_errors, section) update_via_turbo_stream( - component: ProjectCustomFields::Sections::EditComponent.new( + component: Overviews::ProjectCustomFields::EditComponent.new( project: project_with_errors, project_custom_field_section: section ) @@ -154,7 +114,7 @@ module ::Overviews def update_sidebar_component update_via_turbo_stream( - component: ProjectCustomFields::SidePanelComponent.new(project: @project) + component: Overviews::ProjectCustomFields::SidePanelComponent.new(project: @project) ) end end diff --git a/modules/overviews/app/controllers/overviews/project_phases_controller.rb b/modules/overviews/app/controllers/overviews/project_phases_controller.rb new file mode 100644 index 00000000000..908c0917d54 --- /dev/null +++ b/modules/overviews/app/controllers/overviews/project_phases_controller.rb @@ -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 ::Overviews + class ProjectPhasesController < ::ApplicationController + include OpTurbo::ComponentStream + include OpTurbo::DialogStreamHelper + + before_action :find_project_phase_and_project + before_action :authorize + + def edit + respond_with_dialog(Overviews::ProjectPhases::EditDialogComponent.new(@project_phase)) + end + + def preview + service_call = ::ProjectLifeCycleSteps::SetAttributesService + .new(user: current_user, + model: @project_phase, + contract_class: ProjectLifeCycleSteps::UpdateContract) + .call(permitted_params.project_phase) + + update_via_turbo_stream( + component: Overviews::ProjectPhases::EditComponent.new(service_call.result), + method: "morph" + ) + # TODO: :unprocessable_entity is not nice, change the dialog logic to accept :ok + # without dismissing the dialog, alternatively use turbo frames instead of streams. + respond_to_with_turbo_streams(status: :unprocessable_entity) + end + + def update + service_call = ::ProjectLifeCycleSteps::UpdateService + .new(user: current_user, model: @project_phase) + .call(permitted_params.project_phase) + + component, status = + if service_call.success? + [Overviews::ProjectPhases::SidePanelComponent.new(project: @project), :ok] + else + [Overviews::ProjectPhases::EditComponent.new(service_call.result), :unprocessable_entity] + end + + update_via_turbo_stream(component:) + respond_to_with_turbo_streams(status:) + end + + private + + def find_project_phase_and_project + @project_phase = Project::Phase.where(active: true) + .eager_load(:definition, :project) + .find(params[:id]) + @project = @project_phase.project + end + end +end diff --git a/modules/overviews/app/views/overviews/overviews/project_custom_fields_sidebar.html.erb b/modules/overviews/app/views/overviews/overviews/project_custom_fields_sidebar.html.erb index 63d2b46f856..b04c17adec2 100644 --- a/modules/overviews/app/views/overviews/overviews/project_custom_fields_sidebar.html.erb +++ b/modules/overviews/app/views/overviews/overviews/project_custom_fields_sidebar.html.erb @@ -27,5 +27,5 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= content_tag("turbo-frame", id: "project-custom-fields-sidebar") do %> - <%= render(ProjectCustomFields::SidePanelComponent.new(project: @project)) %> + <%= render(Overviews::ProjectCustomFields::SidePanelComponent.new(project: @project)) %> <% end %> diff --git a/modules/overviews/app/views/overviews/overviews/project_life_cycles_sidebar.html.erb b/modules/overviews/app/views/overviews/overviews/project_life_cycles_sidebar.html.erb index 1b05538bb3c..5821f23a127 100644 --- a/modules/overviews/app/views/overviews/overviews/project_life_cycles_sidebar.html.erb +++ b/modules/overviews/app/views/overviews/overviews/project_life_cycles_sidebar.html.erb @@ -27,5 +27,5 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= content_tag("turbo-frame", id: "project-life-cycles-sidebar") do %> - <%= render(ProjectLifeCycles::SidePanelComponent.new(project: @project)) %> + <%= render(Overviews::ProjectPhases::SidePanelComponent.new(project: @project)) %> <% end %> diff --git a/modules/overviews/config/routes.rb b/modules/overviews/config/routes.rb index c6a9a0b1b44..c800c2e7cdf 100644 --- a/modules/overviews/config/routes.rb +++ b/modules/overviews/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.routes.draw do constraints(project_id: Regexp.new("(?!(#{Project::RESERVED_IDENTIFIERS.join('|')})$)(\\w|-)+"), format: :html) do get "projects/:project_id", @@ -10,13 +12,13 @@ Rails.application.routes.draw do put "projects/:project_id/update_project_custom_values/:section_id", to: "overviews/overviews#update_project_custom_values", as: :update_project_custom_values - get "projects/:project_id/project_life_cycles_sidebar", to: "overviews/overviews#project_life_cycles_sidebar", - as: :project_life_cycles_sidebar - get "projects/:project_id/project_life_cycles_dialog", to: "overviews/overviews#project_life_cycles_dialog", - as: :project_life_cycles_dialog - put "projects/:project_id/project_life_cycles_form", to: "overviews/overviews#project_life_cycles_form", - as: :project_life_cycles_form - put "projects/:project_id/update_project_life_cycles", to: "overviews/overviews#update_project_life_cycles", - as: :update_project_life_cycles + get "projects/:project_id/project_life_cycles_sidebar", + to: "overviews/overviews#project_life_cycles_sidebar", as: :project_life_cycles_sidebar + end + + resources :project_phases, controller: "overviews/project_phases", only: %i[edit update] do + member do + put :preview + end end end diff --git a/modules/overviews/lib/overviews/engine.rb b/modules/overviews/lib/overviews/engine.rb index 3e73ddfebeb..fef3453e7cc 100644 --- a/modules/overviews/lib/overviews/engine.rb +++ b/modules/overviews/lib/overviews/engine.rb @@ -72,9 +72,9 @@ module Overviews OpenProject::AccessControl.permission(:edit_project_phases) .controller_actions .push( - "overviews/overviews/project_life_cycles_dialog", - "overviews/overviews/project_life_cycles_form", - "overviews/overviews/update_project_life_cycles" + "overviews/project_phases/edit", + "overviews/project_phases/preview", + "overviews/project_phases/update" ) OpenProject::AccessControl.permission(:view_work_packages) diff --git a/modules/reporting/config/locales/crowdin/ro.yml b/modules/reporting/config/locales/crowdin/ro.yml index 1c664d6edf7..459bfcc7f5a 100644 --- a/modules/reporting/config/locales/crowdin/ro.yml +++ b/modules/reporting/config/locales/crowdin/ro.yml @@ -70,7 +70,7 @@ ro: label_filter: "Filtrează" label_filter_add: "Adaugă filtru" label_filter_plural: "Filtre" - label_group_by: "Grupează după" + label_group_by: "Grupare după" label_group_by_add: "Adaugă atributul Grupează-după" label_inactive: "Inactiv" label_no: "Nu" diff --git a/modules/reporting/config/locales/crowdin/zh-TW.yml b/modules/reporting/config/locales/crowdin/zh-TW.yml index 160d6764df0..5885d7e25a2 100644 --- a/modules/reporting/config/locales/crowdin/zh-TW.yml +++ b/modules/reporting/config/locales/crowdin/zh-TW.yml @@ -53,7 +53,7 @@ zh-TW: label_money: "現金價值" label_month_reporting: "月" label_new_report: "新建成本報表" - label_open: "開啟" + label_open: "開啟中" label_operator: "操作員" label_private_report_plural: "私密成本報告" label_progress_bar_explanation: "產生報告中..." @@ -70,7 +70,7 @@ zh-TW: label_filter: "篩選條件" label_filter_add: "新增篩選條件" label_filter_plural: "篩選條件" - label_group_by: "分類" + label_group_by: "分組依據" label_group_by_add: "新增分組依據屬性" label_inactive: "«不活動»" label_no: "否" diff --git a/modules/reporting/spec/features/export_cost_report_spec.rb b/modules/reporting/spec/features/export_cost_report_spec.rb index a687fdd7cc4..09dc85507bf 100644 --- a/modules/reporting/spec/features/export_cost_report_spec.rb +++ b/modules/reporting/spec/features/export_cost_report_spec.rb @@ -31,7 +31,7 @@ require_relative "../spec_helper" require_relative "support/pages/cost_report_page" -RSpec.describe "Cost reports XLS export", :js, with_flag: { track_start_and_end_times_for_time_entries: true } do +RSpec.describe "Cost reports XLS export", :js do shared_let(:project) { create(:project) } shared_let(:user) { create(:admin) } shared_let(:cost_type) { create(:cost_type, name: "Post-war", unit: "cap", unit_plural: "caps") } diff --git a/modules/reporting/spec/features/time_entries_spec.rb b/modules/reporting/spec/features/time_entries_spec.rb index d7867e33d44..59cf2e54764 100644 --- a/modules/reporting/spec/features/time_entries_spec.rb +++ b/modules/reporting/spec/features/time_entries_spec.rb @@ -4,8 +4,7 @@ require "spec_helper" require_relative "support/pages/cost_report_page" require_relative "support/components/cost_reports_base_table" -RSpec.describe "Cost report showing time entries with start & end times", :js, - with_flag: { track_start_and_end_times_for_time_entries: true } do +RSpec.describe "Cost report showing time entries with start & end times", :js do shared_let(:project) { create(:project) } shared_let(:user) { create(:admin) } shared_let(:work_package) { create(:work_package, project:) } diff --git a/modules/overviews/app/components/project_life_cycles/sections/project_life_cycles/show_component.rb b/modules/storages/app/common/storages/peripherals/connection_validators/base_connection_validator.rb similarity index 61% rename from modules/overviews/app/components/project_life_cycles/sections/project_life_cycles/show_component.rb rename to modules/storages/app/common/storages/peripherals/connection_validators/base_connection_validator.rb index d1e6f09be73..7846ee10823 100644 --- a/modules/overviews/app/components/project_life_cycles/sections/project_life_cycles/show_component.rb +++ b/modules/storages/app/common/storages/peripherals/connection_validators/base_connection_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -26,30 +28,35 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ProjectLifeCycles - module Sections - module ProjectLifeCycles - class ShowComponent < ApplicationComponent - include ApplicationHelper - include OpPrimer::ComponentHelpers +module Storages + module Peripherals + module ConnectionValidators + class BaseConnectionValidator + class << self + def validation_groups + @validation_groups ||= {} + end + + def register_group(group_name, klass, precondition: ->(*) { true }) + validation_groups[group_name] = { klass:, precondition: } + end + end + + def initialize(storage) + @storage = storage + end + + def call + validation_groups.each_with_object(ValidatorResult.new) do |(key, group_metadata), result| + if group_metadata[:precondition].call(@storage, result) + result.add_group_result(key, group_metadata[:klass].call(@storage)) + end + end + end private - def not_set? - model.not_set? - end - - def icon - :"op-phase" - end - - def icon_color_class - helpers.hl_inline_class("project_phase_definition", model.definition) - end - - def text - model.name - end + def validation_groups = self.class.validation_groups end end end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb b/modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb index 36e5501170a..d6e6c904cfe 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb +++ b/modules/storages/app/common/storages/peripherals/connection_validators/base_validator_group.rb @@ -34,6 +34,10 @@ module Storages class BaseValidatorGroup include TaggedLogging + def self.call(storage) + new(storage).call + end + def initialize(storage) @storage = storage @results = ValidationGroupResult.new diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb index 511c53dceb0..07185f8b8f0 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb +++ b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud/authentication_validator.rb @@ -52,7 +52,7 @@ module Storages end def oauth_token - if OAuthClientToken.where(user: @user, oauth_client: @storage.oauth_client).any? + if OAuthClientToken.for_user_and_client(@user, @storage.oauth_client).exists? pass_check(:existing_token) else warn_check(:existing_token, message(:oauth_token_missing), halt_validation: true) diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb index 7c75def5754..20ec6c4e7ae 100644 --- a/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb +++ b/modules/storages/app/common/storages/peripherals/connection_validators/nextcloud_validator.rb @@ -31,41 +31,15 @@ module Storages module Peripherals module ConnectionValidators - class NextcloudValidator - # Class Level interface will be moved to a superclass/mixin once we do the OneDrive port - class << self - def validation_groups - @validation_groups ||= {} - end - - def register_group(group_name, klass, when: ->(*) { true }) - validation_groups[group_name] = { klass:, when: } - end - end + class NextcloudValidator < BaseConnectionValidator register_group :base_configuration, Nextcloud::StorageConfigurationValidator register_group :authentication, Nextcloud::AuthenticationValidator, - when: ->(_, result) { result.group(:base_configuration).non_failure? } + precondition: ->(_, result) { result.group(:base_configuration).non_failure? } register_group :ampf_configuration, Nextcloud::AmpfConfigurationValidator, - when: ->(storage, result) { + precondition: ->(storage, result) { result.group(:base_configuration).non_failure? && storage.automatic_management_enabled? } - - def initialize(storage:) - @storage = storage - end - - def validate - validation_groups.each_with_object(ValidatorResult.new) do |(key, group_metadata), result| - if group_metadata[:when].call(@storage, result) - result.add_group_result(key, group_metadata[:klass].new(@storage).call) - end - end - end - - private - - def validation_groups = self.class.validation_groups end end end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator.rb new file mode 100644 index 00000000000..d6448d73895 --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator.rb @@ -0,0 +1,104 @@ +# 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 Storages + module Peripherals + module ConnectionValidators + module OneDrive + class AmpfConfigurationValidator < BaseValidatorGroup + TEST_FOLDER_NAME = "ConnectionValidatorFolder" + + private + + def validate + register_checks :client_folder_creation, :client_folder_removal, :drive_contents + + client_permissions + unexpected_content + end + + def unexpected_content + unexpected_files = files_query.on_failure { fail_check(:drive_content, message(:unknown_error)) } + .result.files.reject { managed_project_folder_ids.include?(it.id) } + + if unexpected_files.empty? + pass_check(:drive_contents) + else + log_extraneous_files(unexpected_files) + warn_check(:drive_contents, message("one_drive.unexpected_content")) + end + end + + # Testing setting permissions and checking permission inheritance would be great + # but there are some challenges to it. We need to figure out a good way to go about this + # 2025-04-08 @mereghost + def client_permissions + folder = create_folder.result + delete_folder(folder) + end + + def delete_folder(folder) + Registry["one_drive.commands.delete_folder"] + .call(storage: @storage, auth_strategy:, location: folder.id) + .on_failure { fail_check(:client_folder_removal, message("one_drive.client_cant_delete_folder")) } + .on_success { pass_check(:client_folder_removal) } + end + + def create_folder + Registry["one_drive.commands.create_folder"] + .call(storage: @storage, auth_strategy:, folder_name: TEST_FOLDER_NAME, parent_location: ParentFolder.root) + .on_success { pass_check(:client_folder_creation) } + .on_failure do + reason = it.result == :already_exists ? :existing_test_folder : :client_write_permission_missing + + fail_check(:client_folder_creation, message("one_drive.#{reason}", folder_name: TEST_FOLDER_NAME)) + end + end + + def log_extraneous_files(unexpected_files) + file_representation = unexpected_files.map do |file| + "Name: #{file.name}, ID: #{file.id}, Location: #{file.location}" + end + + warn "Unexpected files/folder found in group folder:\n\t#{file_representation.join("\n\t")}" + end + + def managed_project_folder_ids + @managed_project_folder_ids ||= ProjectStorage.automatic.where(storage: @storage) + .pluck(:project_folder_id).to_set + end + + def files_query = Registry["one_drive.queries.files"].call(storage: @storage, auth_strategy:, folder: ParentFolder.root) + def auth_strategy = Registry["one_drive.authentication.userless"].call + end + end + end + end +end diff --git a/modules/meeting/lib/api/v3/meeting_contents/meeting_content_representer.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/authentication_validator.rb similarity index 52% rename from modules/meeting/lib/api/v3/meeting_contents/meeting_content_representer.rb rename to modules/storages/app/common/storages/peripherals/connection_validators/one_drive/authentication_validator.rb index 02d7341e290..17256dacaad 100644 --- a/modules/meeting/lib/api/v3/meeting_contents/meeting_content_representer.rb +++ b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/authentication_validator.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,30 +28,42 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module API - module V3 - module MeetingContents - class MeetingContentRepresenter < ::API::Decorators::Single - include API::Decorators::LinkedResource - include API::Caching::CachedRepresenter - include ::API::V3::Attachments::AttachableRepresenterMixin +module Storages + module Peripherals + module ConnectionValidators + module OneDrive + class AuthenticationValidator < BaseValidatorGroup + def initialize(storage) + super + @user = User.current + end - self_link title_getter: ->(*) {} + private - property :id + def validate + register_checks(:existing_token, :user_bound_request) - associated_resource :project, - link: ->(*) do - next unless represented.project.present? + oauth_token + user_bound_request + end - { - href: api_v3_paths.project(represented.project.id), - title: represented.project.name - } - end + def oauth_token + if OAuthClientToken.for_user_and_client(@user, @storage.oauth_client).exists? + pass_check(:existing_token) + else + warn_check(:existing_token, message(:oauth_token_missing), halt_validation: true) + end + end - def _type - "MeetingContent" + def user_bound_request + Registry["one_drive.queries.user"].call(storage: @storage, auth_strategy:).on_failure do + fail_check(:user_bound_request, message("oauth_request_#{it.result}")) + end + + pass_check(:user_bound_request) + end + + def auth_strategy = Registry["one_drive.authentication.user_bound"].call(storage: @storage, user: @user) end end end diff --git a/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator.rb new file mode 100644 index 00000000000..474f63d5b8e --- /dev/null +++ b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator.rb @@ -0,0 +1,141 @@ +# 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 Storages + module Peripherals + module ConnectionValidators + module OneDrive + class StorageConfigurationValidator < BaseValidatorGroup + using ServiceResultRefinements + + private + + def validate + register_checks :storage_configured, :diagnostic_request, :tenant_id, :client_secret, :client_id, + :drive_id_format, :drive_id_not_found + + storage_configuration_status + diagnostic_request + check_tenant_id + check_client_secret + check_client_id + malformed_drive_id + drive_not_found + end + + def malformed_drive_id + return pass_check(:drive_id_format) if query_result.success? + + if error_payload.dig(:error, :code) == "invalidRequest" + fail_check(:drive_id_format, message("one_drive.drive_id_wrong")) + else + pass_check(:drive_id_format) + end + end + + def drive_not_found + if query_result.result == :not_found + fail_check(:drive_id_not_found, message("one_drive.drive_id_not_found")) + else + pass_check(:drive_id_not_found) + end + end + + def check_tenant_id + return pass_check(:tenant_id) if query_result.success? + + tenant_id_regex = /tenant (?:identifier )?'#{@storage.tenant_id}' (?:not found|is neither)/i + + if error_payload[:error] == "invalid_request" && error_payload[:error_description].match?(tenant_id_regex) + fail_check(:tenant_id, message(:tenant_id_wrong)) + else + pass_check(:tenant_id) + end + end + + def check_client_id + return pass_check(:client_id) if query_result.success? + + if error_payload[:error] == "unauthorized_client" + fail_check(:client_id, message(:client_id_wrong)) + else + pass_check(:client_id) + end + end + + def check_client_secret + return pass_check(:client_secret) if query_result.success? + + if error_payload[:error] == "invalid_client" + fail_check(:client_secret, message(:client_secret_wrong)) + else + pass_check(:client_secret) + end + end + + def diagnostic_request + if query_result.result == :error + error "Connection validation failed with unknown error:\n" \ + "\tstorage: ##{@storage.id} #{@storage.name}\n" \ + "\tstatus: #{query_result.result}\n" \ + "\tresponse: #{query_result.error_payload}" + + fail_check(:diagnostic_request, message(:unknown_error)) + else + pass_check :diagnostic_request + end + end + + def storage_configuration_status + if @storage.configured? + pass_check(:storage_configured) + else + fail_check(:storage_configured, message(:not_configured)) + end + end + + def query_result + @query_result ||= Registry.resolve("#{@storage}.queries.files") + .call(storage: @storage, auth_strategy:, folder: ParentFolder.root) + end + + def auth_strategy = Registry.resolve("one_drive.authentication.userless").call + + def error_payload + return {} if query_result.success? + return query_result.error_payload if query_result.error_payload.is_a?(Hash) + + @error_payload ||= MultiJson.load(query_result.error_payload, symbolize_keys: true) + end + end + end + end + end +end diff --git a/modules/meeting/app/services/meeting_contents/set_attributes_service.rb b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive_validator.rb similarity index 65% rename from modules/meeting/app/services/meeting_contents/set_attributes_service.rb rename to modules/storages/app/common/storages/peripherals/connection_validators/one_drive_validator.rb index 67fbe5cddc2..c280bae9085 100644 --- a/modules/meeting/app/services/meeting_contents/set_attributes_service.rb +++ b/modules/storages/app/common/storages/peripherals/connection_validators/one_drive_validator.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,13 +28,17 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module MeetingContents - class SetAttributesService < ::BaseServices::SetAttributes - include Attachments::SetReplacements - - def set_default_attributes(_params) - model.change_by_system do - model.author = user if model.author.nil? +module Storages + module Peripherals + module ConnectionValidators + class OneDriveValidator < BaseConnectionValidator + register_group :base_configuration, OneDrive::StorageConfigurationValidator + register_group :authentication, OneDrive::AuthenticationValidator, + precondition: ->(_, result) { result.group(:base_configuration).non_failure? } + register_group :ampf_configuration, OneDrive::AmpfConfigurationValidator, + precondition: ->(storage, result) { + result.group(:base_configuration).non_failure? && storage.automatic_management_enabled? + } end end end diff --git a/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb b/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb index b03004d6b96..4ed520a72cd 100644 --- a/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb +++ b/modules/storages/app/common/storages/peripherals/nextcloud_registry.rb @@ -59,32 +59,36 @@ module Storages namespace("components") do namespace("forms") do - register(:automatically_managed_folders, ::Storages::Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent) - register(:general_information, ::Storages::Admin::Forms::GeneralInfoFormComponent) - register(:storage_audience, ::Storages::Admin::Forms::StorageAudienceFormComponent) - register(:oauth_application, ::Storages::Admin::OAuthApplicationInfoCopyComponent) - register(:oauth_client, ::Storages::Admin::Forms::OAuthClientFormComponent) + register(:automatically_managed_folders, Admin::Forms::AutomaticallyManagedProjectFoldersFormComponent) + register(:general_information, Admin::Forms::GeneralInfoFormComponent) + register(:storage_audience, Admin::Forms::StorageAudienceFormComponent) + register(:oauth_application, Admin::OAuthApplicationInfoCopyComponent) + register(:oauth_client, Admin::Forms::OAuthClientFormComponent) end register(:setup_wizard, NextcloudStorageWizard) - register(:automatically_managed_folders, ::Storages::Admin::AutomaticallyManagedProjectFoldersInfoComponent) - register(:general_information, ::Storages::Admin::GeneralInfoComponent) - register(:storage_audience, ::Storages::Admin::StorageAudienceInfoComponent) - register(:oauth_application, ::Storages::Admin::OAuthApplicationInfoComponent) - register(:oauth_client, ::Storages::Admin::OAuthClientInfoComponent) + register(:automatically_managed_folders, Admin::AutomaticallyManagedProjectFoldersInfoComponent) + register(:general_information, Admin::GeneralInfoComponent) + register(:storage_audience, Admin::StorageAudienceInfoComponent) + register(:oauth_application, Admin::OAuthApplicationInfoComponent) + register(:oauth_client, Admin::OAuthClientInfoComponent) end namespace("contracts") do - register(:storage, ::Storages::Storages::NextcloudContract) - register(:general_information, ::Storages::Storages::NextcloudGeneralInformationContract) - register(:storage_audience, ::Storages::Storages::NextcloudAudienceContract) + register(:storage, Storages::NextcloudContract) + register(:general_information, Storages::NextcloudGeneralInformationContract) + register(:storage_audience, Storages::NextcloudAudienceContract) end namespace("models") do register(:managed_folder_identifier, ManagedFolderIdentifier::Nextcloud) end + namespace("validators") do + register(:connection, ConnectionValidators::NextcloudValidator) + end + namespace("authentication") do register(:userless, StorageInteraction::AuthenticationStrategies::NextcloudStrategies::UserLess, call: false) register(:user_bound, StorageInteraction::AuthenticationStrategies::NextcloudStrategies::UserBound) diff --git a/modules/storages/app/common/storages/peripherals/one_drive_registry.rb b/modules/storages/app/common/storages/peripherals/one_drive_registry.rb index 3e561d31c45..c50074e3109 100644 --- a/modules/storages/app/common/storages/peripherals/one_drive_registry.rb +++ b/modules/storages/app/common/storages/peripherals/one_drive_registry.rb @@ -53,29 +53,33 @@ module Storages namespace("components") do namespace("forms") do - register(:access_management, ::Storages::Admin::Forms::AccessManagementFormComponent) - register(:general_information, ::Storages::Admin::Forms::GeneralInfoFormComponent) - register(:oauth_client, ::Storages::Admin::Forms::OAuthClientFormComponent) - register(:redirect_uri, ::Storages::Admin::Forms::RedirectUriFormComponent) + register(:access_management, Admin::Forms::AccessManagementFormComponent) + register(:general_information, Admin::Forms::GeneralInfoFormComponent) + register(:oauth_client, Admin::Forms::OAuthClientFormComponent) + register(:redirect_uri, Admin::Forms::RedirectUriFormComponent) end register(:setup_wizard, OneDriveStorageWizard) - register(:access_management, ::Storages::Admin::AccessManagementComponent) - register(:general_information, ::Storages::Admin::GeneralInfoComponent) - register(:oauth_client, ::Storages::Admin::OAuthClientInfoComponent) - register(:redirect_uri, ::Storages::Admin::RedirectUriComponent) + register(:access_management, Admin::AccessManagementComponent) + register(:general_information, Admin::GeneralInfoComponent) + register(:oauth_client, Admin::OAuthClientInfoComponent) + register(:redirect_uri, Admin::RedirectUriComponent) end namespace("contracts") do - register(:storage, ::Storages::Storages::OneDriveContract) - register(:general_information, ::Storages::Storages::OneDriveContract) + register(:storage, Storages::OneDriveContract) + register(:general_information, Storages::OneDriveContract) end namespace("models") do register(:managed_folder_identifier, ManagedFolderIdentifier::OneDrive) end + namespace("validations") do + register(:connection, ConnectionValidators::OneDriveValidator) + end + namespace("authentication") do register(:userless, StorageInteraction::AuthenticationStrategies::OneDriveStrategies::UserLess, call: false) register(:user_bound, StorageInteraction::AuthenticationStrategies::OneDriveStrategies::UserBound) diff --git a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb index fe8098ad1bf..0e2680a4758 100644 --- a/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb +++ b/modules/storages/app/common/storages/peripherals/storage_interaction/authentication_strategies/strategy.rb @@ -56,6 +56,14 @@ module Storages @token = token self end + + def ==(other) + @key == other.key && @use_cache == other.use_cache && @user == other.user && @token == other.token + end + + def hash + [@key, @use_cache, @user, @token].hash + end end end end diff --git a/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb b/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb index a5fe0f728cf..c8b2d8581ba 100644 --- a/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb +++ b/modules/storages/app/common/storages/peripherals/storage_parent_folder_extractor.rb @@ -33,6 +33,8 @@ module Storages ParentFolder = Data.define(:path) do delegate :split, :empty?, to: :path + def self.root = new("/") + def root? path == "/" end diff --git a/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb index fd3e5c3201e..5c14bfa3dde 100644 --- a/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb +++ b/modules/storages/app/components/storages/admin/storages/projects_storage_form_modal_component.html.erb @@ -85,8 +85,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 diff --git a/modules/storages/app/components/storages/open_project_storage_modal_component.html.erb b/modules/storages/app/components/storages/open_project_storage_modal_component.html.erb index 51001781bd3..6ca4884f067 100644 --- a/modules/storages/app/components/storages/open_project_storage_modal_component.html.erb +++ b/modules/storages/app/components/storages/open_project_storage_modal_component.html.erb @@ -10,6 +10,6 @@ <%= render(self.class::Body.new(@state)) %> <% end %> <% dialog.with_footer do %> - <%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": self.class.dialog_id })) { I18n.t("button_close") } %> + <%= render(Primer::Beta::Button.new(data: { "close-dialog-id": self.class.dialog_id })) { I18n.t("button_close") } %> <% end %> <% end %> diff --git a/modules/storages/app/views/storages/admin/storages/upsale.html.erb b/modules/storages/app/views/storages/admin/storages/upsale.html.erb index 6f3fcba3990..aae18ca0597 100644 --- a/modules/storages/app/views/storages/admin/storages/upsale.html.erb +++ b/modules/storages/app/views/storages/admin/storages/upsale.html.erb @@ -1,9 +1,5 @@ <% html_title(t(:label_administration), t("storages.upsale.title")) -%> -<%= render template: "common/upsale", - locals: { - feature_title: t("storages.upsale.title"), - feature_description: t("storages.upsale.description"), - feature_reference: "one_drive_sharepoint_file_storage", - feature_video: "enterprise/one_drive_sharepoint_integration.mp4" - } %> +<%= render EnterpriseEdition::UpsalePageComponent.new(:one_drive_sharepoint_file_storage, + video: "enterprise/one_drive_sharepoint_integration.mp4", + i18n_scope: "storages.upsale") %> diff --git a/modules/storages/config/locales/crowdin/af.yml b/modules/storages/config/locales/crowdin/af.yml index affc8044625..cff880c5870 100644 --- a/modules/storages/config/locales/crowdin/af.yml +++ b/modules/storages/config/locales/crowdin/af.yml @@ -250,6 +250,13 @@ af: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ar.yml b/modules/storages/config/locales/crowdin/ar.yml index ad77ff4006b..3b0abd7c58f 100644 --- a/modules/storages/config/locales/crowdin/ar.yml +++ b/modules/storages/config/locales/crowdin/ar.yml @@ -250,6 +250,13 @@ ar: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/az.yml b/modules/storages/config/locales/crowdin/az.yml index 4a6f95db32e..5ca6f25ab83 100644 --- a/modules/storages/config/locales/crowdin/az.yml +++ b/modules/storages/config/locales/crowdin/az.yml @@ -250,6 +250,13 @@ az: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/be.yml b/modules/storages/config/locales/crowdin/be.yml index efbde5f153b..d5e94a73c9c 100644 --- a/modules/storages/config/locales/crowdin/be.yml +++ b/modules/storages/config/locales/crowdin/be.yml @@ -250,6 +250,13 @@ be: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/bg.yml b/modules/storages/config/locales/crowdin/bg.yml index 4d0a1549662..804df4dc699 100644 --- a/modules/storages/config/locales/crowdin/bg.yml +++ b/modules/storages/config/locales/crowdin/bg.yml @@ -250,6 +250,13 @@ bg: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ca.yml b/modules/storages/config/locales/crowdin/ca.yml index 82c2c12d651..9e3885a79fc 100644 --- a/modules/storages/config/locales/crowdin/ca.yml +++ b/modules/storages/config/locales/crowdin/ca.yml @@ -250,6 +250,13 @@ ca: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ckb-IR.yml b/modules/storages/config/locales/crowdin/ckb-IR.yml index d0bae34d347..f68533f92d8 100644 --- a/modules/storages/config/locales/crowdin/ckb-IR.yml +++ b/modules/storages/config/locales/crowdin/ckb-IR.yml @@ -250,6 +250,13 @@ ckb-IR: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/cs.yml b/modules/storages/config/locales/crowdin/cs.yml index 1f41a301bc6..99c932d88a4 100644 --- a/modules/storages/config/locales/crowdin/cs.yml +++ b/modules/storages/config/locales/crowdin/cs.yml @@ -250,6 +250,13 @@ cs: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Zkontrolujte připojení k serveru. subtitle: Ověřování připojení tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/da.yml b/modules/storages/config/locales/crowdin/da.yml index eda61b3a2fc..8816ab4f0d7 100644 --- a/modules/storages/config/locales/crowdin/da.yml +++ b/modules/storages/config/locales/crowdin/da.yml @@ -250,6 +250,13 @@ da: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/de.yml b/modules/storages/config/locales/crowdin/de.yml index 07143edfcff..d7aa2bcd96f 100644 --- a/modules/storages/config/locales/crowdin/de.yml +++ b/modules/storages/config/locales/crowdin/de.yml @@ -173,7 +173,7 @@ de: storages: buttons: complete_without_setup: Ohne fortfahren - done_complete_setup: Fertig, Einrichtung abschließen + done_complete_setup: Fertig, Einrichtung abgeschlossen done_continue: Fertig, Fortfahren open_storage: Dateispeicher öffnen replace_oauth_application: OpenProject OAuth ersetzen @@ -250,6 +250,13 @@ de: oidc_cant_refresh_token: Beim Versuch, Ihren Zugriff auf den Speicher zu überprüfen, ist ein Fehler aufgetreten. Bitte überprüfen Sie die Server-Protokolle auf weitere Informationen. oidc_non_oidc_user: Der aktuelle Benutzer wurde zwar bereitgestellt, aber nicht von einem OpenID Connect (OIDC) Identity Provider. Bitte wiederholen Sie die Prüfung mit einem OIDC-bereitgestellten Benutzer. oidc_non_provisioned_user: Der aktuelle Benutzer wird nicht von einem OpenID Connect Identity Provider bereitgestellt. Bitte führen Sie die Prüfung mit einem bereitgestellten Benutzer erneut durch. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Überprüfen Sie Ihre Verbindung mit dem Server. subtitle: Verbindungsvalidierung tenant_id_wrong: Die konfigurierte Verzeichnis-(Mandanten-)ID ist ungültig. Bitte überprüfen Sie die Konfiguration. diff --git a/modules/storages/config/locales/crowdin/el.yml b/modules/storages/config/locales/crowdin/el.yml index 4356623a5b5..fe79415d2e0 100644 --- a/modules/storages/config/locales/crowdin/el.yml +++ b/modules/storages/config/locales/crowdin/el.yml @@ -250,6 +250,13 @@ el: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/eo.yml b/modules/storages/config/locales/crowdin/eo.yml index 8a174430235..981c11b4e06 100644 --- a/modules/storages/config/locales/crowdin/eo.yml +++ b/modules/storages/config/locales/crowdin/eo.yml @@ -250,6 +250,13 @@ eo: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/es.yml b/modules/storages/config/locales/crowdin/es.yml index fb4707e88d5..caf0b8a626d 100644 --- a/modules/storages/config/locales/crowdin/es.yml +++ b/modules/storages/config/locales/crowdin/es.yml @@ -250,6 +250,13 @@ es: oidc_cant_refresh_token: Se ha producido un error al intentar comprobar su acceso al almacenamiento. Compruebe los registros del servidor para más información. oidc_non_oidc_user: El usuario actual, aunque está aprovisionado, no fue aprovisionado por un proveedor de identidad OpenID Connect (OIDC). Vuelva a ejecutar la comprobación con un usuario aprovisionado por OIDC. oidc_non_provisioned_user: El usuario actual no ha sido proporcionado por un proveedor de identidad OpenID Connect. Vuelva a ejecutar la comprobación con un usuario proporcionado. + one_drive: + client_cant_delete_folder: El cliente tiene problemas para eliminar carpetas. Consulte la documentación de configuración de su almacenamiento. + client_write_permission_missing: Parece que el cliente no tiene permisos de escritura. Consulte la documentación de configuración de su almacenamiento. + drive_id_not_found: No se ha podido encontrar el identificador de unidad configurado. Compruebe la configuración. + drive_id_wrong: El ID de unidad configurada no parece válido. Compruebe la configuración. + existing_test_folder: La carpeta %{folder_name} necesaria para la prueba ya existe. Elimínela e inténtelo de nuevo. + unexpected_content: Contenido inesperado encontrado dentro la unidad. placeholder: Compruebe su conexión con el servidor. subtitle: Validación de conexión tenant_id_wrong: El id de directorio (tenant) configurado no es válido. Por favor, compruebe la configuración. diff --git a/modules/storages/config/locales/crowdin/et.yml b/modules/storages/config/locales/crowdin/et.yml index a8eff498651..879daa0a8b1 100644 --- a/modules/storages/config/locales/crowdin/et.yml +++ b/modules/storages/config/locales/crowdin/et.yml @@ -250,6 +250,13 @@ et: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/eu.yml b/modules/storages/config/locales/crowdin/eu.yml index 35efbe5a5bf..11ba1e21419 100644 --- a/modules/storages/config/locales/crowdin/eu.yml +++ b/modules/storages/config/locales/crowdin/eu.yml @@ -250,6 +250,13 @@ eu: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/fa.yml b/modules/storages/config/locales/crowdin/fa.yml index 4d12459494a..dc94fc0d0f3 100644 --- a/modules/storages/config/locales/crowdin/fa.yml +++ b/modules/storages/config/locales/crowdin/fa.yml @@ -250,6 +250,13 @@ fa: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/fi.yml b/modules/storages/config/locales/crowdin/fi.yml index ac10e5301c0..6803acdf56f 100644 --- a/modules/storages/config/locales/crowdin/fi.yml +++ b/modules/storages/config/locales/crowdin/fi.yml @@ -250,6 +250,13 @@ fi: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/fil.yml b/modules/storages/config/locales/crowdin/fil.yml index d1d03acc613..9e739e7dfb0 100644 --- a/modules/storages/config/locales/crowdin/fil.yml +++ b/modules/storages/config/locales/crowdin/fil.yml @@ -250,6 +250,13 @@ fil: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/fr.yml b/modules/storages/config/locales/crowdin/fr.yml index 65bf4c513a0..2901753210c 100644 --- a/modules/storages/config/locales/crowdin/fr.yml +++ b/modules/storages/config/locales/crowdin/fr.yml @@ -250,6 +250,13 @@ fr: oidc_cant_refresh_token: Une erreur s'est produite lors de la vérification de votre accès au stockage. Veuillez consulter les journaux du serveur pour obtenir plus d'informations. oidc_non_oidc_user: L'utilisateur actuel, même s'il est provisionné, n'a pas été provisionné par un fournisseur d'identité OpenID Connect (OIDC). Veuillez refaire la vérification avec un utilisateur OIDC provisionné. oidc_non_provisioned_user: L'utilisateur actuel n'est pas fourni par un fournisseur d'identité OpenID Connect. Veuillez réexécuter la vérification avec un utilisateur fourni. + one_drive: + client_cant_delete_folder: Le client rencontre des difficultés pour supprimer des dossiers. Veuillez consulter la documentation d'installation de votre espace de stockage. + client_write_permission_missing: Le client semble ne pas avoir les autorisations d'écriture. Veuillez vérifier la documentation d'installation de votre espace de stockage. + drive_id_not_found: L'identifiant du lecteur configuré est introuvable. Veuillez vérifier la configuration. + drive_id_wrong: L'identifiant du lecteur configuré ne semble pas valide. Veuillez vérifier la configuration. + existing_test_folder: Le dossier %{folder_name} requis pour le test existe déjà. Veuillez le supprimer et réessayer. + unexpected_content: Contenu inattendu trouvé dans le lecteur. placeholder: Vérifiez votre connexion avec le serveur. subtitle: Validation de la connexion tenant_id_wrong: L'identifiant du répertoire (locataire) configuré n'est pas valide. Veuillez vérifier la configuration. diff --git a/modules/storages/config/locales/crowdin/he.yml b/modules/storages/config/locales/crowdin/he.yml index 94032c41f32..02ff4de0c7e 100644 --- a/modules/storages/config/locales/crowdin/he.yml +++ b/modules/storages/config/locales/crowdin/he.yml @@ -250,6 +250,13 @@ he: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/hi.yml b/modules/storages/config/locales/crowdin/hi.yml index 053c2d2023c..c143e0f6e20 100644 --- a/modules/storages/config/locales/crowdin/hi.yml +++ b/modules/storages/config/locales/crowdin/hi.yml @@ -250,6 +250,13 @@ hi: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/hr.yml b/modules/storages/config/locales/crowdin/hr.yml index 2ec561d55fc..b470b37a46a 100644 --- a/modules/storages/config/locales/crowdin/hr.yml +++ b/modules/storages/config/locales/crowdin/hr.yml @@ -250,6 +250,13 @@ hr: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/hu.yml b/modules/storages/config/locales/crowdin/hu.yml index 3d6a874455c..149ac6fa058 100644 --- a/modules/storages/config/locales/crowdin/hu.yml +++ b/modules/storages/config/locales/crowdin/hu.yml @@ -250,6 +250,13 @@ hu: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/id.yml b/modules/storages/config/locales/crowdin/id.yml index d51bc3fe4fd..ffe1646dbc3 100644 --- a/modules/storages/config/locales/crowdin/id.yml +++ b/modules/storages/config/locales/crowdin/id.yml @@ -250,6 +250,13 @@ id: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/it.yml b/modules/storages/config/locales/crowdin/it.yml index a1eb56ae852..c1fb8b66b3d 100644 --- a/modules/storages/config/locales/crowdin/it.yml +++ b/modules/storages/config/locales/crowdin/it.yml @@ -250,6 +250,13 @@ it: oidc_cant_refresh_token: Si è verificato un errore durante il tentativo di verificare il tuo accesso all'archiviazione. Controlla i log del server per ulteriori informazioni. oidc_non_oidc_user: L'utente attuale, pur essendo stato registrato, non è stato fornito da un provider di identità OpenID Connect (OIDC). Esegui nuovamente il controllo con un utente fornito da OIDC. oidc_non_provisioned_user: L'utente attuale non è fornito da un provider di identità OpenID Connect. Esegui nuovamente il controllo con un utente fornito. + one_drive: + client_cant_delete_folder: Il cliente riscontra problemi con l'eliminazione delle cartelle. Consulta la documentazione di configurazione del tuo archivio. + client_write_permission_missing: Il client non ha i permessi di scrittura necessari. Consulta la documentazione di configurazione del tuo archivio. + drive_id_not_found: Impossibile trovare l'ID dell'archivio configurato. Controlla la configurazione. + drive_id_wrong: L'ID dell'archivio configurato non è valido. Controlla la configurazione. + existing_test_folder: La cartella %{folder_name} necessaria per il test esiste già. Eliminala e riprova. + unexpected_content: Contenuto inatteso nello spazio di archiviazione. placeholder: Verifica la tua connessione con il server. subtitle: Verifica della connessione tenant_id_wrong: L'ID della directory non è valido. Verifica la configurazione. diff --git a/modules/storages/config/locales/crowdin/ja.yml b/modules/storages/config/locales/crowdin/ja.yml index cd56291b2f3..f71ac01c1c6 100644 --- a/modules/storages/config/locales/crowdin/ja.yml +++ b/modules/storages/config/locales/crowdin/ja.yml @@ -250,6 +250,13 @@ ja: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ka.yml b/modules/storages/config/locales/crowdin/ka.yml index 453f5d29dec..f7161f9281b 100644 --- a/modules/storages/config/locales/crowdin/ka.yml +++ b/modules/storages/config/locales/crowdin/ka.yml @@ -250,6 +250,13 @@ ka: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/kk.yml b/modules/storages/config/locales/crowdin/kk.yml index 1f757bedde2..ecdc00e4ef9 100644 --- a/modules/storages/config/locales/crowdin/kk.yml +++ b/modules/storages/config/locales/crowdin/kk.yml @@ -250,6 +250,13 @@ kk: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ko.yml b/modules/storages/config/locales/crowdin/ko.yml index 03f61861d02..6a6651cbfea 100644 --- a/modules/storages/config/locales/crowdin/ko.yml +++ b/modules/storages/config/locales/crowdin/ko.yml @@ -250,6 +250,13 @@ ko: oidc_cant_refresh_token: 저장소에 대한 액세스를 확인하는 동안 오류가 발생했습니다. 자세한 내용은 서버 로그를 확인하세요. oidc_non_oidc_user: 현재 사용자는 프로비저닝되는 동안, OpenID Connect(OIDC) ID 공급자에 의해 프로비저닝되지 않았습니다. OIDC 프로비저닝 사용자로 확인을 다시 실행하세요. oidc_non_provisioned_user: 현재 사용자는 OpenID Connect ID 공급자에 의해 제공되지 않았습니다. 제공된 사용자로 확인을 다시 실행하세요. + one_drive: + client_cant_delete_folder: 클라이언트에서 폴더를 삭제하는 중에 문제가 발생했습니다. 저장소에 대한 설정 설명서를 확인하세요. + client_write_permission_missing: 클라이언트에 쓰기 권한이 없는 것 같습니다. 저장소에 대한 설정 설명서를 확인하세요. + drive_id_not_found: 구성된 드라이브 ID를 찾을 수 없습니다. 구성을 확인하세요. + drive_id_wrong: 구성된 드라이브 ID가 유효하지 않은 것 같습니다. 구성을 확인하세요. + existing_test_folder: 테스트에 필요한 %{folder_name} 폴더가 이미 있습니다. 이 폴더를 삭제한 후에 다시 시도하세요. + unexpected_content: 드라이브에서 예기치 않은 콘텐츠가 발견되었습니다. placeholder: 서버에 대한 연결을 확인하세요. subtitle: 연결 유효성 검사 tenant_id_wrong: 구성된 디렉터리(테넌트) ID가 잘못되었습니다. 구성을 확인하세요. diff --git a/modules/storages/config/locales/crowdin/lt.yml b/modules/storages/config/locales/crowdin/lt.yml index f0c5298aa46..673d67dd105 100644 --- a/modules/storages/config/locales/crowdin/lt.yml +++ b/modules/storages/config/locales/crowdin/lt.yml @@ -250,6 +250,13 @@ lt: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/lv.yml b/modules/storages/config/locales/crowdin/lv.yml index e08358d0569..181ed849083 100644 --- a/modules/storages/config/locales/crowdin/lv.yml +++ b/modules/storages/config/locales/crowdin/lv.yml @@ -250,6 +250,13 @@ lv: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/mn.yml b/modules/storages/config/locales/crowdin/mn.yml index 03478cb6383..3e41f104bd6 100644 --- a/modules/storages/config/locales/crowdin/mn.yml +++ b/modules/storages/config/locales/crowdin/mn.yml @@ -250,6 +250,13 @@ mn: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ms.yml b/modules/storages/config/locales/crowdin/ms.yml index 0f55e2e2b8b..a22e011fca3 100644 --- a/modules/storages/config/locales/crowdin/ms.yml +++ b/modules/storages/config/locales/crowdin/ms.yml @@ -250,6 +250,13 @@ ms: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Semak sambungan anda terhadap pelayan. subtitle: Pengesahan sambungan tenant_id_wrong: Id direktori (penyewa) yang dikonfigurasikan adalah tidak sah. Sila semak konfigurasi. diff --git a/modules/storages/config/locales/crowdin/ne.yml b/modules/storages/config/locales/crowdin/ne.yml index f004121097a..2f9f4adf6c7 100644 --- a/modules/storages/config/locales/crowdin/ne.yml +++ b/modules/storages/config/locales/crowdin/ne.yml @@ -250,6 +250,13 @@ ne: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/nl.yml b/modules/storages/config/locales/crowdin/nl.yml index 22b19df5dc4..1c09fdb400d 100644 --- a/modules/storages/config/locales/crowdin/nl.yml +++ b/modules/storages/config/locales/crowdin/nl.yml @@ -250,6 +250,13 @@ nl: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Controleer uw verbinding met de server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/no.yml b/modules/storages/config/locales/crowdin/no.yml index c465aea61ad..fb45924f8d9 100644 --- a/modules/storages/config/locales/crowdin/no.yml +++ b/modules/storages/config/locales/crowdin/no.yml @@ -250,6 +250,13 @@ oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/pl.yml b/modules/storages/config/locales/crowdin/pl.yml index 9264fea76a7..c8cfab67b8c 100644 --- a/modules/storages/config/locales/crowdin/pl.yml +++ b/modules/storages/config/locales/crowdin/pl.yml @@ -250,6 +250,13 @@ pl: oidc_cant_refresh_token: Podczas próby sprawdzenia Twojego dostępu do magazynu wystąpił błąd. Aby uzyskać dalsze informacje, sprawdź dzienniki serwera. oidc_non_oidc_user: Bieżący użytkownik, choć otrzymał konfigurację, nie otrzymał konfiguracji od dostawcy tożsamości OpenID Connect (OIDC). Przeprowadź ponowne sprawdzenie z użytkownikiem, który otrzymał konfigurację od OIDC. oidc_non_provisioned_user: Bieżący użytkownik nie jest dostarczany przez dostawcę tożsamości OpenID Connect. Ponownie uruchom sprawdzanie z dostarczonym użytkownikiem. + one_drive: + client_cant_delete_folder: Klient ma problemy z usunięciem folderów. Sprawdź dokumentację konfiguracji swojej pamięci masowej. + client_write_permission_missing: Klient nie ma uprawnień zapisu. Sprawdź dokumentację konfiguracji swojej pamięci masowej. + drive_id_not_found: Nie można znaleźć skonfigurowanego Drive ID. Sprawdź konfigurację. + drive_id_wrong: Skonfigurowany Drive ID jest nieprawidłowy. Sprawdź konfigurację. + existing_test_folder: Wymagany do testowania folder %{folder_name} już istnieje. Usuń go i spróbuj ponownie. + unexpected_content: Na dysku znaleziono nieoczekiwaną zawartość. placeholder: Sprawdź połączenie z serwerem. subtitle: Weryfikacja połączenia tenant_id_wrong: Skonfigurowany katalog (dzierżawca) jest nieprawidłowy. Sprawdź konfigurację. diff --git a/modules/storages/config/locales/crowdin/pt-BR.yml b/modules/storages/config/locales/crowdin/pt-BR.yml index a1f7b7e7623..8709bc3d0a2 100644 --- a/modules/storages/config/locales/crowdin/pt-BR.yml +++ b/modules/storages/config/locales/crowdin/pt-BR.yml @@ -250,6 +250,13 @@ pt-BR: oidc_cant_refresh_token: Ocorreu um erro ao tentar verificar seu acesso ao armazenamento. Verifique os logs do servidor para mais informações. oidc_non_oidc_user: O usuário atual, embora provisionado, não foi provisionado por um Provedor de Identidade OpenID Connect (OIDC). Execute a verificação novamente com um usuário provisionado pelo OIDC. oidc_non_provisioned_user: O usuário atual não foi fornecido por um Provedor de Identidade OpenID Connect. Execute a verificação novamente com um usuário fornecido. + one_drive: + client_cant_delete_folder: O cliente está com dificuldades para excluir pastas. Verifique a documentação de configuração do seu armazenamento. + client_write_permission_missing: O cliente parece não ter permissão para gravar. Verifique a documentação de configuração do armazenamento. + drive_id_not_found: O drive Id configurado não foi encontrado. Verifique a configuração. + drive_id_wrong: O Id da unidade configurada parece ser inválido. Verifique a configuração. + existing_test_folder: A pasta %{folder_name}, necessária para o teste, já existe. Exclua-a e tente novamente. + unexpected_content: Conteúdo inesperado encontrado na unidade. placeholder: Verifique sua conexão com o servidor. subtitle: Validação de conexão tenant_id_wrong: O ID do diretório (inquilino) configurado é inválido. Verifique a configuração. diff --git a/modules/storages/config/locales/crowdin/pt-PT.yml b/modules/storages/config/locales/crowdin/pt-PT.yml index 31696e96abe..9d215059245 100644 --- a/modules/storages/config/locales/crowdin/pt-PT.yml +++ b/modules/storages/config/locales/crowdin/pt-PT.yml @@ -250,6 +250,13 @@ pt-PT: oidc_cant_refresh_token: Ocorreu um erro ao tentar verificar o seu acesso ao armazenamento. Consulte os registos do servidor para obter mais informações. oidc_non_oidc_user: O utilizador atual, embora aprovisionado, não foi aprovisionado por um fornecedor de identidade OpenID Connect (OIDC). Volte a executar a verificação com um utilizador aprovisionado pelo OIDC. oidc_non_provisioned_user: O utilizador atual não é fornecido por um fornecedor de identidade OpenID Connect. Volte a executar a verificação com um utilizador fornecido. + one_drive: + client_cant_delete_folder: O cliente está a ter problemas a eliminar pastas. Consulte a documentação de configuração para o seu armazenamento. + client_write_permission_missing: O cliente parece estár ter permissões de escrita em falta. Consulte a documentação de configuração para o seu armazenamento. + drive_id_not_found: Não foi possível encontrar o Drive ID configurado. Verifique a configuração. + drive_id_wrong: O Drive ID configurado parece ser inválido. Verifique a configuração. + existing_test_folder: A pasta %{folder_name} necessária para teste já existe. Elimine a pasta e tente novamente. + unexpected_content: Conteúdo inesperado encontrado na unidade. placeholder: Verifique a sua ligação com o servidor. subtitle: Validação da ligação tenant_id_wrong: O ID do diretório (inquilino) configurado é inválido. Verifique a configuração. diff --git a/modules/storages/config/locales/crowdin/ro.yml b/modules/storages/config/locales/crowdin/ro.yml index 2d3dd5ab394..4d231155739 100644 --- a/modules/storages/config/locales/crowdin/ro.yml +++ b/modules/storages/config/locales/crowdin/ro.yml @@ -250,6 +250,13 @@ ro: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/ru.yml b/modules/storages/config/locales/crowdin/ru.yml index 58d4247d62e..6a6957cb766 100644 --- a/modules/storages/config/locales/crowdin/ru.yml +++ b/modules/storages/config/locales/crowdin/ru.yml @@ -250,6 +250,13 @@ ru: oidc_cant_refresh_token: Произошла ошибка во время проверки Вашего доступа к хранилищу. Пожалуйста, проверьте журналы сервера для получения дополнительной информации. oidc_non_oidc_user: Текущий пользователь не представлен провайдером идентификации OpenID Connect(OIDC). Пожалуйста, повторите проверку пользователя, идентифицированного через OIDC. oidc_non_provisioned_user: Текущий пользователь не представлен провайдером идентификации OpenID Connect. Пожалуйста, повторите проверку пользователя. + one_drive: + client_cant_delete_folder: У клиента возникли проблемы с удалением папок. Пожалуйста, проверьте документацию по конфигурации для вашего хранилища. + client_write_permission_missing: Возможно, клиент не имеет прав на запись. Пожалуйста, проверьте документацию по конфигурации для вашего хранилища. + drive_id_not_found: Настроенный идентификатор диска не найден. Пожалуйста, проверьте конфигурацию. + drive_id_wrong: Настроенный идентификатор диска не найден. Пожалуйста, проверьте конфигурацию. + existing_test_folder: Папка %{folder_name}, необходимая для тестирования, уже существует. Удалите ее и повторите попытку. + unexpected_content: На диске обнаружено неожидаемое содержимое. placeholder: Проверьте подключение к серверу. subtitle: Проверка соединения tenant_id_wrong: Настроенный идентификатор каталога недействителен. Пожалуйста, проверьте конфигурацию. diff --git a/modules/storages/config/locales/crowdin/rw.yml b/modules/storages/config/locales/crowdin/rw.yml index 94ce3308e9e..c0c4df0c0f8 100644 --- a/modules/storages/config/locales/crowdin/rw.yml +++ b/modules/storages/config/locales/crowdin/rw.yml @@ -250,6 +250,13 @@ rw: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/si.yml b/modules/storages/config/locales/crowdin/si.yml index 55d415475a0..a5d6346c5de 100644 --- a/modules/storages/config/locales/crowdin/si.yml +++ b/modules/storages/config/locales/crowdin/si.yml @@ -250,6 +250,13 @@ si: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/sk.yml b/modules/storages/config/locales/crowdin/sk.yml index 84b3ff4db28..940ae818fae 100644 --- a/modules/storages/config/locales/crowdin/sk.yml +++ b/modules/storages/config/locales/crowdin/sk.yml @@ -250,6 +250,13 @@ sk: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/sl.yml b/modules/storages/config/locales/crowdin/sl.yml index ae8b6cc4a02..c6b69f099a0 100644 --- a/modules/storages/config/locales/crowdin/sl.yml +++ b/modules/storages/config/locales/crowdin/sl.yml @@ -250,6 +250,13 @@ sl: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/sr.yml b/modules/storages/config/locales/crowdin/sr.yml index be750425ddc..1e559a077fb 100644 --- a/modules/storages/config/locales/crowdin/sr.yml +++ b/modules/storages/config/locales/crowdin/sr.yml @@ -250,6 +250,13 @@ sr: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/sv.yml b/modules/storages/config/locales/crowdin/sv.yml index 5c69ea99ec7..6781a7b71fe 100644 --- a/modules/storages/config/locales/crowdin/sv.yml +++ b/modules/storages/config/locales/crowdin/sv.yml @@ -250,6 +250,13 @@ sv: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/th.yml b/modules/storages/config/locales/crowdin/th.yml index 48de95db3c3..f65461f8701 100644 --- a/modules/storages/config/locales/crowdin/th.yml +++ b/modules/storages/config/locales/crowdin/th.yml @@ -250,6 +250,13 @@ th: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/tr.yml b/modules/storages/config/locales/crowdin/tr.yml index f538524fbef..318d5b01492 100644 --- a/modules/storages/config/locales/crowdin/tr.yml +++ b/modules/storages/config/locales/crowdin/tr.yml @@ -250,6 +250,13 @@ tr: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Sunucuyla olan bağlantıyı kontrol edin. subtitle: Bağlantı doğrulaması tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/uk.yml b/modules/storages/config/locales/crowdin/uk.yml index 736cee4d84e..20132de3a18 100644 --- a/modules/storages/config/locales/crowdin/uk.yml +++ b/modules/storages/config/locales/crowdin/uk.yml @@ -250,6 +250,13 @@ uk: oidc_cant_refresh_token: Під час перевірки доступу до сховища сталася помилка. Щоб отримати додаткову інформацію, перевірте журнали сервера. oidc_non_oidc_user: Поточний користувач має доступ, але його надав не постачальник ідентифікаційних даних OpenID Connect (OIDC). Повторіть перевірку з користувачем, створеним через OIDC. oidc_non_provisioned_user: Поточного користувача не надано постачальником ідентифікаційних даних OpenID Connect. Повторіть перевірку з наданим користувачем. + one_drive: + client_cant_delete_folder: Клієнту не вдалося видалити папки. Ознайомтеся з документацією щодо конфігурації для свого сховища. + client_write_permission_missing: Схоже, у клієнта не має дозволів на записування. Ознайомтеся з документацією щодо конфігурації для свого сховища. + drive_id_not_found: Налаштований ідентифікатор Drive ID не знайдено. Перевірте конфігурацію. + drive_id_wrong: Схоже, налаштований ідентифікатор Drive ID недійсний. Перевірте конфігурацію. + existing_test_folder: Папка %{folder_name}, необхідна для тестування, уже існує. Видаліть її та повторіть спробу. + unexpected_content: На диску знайдено неочікуваний контент. placeholder: Перевірте підключення до сервера. subtitle: Перевірка підключення tenant_id_wrong: Налаштований ідентифікатор каталогу (клієнта) недійсний. Перевірте конфігурацію. diff --git a/modules/storages/config/locales/crowdin/uz.yml b/modules/storages/config/locales/crowdin/uz.yml index 1d7e1e1e958..0eb357924c7 100644 --- a/modules/storages/config/locales/crowdin/uz.yml +++ b/modules/storages/config/locales/crowdin/uz.yml @@ -250,6 +250,13 @@ uz: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/config/locales/crowdin/vi.yml b/modules/storages/config/locales/crowdin/vi.yml index e38d49cfc96..b308507dfa6 100644 --- a/modules/storages/config/locales/crowdin/vi.yml +++ b/modules/storages/config/locales/crowdin/vi.yml @@ -250,6 +250,13 @@ vi: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Kiểm tra kết nối của bạn với máy chủ. subtitle: Xác thực kết nối tenant_id_wrong: ID thư mục (người thuê) cấu hình không hợp lệ. Vui lòng kiểm tra cấu hình. diff --git a/modules/storages/config/locales/crowdin/zh-CN.yml b/modules/storages/config/locales/crowdin/zh-CN.yml index 660e8c58c4d..6135de044b8 100644 --- a/modules/storages/config/locales/crowdin/zh-CN.yml +++ b/modules/storages/config/locales/crowdin/zh-CN.yml @@ -250,6 +250,13 @@ zh-CN: oidc_cant_refresh_token: 尝试检查您对存储的访问时出错。请检查服务器日志以获取更多信息。 oidc_non_oidc_user: 当前用户在预配的情况下,没有被 OpenID Connect (OIDC) 身份提供商预置。请重新运行与OIDC 预置用户的检查。 oidc_non_provisioned_user: 当前用户并非由 OpenID Connect 身份提供商提供。请针对提供的用户重新运行检查。 + one_drive: + client_cant_delete_folder: 客户端在删除文件夹时遇到问题。请检查您的存储的设置文档。 + client_write_permission_missing: 客户端似乎没有写入权限。请检查您的存储的设置文档。 + drive_id_not_found: 找不到配置的驱动器 ID。请检查配置。 + drive_id_wrong: 配置的驱动器 ID 似乎无效。请检查配置。 + existing_test_folder: 测试所需的文件夹 %{folder_name} 已经存在。请将其删除并重试。 + unexpected_content: 在驱动器中找到意外内容。 placeholder: 检查您与服务器的连接。 subtitle: 连接验证 tenant_id_wrong: 已配置的 OAuth 2 客户端ID无效。请检查配置。 diff --git a/modules/storages/config/locales/crowdin/zh-TW.yml b/modules/storages/config/locales/crowdin/zh-TW.yml index af5ebddc2b5..066bd0f0123 100644 --- a/modules/storages/config/locales/crowdin/zh-TW.yml +++ b/modules/storages/config/locales/crowdin/zh-TW.yml @@ -250,6 +250,13 @@ zh-TW: oidc_cant_refresh_token: 在嘗試檢查您存取儲存空間時發生錯誤。請檢查伺服器日誌以取得進一步資訊。 oidc_non_oidc_user: 目前使用者雖然已配置,但不是由 OpenID Connect (OIDC) 身分提供者配置的。請使用 OIDC 配置的使用者重新執行檢查。 oidc_non_provisioned_user: 當前用戶不是由 OpenID Connect Identity Provider 提供。請使用提供的使用者重新執行檢查。 + one_drive: + client_cant_delete_folder: 用戶刪除資料夾失敗,請檢查儲存空間的文件設定。 + client_write_permission_missing: 用戶似乎缺少寫入權限,請檢查儲存空間的文件設定。 + drive_id_not_found: 找不到已設定的磁碟代碼。請檢查相關設定。 + drive_id_wrong: 設定的磁碟代碼似乎不對。請檢查相關設定。 + existing_test_folder: 測試所需的資料夾 %{folder_name} 已經存在。請刪除後再重試。 + unexpected_content: 在磁碟中發現未預期的內容。 placeholder: 根據伺服器檢查您的連線。 subtitle: 連線驗證 tenant_id_wrong: 設定的目錄 (租戶) id 無效。請檢查設定。 diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index b444f8424dc..f8cb2c7a370 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -251,6 +251,13 @@ en: oidc_cant_refresh_token: There was an error while trying to check your access to the storage. Please check the server logs for further information. oidc_non_oidc_user: The current user, while provisioned, wasn't provisioned by an OpenID Connect (OIDC) Identity Provider. Please re-run the check with an OIDC provisioned user. oidc_non_provisioned_user: The current user isn't provided by an OpenID Connect Identity Provider. Please re-run the check with a provided user. + one_drive: + client_cant_delete_folder: The client is having trouble deleting folders. Please check the setup documentation for your storage. + client_write_permission_missing: The client seems to have write permissions missing. Please check the setup documentation for your storage. + drive_id_not_found: The configured drive id could not be found. Please check the configuration. + drive_id_wrong: The configured drive id seems invalid. Please check the configuration. + existing_test_folder: The folder %{folder_name} needed for testing already exists. Please delete it and try again. + unexpected_content: Unexpected content found in the drive. placeholder: Check your connection against the server. subtitle: Connection validation tenant_id_wrong: The configured directory (tenant) id is invalid. Please check the configuration. diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud_connection_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/base_connection_validator_spec.rb similarity index 57% rename from modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud_connection_validator_spec.rb rename to modules/storages/spec/common/storages/peripherals/connection_validators/base_connection_validator_spec.rb index ebae600a5cb..5aaaaade745 100644 --- a/modules/storages/spec/common/storages/peripherals/connection_validators/nextcloud_connection_validator_spec.rb +++ b/modules/storages/spec/common/storages/peripherals/connection_validators/base_connection_validator_spec.rb @@ -34,28 +34,43 @@ require_module_spec_helper module Storages module Peripherals module ConnectionValidators - RSpec.describe NextcloudValidator, :webmock do - it "does not run the AMPF tests if the storage is not automatically managed", - vcr: "nextcloud/capabilities_success" do - results = described_class.new(storage: create(:nextcloud_storage_with_local_connection)).validate + class TestValidator < BaseConnectionValidator + def self.reset_groups! + @validation_groups = nil + end + end - expect { results.group(:ampf_configuration) }.to raise_error(KeyError) + RSpec.describe BaseConnectionValidator, :webmock do + let(:storage) { create(:nextcloud_storage_configured) } + + subject(:validator) { TestValidator.new(storage) } + + after { TestValidator.reset_groups! } + + it "returns a ValidationResult" do + expect(validator.call).to be_a(ValidatorResult) + end + + it "only runs a verification if the precondition evaluates as truthy" do + test_group = class_spy(Nextcloud::StorageConfigurationValidator) + TestValidator.register_group :test_group, test_group, precondition: ->(_, _) { false } + + result = validator.call + expect(result).to be_empty + expect(test_group).not_to have_received(:call) end it "aggregates all the results from the tests", vcr: "nextcloud/capabilities_success" do - results = described_class.new(storage: create(:nextcloud_storage_with_local_connection)).validate + TestValidator.register_group :base_configuration, Nextcloud::StorageConfigurationValidator + TestValidator.register_group :authentication, Nextcloud::AuthenticationValidator, + precondition: ->(_, result) { result.group(:base_configuration).non_failure? } + + results = TestValidator.new(create(:nextcloud_storage_with_local_connection)).call expect(results).to be_warning expect(results.group(:base_configuration)).to be_success expect(results.group(:authentication)).to be_warning end - - it "does not run any further tests if base configuration failed", vcr: "nextcloud/capabilities_invalid_data" do - results = described_class.new(storage: create(:nextcloud_storage_with_local_connection)).validate - - expect { results.group(:authentication) }.to raise_error(KeyError) - expect { results.group(:ampf_configuration) }.to raise_error(KeyError) - end end end end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator_spec.rb new file mode 100644 index 00000000000..67f994a943b --- /dev/null +++ b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/ampf_configuration_validator_spec.rb @@ -0,0 +1,118 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +module Storages + module Peripherals + module ConnectionValidators + module OneDrive + RSpec.describe AmpfConfigurationValidator, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage, :as_automatically_managed) } + let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } + let(:folder_name) { described_class::TEST_FOLDER_NAME } + + subject(:validator) { described_class.new(storage) } + + it "returns a GroupValidationResult", vcr: "one_drive/validator_ampf_clean_run" do + results = validator.call + + expect(results).to be_a(ValidationGroupResult) + expect(results).to be_success + end + + describe "possible error scenarios" do + it "fails when there's unexpected folder and files in the drive", vcr: "one_drive/validator_extraneous_files" do + results = validator.call + + expect(results[:drive_contents]).to be_a_warning + expect(results[:drive_contents].message).to eq(I18n.t(i18n_key("one_drive.unexpected_content"))) + end + + it "fails when folders can't be created" do + create_cmd = class_double(StorageInteraction::OneDrive::CreateFolderCommand) + allow(create_cmd).to receive(:call) + .with(storage:, auth_strategy:, folder_name:, parent_location: ParentFolder.root) + .and_return(ServiceResult.failure) + + Registry.stub("one_drive.commands.create_folder", create_cmd) + + results = validator.call + + expect(results[:client_folder_creation]).to be_a_failure + expect(results[:client_folder_creation].message) + .to eq(I18n.t(i18n_key("one_drive.client_write_permission_missing"))) + end + + it "fails when the test folder already exists on the remote", vcr: "one_drive/validator_test_folder_already_exists" do + Registry["one_drive.commands.create_folder"] + .call(storage:, auth_strategy:, folder_name:, parent_location: ParentFolder.root) + + result = validator.call + expect(result[:client_folder_creation]).to be_a_failure + expect(result[:client_folder_creation].message) + .to eq(I18n.t(i18n_key("one_drive.existing_test_folder"), folder_name:)) + ensure + StorageInteraction::OneDrive::DeleteFolderCommand.call(storage:, auth_strategy:, location: created_folder) + end + + it "fails when folders can't be deleted", vcr: "one_drive/validator_create_folder" do + delete_cmd = class_double(StorageInteraction::OneDrive::DeleteFolderCommand) + allow(delete_cmd).to receive(:call).with(storage:, auth_strategy:, location: /.+/) + .and_return(ServiceResult.failure) + + Registry.stub("one_drive.commands.delete_folder", delete_cmd) + + results = validator.call + + expect(results[:client_folder_removal]).to be_a_failure + expect(results[:client_folder_removal].message).to eq(I18n.t(i18n_key("one_drive.client_cant_delete_folder"))) + ensure + StorageInteraction::OneDrive::DeleteFolderCommand.call(storage:, auth_strategy:, location: created_folder) + end + end + + private + + def created_folder + Registry["one_drive.queries.files"].call(storage:, auth_strategy:, folder: ParentFolder.root).on_success do + folder = it.result.files.detect { |file| file.name.include?(folder_name) } + + return folder.id + end + end + + def i18n_key(key) = "storages.health.connection_validation.#{key}" + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/authentication_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/authentication_validator_spec.rb new file mode 100644 index 00000000000..eea23f6958d --- /dev/null +++ b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/authentication_validator_spec.rb @@ -0,0 +1,76 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +module Storages + module Peripherals + module ConnectionValidators + module OneDrive + RSpec.describe AuthenticationValidator, :webmock do + subject(:validator) { described_class.new(storage) } + + context "when using OAuth2" do + let(:user) { create(:user) } + let(:storage) { create(:sharepoint_dev_drive_storage, oauth_client_token_user: user) } + + before { User.current = user } + + it "passes when the user has a token and the request works", vcr: "one_drive/user_query_success" do + expect(validator.call).to be_success + end + + it "returns a warning when there's no token for the current user" do + User.current = create(:user) + result = validator.call + + expect(result[:existing_token]).to be_a_warning + expect(result[:existing_token].message).to eq(I18n.t(i18n_key(:oauth_token_missing))) + expect(result[:user_bound_request]).to be_skipped + end + + it "returns a failure if the remote call failed" do + Registry.stub("one_drive.queries.user", ->(_) { ServiceResult.failure(result: :unauthorized) }) + + result = validator.call + expect(result[:user_bound_request]).to be_a_failure + expect(result[:user_bound_request].message).to eq(I18n.t(i18n_key(:oauth_request_unauthorized))) + end + end + + private + + def i18n_key(key) = "storages.health.connection_validation.#{key}" + end + end + end + end +end diff --git a/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator_spec.rb b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator_spec.rb new file mode 100644 index 00000000000..11a18dcba43 --- /dev/null +++ b/modules/storages/spec/common/storages/peripherals/connection_validators/one_drive/storage_configuration_validator_spec.rb @@ -0,0 +1,153 @@ +# 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. +#++ + +require "spec_helper" +require_module_spec_helper + +module Storages + module Peripherals + module ConnectionValidators + module OneDrive + RSpec.describe StorageConfigurationValidator, :webmock do + let(:storage) { create(:sharepoint_dev_drive_storage, :as_automatically_managed) } + let(:auth_strategy) { Registry["one_drive.authentication.userless"].call } + + subject(:validator) { described_class.new(storage) } + + it "returns a GroupValidationResult", vcr: "one_drive/files_query_userless" do + results = validator.call + + expect(results).to be_a(ValidationGroupResult) + expect(results).to be_success + end + + describe "possible error scenarios" do + let(:files_double) { class_double(StorageInteraction::OneDrive::FilesQuery) } + let(:result) { ServiceResult.success } + + before do + allow(files_double).to receive(:call).with(storage:, auth_strategy:, folder: ParentFolder.root).and_return(result) + end + + context "when the storage isn't configured" do + let(:storage) { create(:one_drive_storage) } + + it "the check fails" do + results = validator.call + expect(results[:storage_configured]).to be_a_failure + expect(results[:storage_configured].message).to eq(I18n.t(i18n_key(:not_configured))) + end + end + + context "when diagnostic request fails with an unhandled error" do + let(:result) { ServiceResult.failure(result: :error, errors: StorageError.new(code: :error)) } + + before { Registry.stub("one_drive.queries.files", files_double) } + + it "the check fails" do + results = validator.call + + expect(results[:diagnostic_request]).to be_a_failure + expect(results[:diagnostic_request].message).to eq(I18n.t(i18n_key(:unknown_error))) + end + + it "logs an error" do + allow(Rails.logger).to receive(:error) + validator.call + + expect(Rails.logger).to have_received(:error).with(/Connection validation failed with unknown/) + end + end + + context "when the tenant id is wrong" do + it "but looks like an actual valid value", vcr: "one_drive/validation_wrong_tenant_id" do + storage.tenant_id = "itdoesnotexists9000.sharepoint.com" + results = described_class.new(storage).call + + expect(results[:tenant_id]).to be_a_failure + expect(results[:tenant_id].message).to eq(I18n.t(i18n_key(:tenant_id_wrong))) + end + + it "but is blatantly wrong", vcr: "one_drive/validation_absurd_tenant_id" do + storage.tenant_id = "wrong" + results = described_class.new(storage).call + + expect(results[:tenant_id]).to be_a_failure + expect(results[:tenant_id].message).to eq(I18n.t(i18n_key(:tenant_id_wrong))) + end + end + + context "when the client secret is wrong" do + it "fails the check", vcr: "one_drive/validation_wrong_client_secret" do + storage.oauth_client.client_secret = "wrong" + results = described_class.new(storage).call + + expect(results[:client_secret]).to be_a_failure + expect(results[:client_secret].message).to eq(I18n.t(i18n_key(:client_secret_wrong))) + end + end + + context "when the client id is wrong" do + it "fails the check", vcr: "one_drive/validation_wrong_client_id" do + storage.oauth_client.client_id = "wrong" + results = described_class.new(storage).call + + expect(results[:client_id]).to be_a_failure + expect(results[:client_id].message).to eq(I18n.t(i18n_key(:client_id_wrong))) + end + end + + context "when the drive id is wrong" do + it "fails when looks malformed", vcr: "one_drive/validation_drive_id_malformed" do + storage.drive_id = "not-a-drive-id" + results = described_class.new(storage).call + + expect(results[:drive_id_format]).to be_a_failure + expect(results[:drive_id_format].message).to eq(I18n.t(i18n_key("one_drive.drive_id_wrong"))) + end + + it "fails when is not found", vcr: "one_drive/validation_drive_id_not_found" do + storage.drive_id = "#{storage.drive_id[0..-2]}0" + results = described_class.new(storage).call + + expect(results[:drive_id_not_found]).to be_a_failure + expect(results[:drive_id_not_found].message).to eq(I18n.t(i18n_key("one_drive.drive_id_not_found"))) + end + end + end + + private + + def i18n_key(key) = "storages.health.connection_validation.#{key}" + end + end + end + end +end diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/files_query_userless.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/files_query_userless.yml new file mode 100644 index 00000000000..f3baf965b52 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/files_query_userless.yml @@ -0,0 +1,114 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - b50c2aba-1a4b-4915-a71e-b29d53290900 + X-Ms-Ests-Server: + - 2.1.20393.4 - SEC ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-3sWFN2meJ8S-bmklrr6Kbg' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AqVrum5XuR9KsQqo956mtLBRVKK0AQAAAG24hd8OAAAA; expires=Wed, 07-May-2025 + 12:13:34 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 07 Apr 2025 12:13:33 GMT + Content-Length: + - '1696' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Mon, 07 Apr 2025 12:13:34 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 9625a92b-e916-425f-b889-e4f0f06ec4e1 + Client-Request-Id: + - 9625a92b-e916-425f-b889-e4f0f06ec4e1 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"001","RoleInstance":"FR2PEPF000001EE"}}' + Date: + - Mon, 07 Apr 2025 12:13:33 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)","value":[{"@odata.etag":"\"{8DA9DCAF-43E4-419C-B52E-F1303A0D3372},1\"","createdBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"id":"01ANJ53W5P3SUY3ZCDTRA3KLXRGA5A2M3S","lastModifiedBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"name":"data","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/data","fileSystemInfo":{"createdDateTime":"2025-04-07T12:02:26Z","lastModifiedDateTime":"2025-04-07T12:02:26Z"},"folder":{"childCount":0},"size":0},{"@odata.etag":"\"{992C5835-AA54-4B71-B3AF-197B1F4578E8},1\"","createdBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"id":"01ANJ53WZVLAWJSVFKOFF3HLYZPMPUK6HI","lastModifiedBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"name":"simply_oidc.jpg","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/simply_oidc.jpg","file":{"hashes":{"quickXorHash":"qzqT/ZJgJnvhu77aF+2RcdH62Sc="},"mimeType":"image/jpeg"},"fileSystemInfo":{"createdDateTime":"2025-04-07T12:02:42Z","lastModifiedDateTime":"2025-04-07T12:02:42Z"},"size":56483}]}' + recorded_at: Mon, 07 Apr 2025 12:13:34 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_absurd_tenant_id.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_absurd_tenant_id.yml new file mode 100644 index 00000000000..32e26326194 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_absurd_tenant_id.yml @@ -0,0 +1,71 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/wrong/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 400 + message: Bad Request + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 6d866228-7a7d-4c37-aedc-c219ccd99200 + X-Ms-Ests-Server: + - 2.1.20393.4 - FRC ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-ZuX_mRovdaFEsnx7JOyHtQ' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AkEOv7Godm1MpiXqOWOQa-U; expires=Wed, 07-May-2025 12:54:01 GMT; path=/; + secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; path=/; secure; + samesite=none; httponly, stsservicecookie=estsfd; path=/; secure; samesite=none; + httponly + Date: + - Mon, 07 Apr 2025 12:54:01 GMT + Content-Length: + - '519' + body: + encoding: UTF-8 + string: '{"error":"invalid_request","error_description":"AADSTS900023: Specified + tenant identifier ''wrong'' is neither a valid DNS name, nor a valid external + domain. Trace ID: 6d866228-7a7d-4c37-aedc-c219ccd99200 Correlation ID: eaa0b807-04e6-483b-bfaf-e88ab94a8089 + Timestamp: 2025-04-07 12:54:01Z","error_codes":[900023],"timestamp":"2025-04-07 + 12:54:01Z","trace_id":"6d866228-7a7d-4c37-aedc-c219ccd99200","correlation_id":"eaa0b807-04e6-483b-bfaf-e88ab94a8089","error_uri":"https://login.microsoftonline.com/error?code=900023"}' + recorded_at: Mon, 07 Apr 2025 12:54:01 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_drive_id_malformed.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_drive_id_malformed.yml new file mode 100644 index 00000000000..a5bd6d4b617 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_drive_id_malformed.yml @@ -0,0 +1,111 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 4baa8563-b8be-4599-9f6b-d4169d949d00 + X-Ms-Ests-Server: + - 2.1.20393.4 - FRC ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-ePygL8dNIpLEv2G4rO9RvA' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AoMQayjayslJnblHnzkv3xRRVKK0AQAAAFfQhd8OAAAA; expires=Wed, 07-May-2025 + 13:55:35 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 07 Apr 2025 13:55:34 GMT + Content-Length: + - '1696' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Mon, 07 Apr 2025 13:55:35 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/not-a-drive-id/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 400 + message: Bad Request + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 6f836231-a3f9-4005-85a1-f4246c7e0746 + Client-Request-Id: + - 6f836231-a3f9-4005-85a1-f4246c7e0746 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"004","RoleInstance":"FR2PEPF000003FC"}}' + Date: + - Mon, 07 Apr 2025 13:55:35 GMT + body: + encoding: UTF-8 + string: '{"error":{"code":"invalidRequest","message":"The provided drive id + appears to be malformed, or does not represent a valid drive."}}' + recorded_at: Mon, 07 Apr 2025 13:55:35 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_drive_id_not_found.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_drive_id_not_found.yml new file mode 100644 index 00000000000..f8fc1b89e7b --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_drive_id_not_found.yml @@ -0,0 +1,109 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 5c707cf2-ee49-40ea-9f3a-12135a310400 + X-Ms-Ests-Server: + - 2.1.20465.4 - SEC ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-IKAIoxsi5jE7daVt7_RV1g' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=Atm5OoxKOcJEm_fG2VtSgv5RVKK0AQAAAL_yhd8OAAAA; expires=Wed, 07-May-2025 + 16:22:23 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 07 Apr 2025 16:22:23 GMT + Content-Length: + - '1696' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Mon, 07 Apr 2025 16:22:23 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPX0/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer + response: + status: + code: 404 + message: Not Found + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; charset=utf-8 + Content-Encoding: + - gzip + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - '01824623-0f82-4e02-b56b-91e7db164c27' + Client-Request-Id: + - '01824623-0f82-4e02-b56b-91e7db164c27' + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"UK South","Slice":"E","Ring":"5","ScaleUnit":"001","RoleInstance":"LN2PEPF000114D0"}}' + Date: + - Mon, 07 Apr 2025 16:22:23 GMT + body: + encoding: UTF-8 + string: '{"error":{"code":"itemNotFound","message":"Item not found"}}' + recorded_at: Mon, 07 Apr 2025 16:22:24 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_client_id.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_client_id.yml new file mode 100644 index 00000000000..5acf4d9239e --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_client_id.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=wrong&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '168' + response: + status: + code: 400 + message: Bad Request + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 65abb2e7-9ea0-4b11-ba8a-2e2bfcec4200 + X-Ms-Ests-Server: + - 2.1.20465.4 - WEULR1 ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-ewwu91fb4gwQqBkjUr57Yg' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AlOcQyq-LSdKqIifcwNWuRRkaFwcAQAAAEbJhd8OAAAA; expires=Wed, 07-May-2025 + 13:25:27 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 07 Apr 2025 13:25:27 GMT + Content-Length: + - '707' + body: + encoding: UTF-8 + string: '{"error":"unauthorized_client","error_description":"AADSTS700016: Application + with identifier ''wrong'' was not found in the directory ''MSFT''. This can + happen if the application has not been installed by the administrator of the + tenant or consented to by any user in the tenant. You may have sent your authentication + request to the wrong tenant. Trace ID: 65abb2e7-9ea0-4b11-ba8a-2e2bfcec4200 + Correlation ID: 194e4908-91a7-440c-947d-bf5faf49403f Timestamp: 2025-04-07 + 13:25:27Z","error_codes":[700016],"timestamp":"2025-04-07 13:25:27Z","trace_id":"65abb2e7-9ea0-4b11-ba8a-2e2bfcec4200","correlation_id":"194e4908-91a7-440c-947d-bf5faf49403f","error_uri":"https://login.microsoftonline.com/error?code=700016"}' + recorded_at: Mon, 07 Apr 2025 13:25:27 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_client_secret.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_client_secret.yml new file mode 100644 index 00000000000..4f7bdd9cb57 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_client_secret.yml @@ -0,0 +1,72 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=wrong + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '162' + response: + status: + code: 401 + message: Unauthorized + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - a6f0383f-bbcb-48f5-a08d-36a574b40d00 + X-Ms-Ests-Server: + - 2.1.20465.4 - WEULR1 ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-qNTRKH43OU5V1dsSJzZDuQ' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AsH2DTOUkrFPnjpcckwANXxRVKK0AQAAAMnIhd8OAAAA; expires=Wed, 07-May-2025 + 13:23:21 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 07 Apr 2025 13:23:21 GMT + Content-Length: + - '623' + body: + encoding: UTF-8 + string: '{"error":"invalid_client","error_description":"AADSTS7000215: Invalid + client secret provided. Ensure the secret being sent in the request is the + client secret value, not the client secret ID, for a secret added to app ''b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff''. + Trace ID: a6f0383f-bbcb-48f5-a08d-36a574b40d00 Correlation ID: 80ce0729-9980-4683-a4f8-39cfd3e9c96c + Timestamp: 2025-04-07 13:23:21Z","error_codes":[7000215],"timestamp":"2025-04-07 + 13:23:21Z","trace_id":"a6f0383f-bbcb-48f5-a08d-36a574b40d00","correlation_id":"80ce0729-9980-4683-a4f8-39cfd3e9c96c","error_uri":"https://login.microsoftonline.com/error?code=7000215"}' + recorded_at: Mon, 07 Apr 2025 13:23:21 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_tenant_id.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_tenant_id.yml new file mode 100644 index 00000000000..7c908f6f820 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validation_wrong_tenant_id.yml @@ -0,0 +1,73 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/itdoesnotexists9000.sharepoint.com/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 400 + message: Bad Request + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 2a8d5b96-aa99-443e-a69e-03f02c2e4c00 + X-Ms-Ests-Server: + - 2.1.20465.4 - NEULR1 ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-9cTLDpFlQVtcJ6DtcOmi3g' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AscnkNupYDJFowKyY6L32lERw07PAQAAAI29hd8OAAAA; expires=Wed, 07-May-2025 + 12:35:26 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Mon, 07 Apr 2025 12:35:26 GMT + Content-Length: + - '680' + body: + encoding: UTF-8 + string: '{"error":"invalid_request","error_description":"AADSTS90002: Tenant + ''itdoesnotexists9000.sharepoint.com'' not found. Check to make sure you have + the correct tenant ID and are signing into the correct cloud. Check with your + subscription administrator, this may happen if there are no active subscriptions + for the tenant. Trace ID: 2a8d5b96-aa99-443e-a69e-03f02c2e4c00 Correlation + ID: a1e47b18-9902-42ed-bb50-6478c5aa98cd Timestamp: 2025-04-07 12:35:26Z","error_codes":[90002],"timestamp":"2025-04-07 + 12:35:26Z","trace_id":"2a8d5b96-aa99-443e-a69e-03f02c2e4c00","correlation_id":"a1e47b18-9902-42ed-bb50-6478c5aa98cd","error_uri":"https://login.microsoftonline.com/error?code=90002"}' + recorded_at: Mon, 07 Apr 2025 12:35:26 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_ampf_clean_run.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_ampf_clean_run.yml new file mode 100644 index 00000000000..4fe63906717 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_ampf_clean_run.yml @@ -0,0 +1,240 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 75da94cc-f93b-4957-9472-7f69b50a0500 + X-Ms-Ests-Server: + - 2.1.20465.4 - WEULR1 ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-EnkQImmzmdKVuOaBI_JV2w' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AufQETJp6LNOiwGffjFiBhtRVKK0AQAAAKMlh98OAAAA; expires=Thu, 08-May-2025 + 14:11:48 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Tue, 08 Apr 2025 14:11:48 GMT + Content-Length: + - '1695' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Tue, 08 Apr 2025 14:11:48 GMT +- request: + method: post + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children + body: + encoding: UTF-8 + string: '{"name":"ConnectionValidatorFolder","folder":{},"@microsoft.graph.conflictBehavior":"fail"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '91' + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Content-Encoding: + - gzip + Etag: + - '"{DB39CBFF-7643-4C26-801D-7C3E22B888BA},1"' + Location: + - https://ymt6d.sharepoint.com/_api/v2.0/drives('b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs')/items('root')/children('01ANJ53W77ZM45WQ3WEZGIAHL4HYRLRCF2') + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 7454b765-25ac-43f0-b1e2-9f4ae7122668 + Client-Request-Id: + - 7454b765-25ac-43f0-b1e2-9f4ae7122668 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF00001A2F"}}' + Odata-Version: + - '4.0' + Date: + - Tue, 08 Apr 2025 14:11:48 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21FeOZEMfQx0eGQKqVBLcP__BG8mq-4-9FuRqOyk3MXY8CfNaHr_0ERYs5kgmEWFrX'')/root/children/$entity","@odata.etag":"\"{DB39CBFF-7643-4C26-801D-7C3E22B888BA},1\"","createdDateTime":"2025-04-08T14:11:49Z","eTag":"\"{DB39CBFF-7643-4C26-801D-7C3E22B888BA},1\"","id":"01ANJ53W77ZM45WQ3WEZGIAHL4HYRLRCF2","lastModifiedDateTime":"2025-04-08T14:11:49Z","name":"ConnectionValidatorFolder","size":0,"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20AMPF/ConnectionValidatorFolder","cTag":"\"c:{DB39CBFF-7643-4C26-801D-7C3E22B888BA},0\"","commentSettings":{"commentingDisabled":{"isDisabled":false}},"createdBy":{"application":{"displayName":"OP + Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"lastModifiedBy":{"application":{"displayName":"OP Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"parentReference":{"driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","driveType":"documentLibrary","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","sharepointIds":{"listId":"87d67c02-fdaf-4504-8b39-920984585ad7","listItemUniqueId":"458e6945-3722-40b1-946f-4743a2d480f2","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff","siteUrl":"https://ymt6d.sharepoint.com/sites/OPTest","tenantId":"4d44bf36-9b56-45c0-8807-bbf386dd047f","webId":"6af246f0-e3be-45ef-b91a-8eca4dcc5d8f"}},"fileSystemInfo":{"createdDateTime":"2025-04-08T14:11:49Z","lastModifiedDateTime":"2025-04-08T14:11:49Z"},"folder":{"childCount":0,"view":{"sortBy":"name","sortOrder":"ascending","viewType":"thumbnails"}},"shared":{"scope":"unknown"}}' + recorded_at: Tue, 08 Apr 2025 14:11:48 GMT +- request: + method: delete + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01ANJ53W77ZM45WQ3WEZGIAHL4HYRLRCF2 + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Cache-Control: + - no-store, no-cache + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 8458dc49-bea8-4d56-86ef-b519ef910600 + Client-Request-Id: + - 8458dc49-bea8-4d56-86ef-b519ef910600 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF000007A3"}}' + Date: + - Tue, 08 Apr 2025 14:11:49 GMT + body: + encoding: UTF-8 + string: '' + recorded_at: Tue, 08 Apr 2025 14:11:49 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 9f49a172-9d26-4a50-966d-fa5fe4911dff + Client-Request-Id: + - 9f49a172-9d26-4a50-966d-fa5fe4911dff + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF0000149B"}}' + Date: + - Tue, 08 Apr 2025 14:11:48 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)","value":[]}' + recorded_at: Tue, 08 Apr 2025 14:11:49 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 2a8bae40-ed69-49f2-a9f9-1960a5cdf5b1 + Client-Request-Id: + - 2a8bae40-ed69-49f2-a9f9-1960a5cdf5b1 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF0000149B"}}' + Date: + - Tue, 08 Apr 2025 14:11:48 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)/$entity","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"root","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20AMPF","fileSystemInfo":{"createdDateTime":"2025-04-07T12:03:17Z","lastModifiedDateTime":"2025-04-08T14:11:49Z"},"folder":{"childCount":0},"size":0}' + recorded_at: Tue, 08 Apr 2025 14:11:49 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_create_folder.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_create_folder.yml new file mode 100644 index 00000000000..e0549cc064c --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_create_folder.yml @@ -0,0 +1,208 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - cc781a8e-8383-4c4e-8f6a-354c2bd91a00 + X-Ms-Ests-Server: + - 2.1.20465.4 - SEC ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-sfaLk_7kQLXPiWO9sEYnIw' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AsrHTiwxKD9KmuMnc_TQ-gtRVKK0AQAAAEwih98OAAAA; expires=Thu, 08-May-2025 + 13:57:33 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Tue, 08 Apr 2025 13:57:32 GMT + Content-Length: + - '1696' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Tue, 08 Apr 2025 13:57:33 GMT +- request: + method: post + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children + body: + encoding: UTF-8 + string: '{"name":"ConnectionValidatorFolder","folder":{},"@microsoft.graph.conflictBehavior":"fail"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '91' + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Content-Encoding: + - gzip + Etag: + - '"{33CB5CFC-6E8E-4B49-9503-9E79A62D07ED},1"' + Location: + - https://ymt6d.sharepoint.com/_api/v2.0/drives('b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs')/items('root')/children('01ANJ53W74LTFTHDTOJFFZKA46PGTC2B7N') + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 557973a9-0916-4420-b20c-cfe22c147f78 + Client-Request-Id: + - 557973a9-0916-4420-b20c-cfe22c147f78 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF00000C1C"}}' + Odata-Version: + - '4.0' + Date: + - Tue, 08 Apr 2025 13:57:33 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21FeOZEMfQx0eGQKqVBLcP__BG8mq-4-9FuRqOyk3MXY9jo6leJDqrT7muzvmiWjFW'')/root/children/$entity","@odata.etag":"\"{33CB5CFC-6E8E-4B49-9503-9E79A62D07ED},1\"","createdDateTime":"2025-04-08T13:57:34Z","eTag":"\"{33CB5CFC-6E8E-4B49-9503-9E79A62D07ED},1\"","id":"01ANJ53W74LTFTHDTOJFFZKA46PGTC2B7N","lastModifiedDateTime":"2025-04-08T13:57:34Z","name":"ConnectionValidatorFolder","size":0,"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/ConnectionValidatorFolder","cTag":"\"c:{33CB5CFC-6E8E-4B49-9503-9E79A62D07ED},0\"","commentSettings":{"commentingDisabled":{"isDisabled":false}},"createdBy":{"application":{"displayName":"OP + Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"lastModifiedBy":{"application":{"displayName":"OP Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"parentReference":{"driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","driveType":"documentLibrary","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","sharepointIds":{"listId":"5ea9a363-3a24-4fab-b9ae-cef9a25a3156","listItemUniqueId":"6bc8b02c-f419-4bec-8531-be5b69948a7d","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff","siteUrl":"https://ymt6d.sharepoint.com/sites/OPTest","tenantId":"4d44bf36-9b56-45c0-8807-bbf386dd047f","webId":"6af246f0-e3be-45ef-b91a-8eca4dcc5d8f"}},"fileSystemInfo":{"createdDateTime":"2025-04-08T13:57:34Z","lastModifiedDateTime":"2025-04-08T13:57:34Z"},"folder":{"childCount":0,"view":{"sortBy":"name","sortOrder":"ascending","viewType":"thumbnails"}},"shared":{"scope":"unknown"}}' + recorded_at: Tue, 08 Apr 2025 13:57:34 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - b9666e96-42f3-4c2f-abcc-03678ac30518 + Client-Request-Id: + - b9666e96-42f3-4c2f-abcc-03678ac30518 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF000007A8"}}' + Date: + - Tue, 08 Apr 2025 13:57:33 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)","value":[{"@odata.etag":"\"{33CB5CFC-6E8E-4B49-9503-9E79A62D07ED},2\"","createdBy":{"application":{"id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff","displayName":"OP + Auth"},"user":{"displayName":"SharePoint App"}},"id":"01ANJ53W74LTFTHDTOJFFZKA46PGTC2B7N","lastModifiedBy":{"application":{"id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff","displayName":"OP + Auth"},"user":{"displayName":"SharePoint App"}},"name":"ConnectionValidatorFolder","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/ConnectionValidatorFolder","fileSystemInfo":{"createdDateTime":"2025-04-08T13:57:34Z","lastModifiedDateTime":"2025-04-08T13:57:34Z"},"folder":{"childCount":0},"size":0},{"@odata.etag":"\"{8DA9DCAF-43E4-419C-B52E-F1303A0D3372},1\"","createdBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"id":"01ANJ53W5P3SUY3ZCDTRA3KLXRGA5A2M3S","lastModifiedBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"name":"data","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/data","fileSystemInfo":{"createdDateTime":"2025-04-07T12:02:26Z","lastModifiedDateTime":"2025-04-07T12:02:26Z"},"folder":{"childCount":0},"size":0},{"@odata.etag":"\"{992C5835-AA54-4B71-B3AF-197B1F4578E8},2\"","createdBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"id":"01ANJ53WZVLAWJSVFKOFF3HLYZPMPUK6HI","lastModifiedBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"name":"simply_oidc.jpg","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/simply_oidc.jpg","file":{"hashes":{"quickXorHash":"qzqT/ZJgJnvhu77aF+2RcdH62Sc="},"mimeType":"image/jpeg"},"fileSystemInfo":{"createdDateTime":"2025-04-07T12:02:42Z","lastModifiedDateTime":"2025-04-07T12:02:42Z"},"size":56483}]}' + recorded_at: Tue, 08 Apr 2025 13:57:34 GMT +- request: + method: delete + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01ANJ53W74LTFTHDTOJFFZKA46PGTC2B7N + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Cache-Control: + - no-store, no-cache + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - b14d3091-56c0-4f21-9f7c-4ed0f4c7acc4 + Client-Request-Id: + - b14d3091-56c0-4f21-9f7c-4ed0f4c7acc4 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"000","RoleInstance":"FR1PEPF0000107E"}}' + Date: + - Tue, 08 Apr 2025 13:57:33 GMT + body: + encoding: UTF-8 + string: '' + recorded_at: Tue, 08 Apr 2025 13:57:34 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_extraneous_files.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_extraneous_files.yml new file mode 100644 index 00000000000..aec24c49724 --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_extraneous_files.yml @@ -0,0 +1,205 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 176612aa-10fa-4904-9d12-085a28152100 + X-Ms-Ests-Server: + - 2.1.20465.4 - NEULR1 ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-Zd8MbfrL29hkV1jA5397lg' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AkkUsDa-MSdNrojqp62cRydRVKK0AQAAAPIjh98OAAAA; expires=Thu, 08-May-2025 + 14:04:34 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Tue, 08 Apr 2025 14:04:34 GMT + Content-Length: + - '1701' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Tue, 08 Apr 2025 14:04:34 GMT +- request: + method: post + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children + body: + encoding: UTF-8 + string: '{"name":"ConnectionValidatorFolder","folder":{},"@microsoft.graph.conflictBehavior":"fail"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '91' + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Content-Encoding: + - gzip + Etag: + - '"{610A0BB0-1141-4B56-A0A3-15CB69398698},1"' + Location: + - https://ymt6d.sharepoint.com/_api/v2.0/drives('b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs')/items('root')/children('01ANJ53W5QBMFGCQIRKZF2BIYVZNUTTBUY') + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - c596ba9b-d722-46be-927e-123f12e0e946 + Client-Request-Id: + - c596ba9b-d722-46be-927e-123f12e0e946 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"002","RoleInstance":"FR3PEPF00000AB6"}}' + Odata-Version: + - '4.0' + Date: + - Tue, 08 Apr 2025 14:04:34 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21FeOZEMfQx0eGQKqVBLcP__BG8mq-4-9FuRqOyk3MXY9jo6leJDqrT7muzvmiWjFW'')/root/children/$entity","@odata.etag":"\"{610A0BB0-1141-4B56-A0A3-15CB69398698},1\"","createdDateTime":"2025-04-08T14:04:35Z","eTag":"\"{610A0BB0-1141-4B56-A0A3-15CB69398698},1\"","id":"01ANJ53W5QBMFGCQIRKZF2BIYVZNUTTBUY","lastModifiedDateTime":"2025-04-08T14:04:35Z","name":"ConnectionValidatorFolder","size":0,"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/ConnectionValidatorFolder","cTag":"\"c:{610A0BB0-1141-4B56-A0A3-15CB69398698},0\"","commentSettings":{"commentingDisabled":{"isDisabled":false}},"createdBy":{"application":{"displayName":"OP + Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"lastModifiedBy":{"application":{"displayName":"OP Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"parentReference":{"driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","driveType":"documentLibrary","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","sharepointIds":{"listId":"5ea9a363-3a24-4fab-b9ae-cef9a25a3156","listItemUniqueId":"6bc8b02c-f419-4bec-8531-be5b69948a7d","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff","siteUrl":"https://ymt6d.sharepoint.com/sites/OPTest","tenantId":"4d44bf36-9b56-45c0-8807-bbf386dd047f","webId":"6af246f0-e3be-45ef-b91a-8eca4dcc5d8f"}},"fileSystemInfo":{"createdDateTime":"2025-04-08T14:04:35Z","lastModifiedDateTime":"2025-04-08T14:04:35Z"},"folder":{"childCount":0,"view":{"sortBy":"name","sortOrder":"ascending","viewType":"thumbnails"}},"shared":{"scope":"unknown"}}' + recorded_at: Tue, 08 Apr 2025 14:04:35 GMT +- request: + method: delete + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01ANJ53W5QBMFGCQIRKZF2BIYVZNUTTBUY + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Cache-Control: + - no-store, no-cache + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - ae547ca3-7b02-4039-b9b4-3e2bc3f61816 + Client-Request-Id: + - ae547ca3-7b02-4039-b9b4-3e2bc3f61816 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"002","RoleInstance":"FR3PEPF0000055B"}}' + Date: + - Tue, 08 Apr 2025 14:04:35 GMT + body: + encoding: UTF-8 + string: '' + recorded_at: Tue, 08 Apr 2025 14:04:35 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 67bed2c6-b993-4cbf-a597-5dc017c4285b + Client-Request-Id: + - 67bed2c6-b993-4cbf-a597-5dc017c4285b + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"002","RoleInstance":"FR3PEPF00000AB9"}}' + Date: + - Tue, 08 Apr 2025 14:04:35 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)","value":[{"@odata.etag":"\"{8DA9DCAF-43E4-419C-B52E-F1303A0D3372},1\"","createdBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"id":"01ANJ53W5P3SUY3ZCDTRA3KLXRGA5A2M3S","lastModifiedBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"name":"data","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/data","fileSystemInfo":{"createdDateTime":"2025-04-07T12:02:26Z","lastModifiedDateTime":"2025-04-07T12:02:26Z"},"folder":{"childCount":0},"size":0},{"@odata.etag":"\"{992C5835-AA54-4B71-B3AF-197B1F4578E8},2\"","createdBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"id":"01ANJ53WZVLAWJSVFKOFF3HLYZPMPUK6HI","lastModifiedBy":{"user":{"email":"op.admin@ymt6d.onmicrosoft.com","id":"5b5a7dc4-4539-41ba-9fa9-100f0a26acb7","displayName":"Eric + Schubert"}},"name":"simply_oidc.jpg","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + VCR","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20VCR/simply_oidc.jpg","file":{"hashes":{"quickXorHash":"qzqT/ZJgJnvhu77aF+2RcdH62Sc="},"mimeType":"image/jpeg"},"fileSystemInfo":{"createdDateTime":"2025-04-07T12:02:42Z","lastModifiedDateTime":"2025-04-07T12:02:42Z"},"size":56483}]}' + recorded_at: Tue, 08 Apr 2025 14:04:35 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_test_folder_already_exists.yml b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_test_folder_already_exists.yml new file mode 100644 index 00000000000..72b661cacff --- /dev/null +++ b/modules/storages/spec/support/fixtures/vcr_cassettes/one_drive/validator_test_folder_already_exists.yml @@ -0,0 +1,248 @@ +--- +http_interactions: +- request: + method: post + uri: https://login.microsoftonline.com/4d44bf36-9b56-45c0-8807-bbf386dd047f/oauth2/v2.0/token + body: + encoding: ASCII-8BIT + string: grant_type=client_credentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access&client_id=b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff&client_secret=nB58Q%7EwWf6.4zUIojn2nzkrpzDWlkQtDSWPcLa6R + headers: + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Type: + - application/x-www-form-urlencoded + Content-Length: + - '199' + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Expires: + - "-1" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains + X-Content-Type-Options: + - nosniff + P3p: + - CP="DSP CUR OTPi IND OTRi ONL FIN" + X-Ms-Request-Id: + - 61d16d60-7b4e-4d46-a540-824540685000 + X-Ms-Ests-Server: + - 2.1.20465.4 - FRC ProdSlices + X-Ms-Srs: + - 1.P + Content-Security-Policy-Report-Only: + - object-src 'none'; base-uri 'self'; script-src 'self' 'nonce-Qu0I7ShVrVt-zJPL9d9rnw' + 'unsafe-inline' 'unsafe-eval' https://*.msauth.net https://*.msftauth.net + https://*.msftauthimages.net https://*.msauthimages.net https://*.msidentity.com + https://*.microsoftonline-p.com https://*.microsoftazuread-sso.com https://*.azureedge.net + https://*.outlook.com https://*.office.com https://*.office365.com https://*.microsoft.com + https://*.bing.com 'report-sample'; report-uri https://csp.microsoft.com/report/ESTS-UX-All + X-Xss-Protection: + - '0' + Set-Cookie: + - fpc=AsYLEKhH9htOoOEoaz18W89RVKK0AQAAAPSQiN8OAAAA; expires=Fri, 09-May-2025 + 16:01:57 GMT; path=/; secure; HttpOnly; SameSite=None, x-ms-gateway-slice=estsfd; + path=/; secure; samesite=none; httponly, stsservicecookie=estsfd; path=/; + secure; samesite=none; httponly + Date: + - Wed, 09 Apr 2025 16:01:56 GMT + Content-Length: + - '1696' + body: + encoding: UTF-8 + string: '{"token_type":"Bearer","expires_in":3599,"ext_expires_in":3599,"access_token":""}' + recorded_at: Wed, 09 Apr 2025 16:01:57 GMT +- request: + method: post + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children + body: + encoding: UTF-8 + string: '{"name":"ConnectionValidatorFolder","folder":{},"@microsoft.graph.conflictBehavior":"fail"}' + headers: + Content-Type: + - application/json + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '91' + Authorization: + - Bearer + response: + status: + code: 201 + message: Created + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8 + Content-Encoding: + - gzip + Etag: + - '"{14A012EF-8F6E-4AA3-B137-F12EC82C13C2},1"' + Location: + - https://ymt6d.sharepoint.com/_api/v2.0/drives('b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs')/items('root')/children('01ANJ53W7PCKQBI3UPUNFLCN7RF3ECYE6C') + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 6715ccb0-ed22-4c02-ad8c-3c71d3597a83 + Client-Request-Id: + - 6715ccb0-ed22-4c02-ad8c-3c71d3597a83 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"003","RoleInstance":"FR1PEPF00001088"}}' + Odata-Version: + - '4.0' + Date: + - Wed, 09 Apr 2025 16:01:57 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#drives(''b%21FeOZEMfQx0eGQKqVBLcP__BG8mq-4-9FuRqOyk3MXY8CfNaHr_0ERYs5kgmEWFrX'')/root/children/$entity","@odata.etag":"\"{14A012EF-8F6E-4AA3-B137-F12EC82C13C2},1\"","createdDateTime":"2025-04-09T16:01:58Z","eTag":"\"{14A012EF-8F6E-4AA3-B137-F12EC82C13C2},1\"","id":"01ANJ53W7PCKQBI3UPUNFLCN7RF3ECYE6C","lastModifiedDateTime":"2025-04-09T16:01:58Z","name":"ConnectionValidatorFolder","size":0,"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20AMPF/ConnectionValidatorFolder","cTag":"\"c:{14A012EF-8F6E-4AA3-B137-F12EC82C13C2},0\"","commentSettings":{"commentingDisabled":{"isDisabled":false}},"createdBy":{"application":{"displayName":"OP + Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"lastModifiedBy":{"application":{"displayName":"OP Auth","id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff"},"user":{"displayName":"SharePoint + App"}},"parentReference":{"driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","driveType":"documentLibrary","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","sharepointIds":{"listId":"87d67c02-fdaf-4504-8b39-920984585ad7","listItemUniqueId":"458e6945-3722-40b1-946f-4743a2d480f2","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff","siteUrl":"https://ymt6d.sharepoint.com/sites/OPTest","tenantId":"4d44bf36-9b56-45c0-8807-bbf386dd047f","webId":"6af246f0-e3be-45ef-b91a-8eca4dcc5d8f"}},"fileSystemInfo":{"createdDateTime":"2025-04-09T16:01:58Z","lastModifiedDateTime":"2025-04-09T16:01:58Z"},"folder":{"childCount":0,"view":{"sortBy":"name","sortOrder":"ascending","viewType":"thumbnails"}},"shared":{"scope":"unknown"}}' + recorded_at: Wed, 09 Apr 2025 16:01:57 GMT +- request: + method: post + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children + body: + encoding: UTF-8 + string: '{"name":"ConnectionValidatorFolder","folder":{},"@microsoft.graph.conflictBehavior":"fail"}' + headers: + Content-Type: + - application/json + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + Content-Length: + - '91' + response: + status: + code: 409 + message: Conflict + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json + Content-Encoding: + - gzip + Vary: + - Accept-Encoding + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 8ae2e5a3-51d0-4212-a282-f42474ecd061 + Client-Request-Id: + - 8ae2e5a3-51d0-4212-a282-f42474ecd061 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"003","RoleInstance":"FR1PEPF0000161B"}}' + Date: + - Wed, 09 Apr 2025 16:01:57 GMT + body: + encoding: UTF-8 + string: '{"error":{"code":"nameAlreadyExists","message":"Name already exists","innerError":{"date":"2025-04-09T16:01:58","request-id":"8ae2e5a3-51d0-4212-a282-f42474ecd061","client-request-id":"8ae2e5a3-51d0-4212-a282-f42474ecd061"}}}' + recorded_at: Wed, 09 Apr 2025 16:01:58 GMT +- request: + method: get + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root/children?$select=id,name,size,webUrl,lastModifiedBy,createdBy,fileSystemInfo,file,folder,parentReference + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-store, no-cache + Content-Type: + - application/json; odata.metadata=minimal; odata.streaming=true; IEEE754Compatible=false; + charset=utf-8 + Content-Encoding: + - gzip + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - c5e05cfa-d4d1-4fed-9af1-3d99d5182905 + Client-Request-Id: + - c5e05cfa-d4d1-4fed-9af1-3d99d5182905 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"003","RoleInstance":"FR1PEPF00000AF1"}}' + Date: + - Wed, 09 Apr 2025 16:01:58 GMT + body: + encoding: UTF-8 + string: '{"@odata.context":"https://graph.microsoft.com/v1.0/$metadata#Collection(driveItem)","value":[{"@odata.etag":"\"{14A012EF-8F6E-4AA3-B137-F12EC82C13C2},2\"","createdBy":{"application":{"id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff","displayName":"OP + Auth"},"user":{"displayName":"SharePoint App"}},"id":"01ANJ53W7PCKQBI3UPUNFLCN7RF3ECYE6C","lastModifiedBy":{"application":{"id":"b8a5bb54-5fb2-4e0e-9427-9d24dbac32ff","displayName":"OP + Auth"},"user":{"displayName":"SharePoint App"}},"name":"ConnectionValidatorFolder","parentReference":{"driveType":"documentLibrary","driveId":"b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs","id":"01ANJ53W56Y2GOVW7725BZO354PWSELRRZ","name":"Marcello + AMPF","path":"/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/root:","siteId":"1099e315-d0c7-47c7-8640-aa9504b70fff"},"webUrl":"https://ymt6d.sharepoint.com/sites/OPTest/Marcello%20AMPF/ConnectionValidatorFolder","fileSystemInfo":{"createdDateTime":"2025-04-09T16:01:58Z","lastModifiedDateTime":"2025-04-09T16:01:58Z"},"folder":{"childCount":0},"size":0}]}' + recorded_at: Wed, 09 Apr 2025 16:01:58 GMT +- request: + method: delete + uri: https://graph.microsoft.com/v1.0/drives/b!dmVLG22QlE2PSW0AqVB7UOhZ8n7tjkVGkgqLNnuw2OBb-brzKzZAR4DYT1k9KPXs/items/01ANJ53W7PCKQBI3UPUNFLCN7RF3ECYE6C + body: + encoding: US-ASCII + string: '' + headers: + Authorization: + - Bearer + User-Agent: + - httpx.rb/1.3.4 + Accept: + - "*/*" + Accept-Encoding: + - gzip, deflate + response: + status: + code: 204 + message: No Content + headers: + Cache-Control: + - no-store, no-cache + Strict-Transport-Security: + - max-age=31536000 + Request-Id: + - 60c4b5b1-ff85-4d07-abdf-9255511eec30 + Client-Request-Id: + - 60c4b5b1-ff85-4d07-abdf-9255511eec30 + X-Ms-Ags-Diagnostic: + - '{"ServerInfo":{"DataCenter":"Germany West Central","Slice":"E","Ring":"4","ScaleUnit":"003","RoleInstance":"FR1PEPF00001213"}}' + Date: + - Wed, 09 Apr 2025 16:01:58 GMT + body: + encoding: UTF-8 + string: '' + recorded_at: Wed, 09 Apr 2025 16:01:59 GMT +recorded_with: VCR 6.3.1 diff --git a/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb b/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb index 6b04456e264..fb39836b507 100644 --- a/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb +++ b/modules/team_planner/app/controllers/team_planner/team_planner_controller.rb @@ -82,8 +82,6 @@ module ::TeamPlanner @view = Query .visible(current_user) .find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 end def visible_plans(project = nil) diff --git a/modules/team_planner/app/views/team_planner/team_planner/upsale.html.erb b/modules/team_planner/app/views/team_planner/team_planner/upsale.html.erb index 97b1af78182..bcd94d818e6 100644 --- a/modules/team_planner/app/views/team_planner/team_planner/upsale.html.erb +++ b/modules/team_planner/app/views/team_planner/team_planner/upsale.html.erb @@ -1,10 +1,8 @@ -<% html_title(t(:label_administration), t("team_planner.upsale.title")) -%> +<% html_title(t(:label_administration), t("ee.upsale.team_planner_view.title")) -%> -<%= render template: "common/upsale", - locals: { - feature_title: t("team_planner.upsale.title"), - feature_description: t("team_planner.upsale.description"), - feature_reference: "team_planner", - feature_video: "enterprise/team-planner-animation.mp4", - additional_classes: "upsale-notification_hide-breadcrumb" - } %> +<%= + render EnterpriseEdition::UpsalePageComponent.new( + :team_planner_view, + video: "enterprise/team-planner-animation.mp4" + ) +%> diff --git a/modules/team_planner/config/locales/crowdin/af.yml b/modules/team_planner/config/locales/crowdin/af.yml index 4e1f6fae8ef..4bf6d41f374 100644 --- a/modules/team_planner/config/locales/crowdin/af.yml +++ b/modules/team_planner/config/locales/crowdin/af.yml @@ -6,12 +6,16 @@ af: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Spanbeplanner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Spanbeplanner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/ar.yml b/modules/team_planner/config/locales/crowdin/ar.yml index 275dedc6106..f9df89377e8 100644 --- a/modules/team_planner/config/locales/crowdin/ar.yml +++ b/modules/team_planner/config/locales/crowdin/ar.yml @@ -6,12 +6,16 @@ ar: permission_view_team_planner: "عرض مخطط الفريق" permission_manage_team_planner: "إدارة مخطط الفريق" project_module_team_planner_view: "مخططي الفريق" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "مخطط الفريق" label_new_team_planner: "New team planner" label_create_new_team_planner: "إنشاء مخطط فريق جديد" label_team_planner_plural: "مخططي الفريق" label_assignees: "المعينون" - upsale: - title: "مخطط الفريق" - description: "احصل على نظرة عامة كاملة لتخطيط فريقك مع فريق تخطيط العمل. تمديد, اختصار و سحب و إسقاط مجموعات العمل لتعديل التواريخ أو نقلها أو تغيير المحال إليهم." diff --git a/modules/team_planner/config/locales/crowdin/az.yml b/modules/team_planner/config/locales/crowdin/az.yml index 28935738b38..4873bf3b977 100644 --- a/modules/team_planner/config/locales/crowdin/az.yml +++ b/modules/team_planner/config/locales/crowdin/az.yml @@ -6,12 +6,16 @@ az: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Təyin edilənlər" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/be.yml b/modules/team_planner/config/locales/crowdin/be.yml index 6aef6ef8c09..e4f38e77437 100644 --- a/modules/team_planner/config/locales/crowdin/be.yml +++ b/modules/team_planner/config/locales/crowdin/be.yml @@ -6,12 +6,16 @@ be: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/bg.yml b/modules/team_planner/config/locales/crowdin/bg.yml index 509770de05d..e049c826303 100644 --- a/modules/team_planner/config/locales/crowdin/bg.yml +++ b/modules/team_planner/config/locales/crowdin/bg.yml @@ -6,12 +6,16 @@ bg: permission_view_team_planner: "Вижте екипния плановик" permission_manage_team_planner: "Управлявайте екипния плановик" project_module_team_planner_view: "Екипни плановици" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Екипен плановик" label_new_team_planner: "Нов екипен плановик" label_create_new_team_planner: "Създайте нов екипен плановик" label_team_planner_plural: "Екипни плановици" label_assignees: "Правоприемници" - upsale: - title: "Екипен плановик" - description: "Получете пълен преглед на планирането на вашия екип с Team Planner. Разтягайте, съкращавайте и плъзгайте и пускайте работни пакети, за да промените датите, да ги преместите или да промените възложителите." diff --git a/modules/team_planner/config/locales/crowdin/ca.yml b/modules/team_planner/config/locales/crowdin/ca.yml index 4c03756f764..3097a0629ab 100644 --- a/modules/team_planner/config/locales/crowdin/ca.yml +++ b/modules/team_planner/config/locales/crowdin/ca.yml @@ -6,12 +6,16 @@ ca: permission_view_team_planner: "Veure planificador d'equips" permission_manage_team_planner: "Administrar planificador d'equips" project_module_team_planner_view: "Planificadors d'equips" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Planificador d'equips" label_new_team_planner: "Nou planificador d'equips" label_create_new_team_planner: "Crea un nou planificador d'equips" label_team_planner_plural: "Planificadors d'equips" label_assignees: "Usuaris assignats" - upsale: - title: "Planificador d'equips" - description: "Obté una visió general completa del teu planificador d'equips amb el Planificador d'Equips. Estira, escurça i arrossega paquets de treball per modificar dates, moure-les o canviar l'usuari assignat. " diff --git a/modules/team_planner/config/locales/crowdin/ckb-IR.yml b/modules/team_planner/config/locales/crowdin/ckb-IR.yml index 5b23223d258..a5179784496 100644 --- a/modules/team_planner/config/locales/crowdin/ckb-IR.yml +++ b/modules/team_planner/config/locales/crowdin/ckb-IR.yml @@ -6,12 +6,16 @@ ckb-IR: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/cs.yml b/modules/team_planner/config/locales/crowdin/cs.yml index e3358031d5b..49c45c9d53f 100644 --- a/modules/team_planner/config/locales/crowdin/cs.yml +++ b/modules/team_planner/config/locales/crowdin/cs.yml @@ -6,12 +6,16 @@ cs: permission_view_team_planner: "Zobrazit plánovač týmu" permission_manage_team_planner: "Spravovat plánovač týmu" project_module_team_planner_view: "Týmové plánovače" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Týmový plánovač" label_new_team_planner: "Nový plánovač týmu" label_create_new_team_planner: "Vytvořit nový týmový plánovač" label_team_planner_plural: "Týmové plánovače" label_assignees: "Řešitelé" - upsale: - title: "Týmový plánovač" - description: "Získejte kompletní přehled plánování týmu s týmovým plánovačem. Roztáhněte, zkrátte a přetáhněte pracovní balíčky pro úpravu datumů, přesuňte je nebo změňte řešitele." diff --git a/modules/team_planner/config/locales/crowdin/da.yml b/modules/team_planner/config/locales/crowdin/da.yml index f8b5433a61f..5abd919bd25 100644 --- a/modules/team_planner/config/locales/crowdin/da.yml +++ b/modules/team_planner/config/locales/crowdin/da.yml @@ -6,12 +6,16 @@ da: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/de.yml b/modules/team_planner/config/locales/crowdin/de.yml index 8605a136e35..d13b3baa060 100644 --- a/modules/team_planner/config/locales/crowdin/de.yml +++ b/modules/team_planner/config/locales/crowdin/de.yml @@ -6,12 +6,16 @@ de: permission_view_team_planner: "Teamplaner anzeigen" permission_manage_team_planner: "Teamplaner verwalten" project_module_team_planner_view: "Teamplaner" + ee: + feature_names: + team_planner_view: "Teamplaner" + upsale: + team_planner_view: + title: "Teamplaner" + description: "Erhalten Sie einen kompletten Überblick über die Planung Ihres Teams mit dem Teamplaner. Strecken, verkürzen und ziehen Sie Arbeitspakete zur Änderung von Terminen. Verschieben Sie Arbeitspakete oder ändern Sie die zugewiesenen Benutzer." team_planner: label_team_planner: "Teamplaner" label_new_team_planner: "Neuer Teamplaner" label_create_new_team_planner: "Neuen Teamplaner erstellen" label_team_planner_plural: "Teamplaner" label_assignees: "Zugewiesene Benutzer" - upsale: - title: "Teamplaner" - description: "Erhalten Sie einen kompletten Überblick über die Planung Ihres Teams mit dem Teamplaner. Strecken, verkürzen und ziehen Sie Arbeitspakete zur Änderung von Terminen, Verschieben Sie Arbeitspakete oder Ändern Sie die zugewiesenen Benutzer." diff --git a/modules/team_planner/config/locales/crowdin/el.yml b/modules/team_planner/config/locales/crowdin/el.yml index a64c05f1b55..359ebc9c439 100644 --- a/modules/team_planner/config/locales/crowdin/el.yml +++ b/modules/team_planner/config/locales/crowdin/el.yml @@ -6,12 +6,16 @@ el: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/eo.yml b/modules/team_planner/config/locales/crowdin/eo.yml index cd3ad5690b3..7763ba33dbf 100644 --- a/modules/team_planner/config/locales/crowdin/eo.yml +++ b/modules/team_planner/config/locales/crowdin/eo.yml @@ -6,12 +6,16 @@ eo: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/es.yml b/modules/team_planner/config/locales/crowdin/es.yml index aec978d650e..ba7a138a43e 100644 --- a/modules/team_planner/config/locales/crowdin/es.yml +++ b/modules/team_planner/config/locales/crowdin/es.yml @@ -6,12 +6,16 @@ es: permission_view_team_planner: "Ver planificador del equipo" permission_manage_team_planner: "Gestionar planificador del equipo" project_module_team_planner_view: "Planificadores de equipos" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Planificador de equipo" label_new_team_planner: "Nuevo planificador de equipos" label_create_new_team_planner: "Crear planificador de equipo" label_team_planner_plural: "Planificadores de equipos" label_assignees: "Asignado a" - upsale: - title: "Planificador de equipo" - description: "Obtén una visión completa de la planificación de tu equipo con el Planificador de Equipo. Alargar, acortar y mover paquetes de trabajo para modificar fechas o cambiar asignados." diff --git a/modules/team_planner/config/locales/crowdin/et.yml b/modules/team_planner/config/locales/crowdin/et.yml index c7bf66f3b59..0316e587a79 100644 --- a/modules/team_planner/config/locales/crowdin/et.yml +++ b/modules/team_planner/config/locales/crowdin/et.yml @@ -6,12 +6,16 @@ et: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/eu.yml b/modules/team_planner/config/locales/crowdin/eu.yml index e6510830d73..30855a00fd6 100644 --- a/modules/team_planner/config/locales/crowdin/eu.yml +++ b/modules/team_planner/config/locales/crowdin/eu.yml @@ -6,12 +6,16 @@ eu: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/fa.yml b/modules/team_planner/config/locales/crowdin/fa.yml index 6a01f18b547..7f33d8e5727 100644 --- a/modules/team_planner/config/locales/crowdin/fa.yml +++ b/modules/team_planner/config/locales/crowdin/fa.yml @@ -6,12 +6,16 @@ fa: permission_view_team_planner: "مشاهده برنامه ریز تیم" permission_manage_team_planner: "مدیریت برنامه ریز تیم" project_module_team_planner_view: "برنامه ریزهای تیم" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "برنامه ریز تیم" label_new_team_planner: "برنامه ریز تیم جدید" label_create_new_team_planner: "ایجاد برنامه ریز جدید تیم" label_team_planner_plural: "برنامه ریزهای تیم" label_assignees: "نمایندگان" - upsale: - title: "برنامه ریز تیم" - description: "یک نمای کلی از برنامه ریزی تیم با استفاده از برنامه ریز تیم دریافت کنید. به منظور اصلاح تاریخ، جابه جا کردن بسته های کاری و یا تغییر نمایندگان از بسط دادن، کوتاه کردن و یا کشیدن و رها کردن آنها استفاده کنید." diff --git a/modules/team_planner/config/locales/crowdin/fi.yml b/modules/team_planner/config/locales/crowdin/fi.yml index 04aa440bfcf..d96f093b414 100644 --- a/modules/team_planner/config/locales/crowdin/fi.yml +++ b/modules/team_planner/config/locales/crowdin/fi.yml @@ -6,12 +6,16 @@ fi: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/fil.yml b/modules/team_planner/config/locales/crowdin/fil.yml index 90c8fee95bd..2037e022b0c 100644 --- a/modules/team_planner/config/locales/crowdin/fil.yml +++ b/modules/team_planner/config/locales/crowdin/fil.yml @@ -6,12 +6,16 @@ fil: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/fr.yml b/modules/team_planner/config/locales/crowdin/fr.yml index a9bd27c7593..0017080a3be 100644 --- a/modules/team_planner/config/locales/crowdin/fr.yml +++ b/modules/team_planner/config/locales/crowdin/fr.yml @@ -6,12 +6,16 @@ fr: permission_view_team_planner: "Voir le planificateur d'équipe" permission_manage_team_planner: "Gérer le planificateur d'équipe" project_module_team_planner_view: "Planificateurs d'équipe" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Planificateur d'équipe" label_new_team_planner: "Nouveau planificateur d'équipe" label_create_new_team_planner: "Créer un nouveau planificateur d'équipe" label_team_planner_plural: "Planificateurs d'équipe" label_assignees: "Responsables" - upsale: - title: "Planificateur d'équipe" - description: "Obtenez un aperçu complet du planning de votre équipe avec le planificateur d'équipe. Allongez, raccourcissez et faites glisser les paquets de travail pour modifier les dates, les déplacer ou changer les assignés." diff --git a/modules/team_planner/config/locales/crowdin/he.yml b/modules/team_planner/config/locales/crowdin/he.yml index cd8c9c551a0..65dd0fa14ef 100644 --- a/modules/team_planner/config/locales/crowdin/he.yml +++ b/modules/team_planner/config/locales/crowdin/he.yml @@ -6,12 +6,16 @@ he: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "מתכנן צוות" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "משויך אל" - upsale: - title: "מתכנן צוות" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/hi.yml b/modules/team_planner/config/locales/crowdin/hi.yml index 7ee822bd6be..a2428ccacfc 100644 --- a/modules/team_planner/config/locales/crowdin/hi.yml +++ b/modules/team_planner/config/locales/crowdin/hi.yml @@ -6,12 +6,16 @@ hi: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/hr.yml b/modules/team_planner/config/locales/crowdin/hr.yml index 2c3e89110a3..caaf53c40fb 100644 --- a/modules/team_planner/config/locales/crowdin/hr.yml +++ b/modules/team_planner/config/locales/crowdin/hr.yml @@ -6,12 +6,16 @@ hr: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/hu.yml b/modules/team_planner/config/locales/crowdin/hu.yml index b23388e04fd..65191489a3f 100644 --- a/modules/team_planner/config/locales/crowdin/hu.yml +++ b/modules/team_planner/config/locales/crowdin/hu.yml @@ -6,12 +6,16 @@ hu: permission_view_team_planner: "Csoport tervező megtekintése" permission_manage_team_planner: "Csoport tervező kezelése" project_module_team_planner_view: "Csoport tervezők" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Csoport tervező" label_new_team_planner: "Új csapattervező" label_create_new_team_planner: "Új csoport tervező létrehozása" label_team_planner_plural: "Csoport tervezők" label_assignees: "Megbízottak" - upsale: - title: "Csoport tervező" - description: "Szerezz áttekintést arról, hogy a csapatod mit tervez a Csapattervezővel. Nyújtsd, rövidítsd, fogd és dobd a munkacsomagokat, hogy módosítsd az időpontokat, áthelyezd őket vagy megváltoztasd a hozzárendelt felhasználót." diff --git a/modules/team_planner/config/locales/crowdin/id.yml b/modules/team_planner/config/locales/crowdin/id.yml index 84c9838dfa2..d51bf3363a8 100644 --- a/modules/team_planner/config/locales/crowdin/id.yml +++ b/modules/team_planner/config/locales/crowdin/id.yml @@ -6,12 +6,16 @@ id: permission_view_team_planner: "Lihat rencana tim" permission_manage_team_planner: "Kelola rencana tim" project_module_team_planner_view: "Rencana tim" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Rencana tim" label_new_team_planner: "Perencana tim baru" label_create_new_team_planner: "Buat rencana tim baru" label_team_planner_plural: "Rencana tim" label_assignees: "Penerima tugas" - upsale: - title: "Rencana tim" - description: "Dapatkan ikhtisar lengkap tentang perencanaan tim Anda dengan Team Planner. Rentangkan, perpendek, dan seret dan lepas paket kerja untuk mengubah tanggal, memindahkannya, atau mengubah penerima tugas." diff --git a/modules/team_planner/config/locales/crowdin/it.yml b/modules/team_planner/config/locales/crowdin/it.yml index 47acdd08f40..c070ee68781 100644 --- a/modules/team_planner/config/locales/crowdin/it.yml +++ b/modules/team_planner/config/locales/crowdin/it.yml @@ -6,12 +6,16 @@ it: permission_view_team_planner: "Visualizza il pianificatore di team" permission_manage_team_planner: "Gestisci il pianificatore di team" project_module_team_planner_view: "Organizzatori di gruppo" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Pianificatore di team" label_new_team_planner: "Nuovo pianificatore di team" label_create_new_team_planner: "Crea nuovo pianificatore di team" label_team_planner_plural: "Organizzatori di gruppo" label_assignees: "Assegnatari" - upsale: - title: "Pianificatore di team" - description: "Ottieni una panoramica completa della pianificazione del tuo team con Team Planner. Allunga, accorcia e sposta le macro-attività per modificare le date, riprogrammarle o cambiare gli assegnatari." diff --git a/modules/team_planner/config/locales/crowdin/ja.yml b/modules/team_planner/config/locales/crowdin/ja.yml index e127814312a..497857532a3 100644 --- a/modules/team_planner/config/locales/crowdin/ja.yml +++ b/modules/team_planner/config/locales/crowdin/ja.yml @@ -6,12 +6,16 @@ ja: permission_view_team_planner: "チームプランナーを表示" permission_manage_team_planner: "チームプランナーの管理" project_module_team_planner_view: "チーム プランナー" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "チーム プランナー" label_new_team_planner: "新しいチームプランナー" label_create_new_team_planner: "新しいチームプランナーを作成" label_team_planner_plural: "チーム プランナー" label_assignees: "担当者" - upsale: - title: "チーム プランナー" - description: "Team Plannerでチームの計画の概要を把握しましょう。クリックやドラッグで作業項目を編集したり、日付を変更したり、担当者を変更したりできます。" diff --git a/modules/team_planner/config/locales/crowdin/js-fr.yml b/modules/team_planner/config/locales/crowdin/js-fr.yml index 695779007b4..b0450fae0c8 100644 --- a/modules/team_planner/config/locales/crowdin/js-fr.yml +++ b/modules/team_planner/config/locales/crowdin/js-fr.yml @@ -18,7 +18,7 @@ fr: today: 'Aujourd''hui' drag_here_to_remove: 'Faites glisser ici pour supprimer le responsable et les dates de début et de fin.' cannot_drag_here: 'Impossible de supprimer le lot de travaux en raison des autorisations ou des restrictions d''édition.' - cannot_drag_to_non_working_day: 'Ce lot de travaux ne peut pas démarrer/terminer sur un jour non ouvré.' + cannot_drag_to_non_working_day: 'Ce lot de travail ne peut pas démarrer/terminer sur un jour non ouvré.' quick_add: empty_state: 'Utilisez le champ de recherche pour trouver des lots de travaux et faites-les glisser vers le planificateur pour l''assigner à quelqu''un et définir des dates de début et de fin.' search_placeholder: 'Rechercher...' diff --git a/modules/team_planner/config/locales/crowdin/ka.yml b/modules/team_planner/config/locales/crowdin/ka.yml index 35c66428490..b9171c67f70 100644 --- a/modules/team_planner/config/locales/crowdin/ka.yml +++ b/modules/team_planner/config/locales/crowdin/ka.yml @@ -6,12 +6,16 @@ ka: permission_view_team_planner: "გუნდის მგეგმავის ნახვა" permission_manage_team_planner: "გუნდის მგეგმავის მართვა" project_module_team_planner_view: "გუნდის მგეგმავები" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "გუნდის მგეგმავი" label_new_team_planner: "ახალი გუნდის მგეგმავი" label_create_new_team_planner: "ახალი გუნდის მგეგმავის შექმნა" label_team_planner_plural: "გუნდის მგეგმავები" label_assignees: "მიმნიჭებლები" - upsale: - title: "გუნდის მგეგმავი" - description: "მიიღეთ მთელი თქვენი გუნდის გეგმების მიმოხილვა მგეგმავთან ერთად. გაწელეთ, შეამოკლეთ და გადაათრიეთ სამუშაოს პაკეტები, მათი თარიღების შესაცვლელად, გადასაადგილებლად ან მიმნიჭებლის შესაცვლელად." diff --git a/modules/team_planner/config/locales/crowdin/kk.yml b/modules/team_planner/config/locales/crowdin/kk.yml index 1b7fb3bb702..269e0f7c232 100644 --- a/modules/team_planner/config/locales/crowdin/kk.yml +++ b/modules/team_planner/config/locales/crowdin/kk.yml @@ -6,12 +6,16 @@ kk: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/ko.yml b/modules/team_planner/config/locales/crowdin/ko.yml index 97a7c335943..91d5ba5b105 100644 --- a/modules/team_planner/config/locales/crowdin/ko.yml +++ b/modules/team_planner/config/locales/crowdin/ko.yml @@ -6,12 +6,16 @@ ko: permission_view_team_planner: "팀 플래너 보기" permission_manage_team_planner: "팀 플래너 관리" project_module_team_planner_view: "팀 플래너" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "팀 플래너" label_new_team_planner: "새 팀 플래너" label_create_new_team_planner: "새로운 팀 플래너 만들기" label_team_planner_plural: "팀 플래너" label_assignees: "담당자" - upsale: - title: "팀 플래너" - description: "팀 플래너를 사용하여 팀 계획에 대한 전체 개요를 확인하세요. 작업 패키지를 늘리거나 줄이고 끌어다 놓아 날짜를 수정하거나 이동하거나 담당자를 변경하세요." diff --git a/modules/team_planner/config/locales/crowdin/lt.yml b/modules/team_planner/config/locales/crowdin/lt.yml index 02c372926e4..bc396be33da 100644 --- a/modules/team_planner/config/locales/crowdin/lt.yml +++ b/modules/team_planner/config/locales/crowdin/lt.yml @@ -6,12 +6,16 @@ lt: permission_view_team_planner: "Žiūrėti komandos planą" permission_manage_team_planner: "Tvarkyti komandos planą" project_module_team_planner_view: "Komandos planai" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Komandos planas" label_new_team_planner: "Naujas komandos planas" label_create_new_team_planner: "Kurti naują komandos planą" label_team_planner_plural: "Komandos planai" label_assignees: "Paskirtieji" - upsale: - title: "Komandos planas" - description: "Gaukite pilną jūsų komandos plano vaizdą su Team Planner. Pakeiskite datas prailgindami, sutrumpindami ar nuvilkdami darbo paketus, perkelkite juos ar pakeiskite paskirtuosius." diff --git a/modules/team_planner/config/locales/crowdin/lv.yml b/modules/team_planner/config/locales/crowdin/lv.yml index a60592e31a0..bebef64e75b 100644 --- a/modules/team_planner/config/locales/crowdin/lv.yml +++ b/modules/team_planner/config/locales/crowdin/lv.yml @@ -6,12 +6,16 @@ lv: permission_view_team_planner: "Apskatīt komandas plānotāju" permission_manage_team_planner: "Pārvaldīt komandas plānotāju" project_module_team_planner_view: "Komandas plānotāji" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Komandas plānotājs" label_new_team_planner: "Jauns komandas plānotājs" label_create_new_team_planner: "Izveidot jaunu komandas plānotāju" label_team_planner_plural: "Komandas plānotāji" label_assignees: "Atbildīgais" - upsale: - title: "Komandas plānotājs" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/mn.yml b/modules/team_planner/config/locales/crowdin/mn.yml index 5365c7f03c7..355a64e6183 100644 --- a/modules/team_planner/config/locales/crowdin/mn.yml +++ b/modules/team_planner/config/locales/crowdin/mn.yml @@ -6,12 +6,16 @@ mn: permission_view_team_planner: "Багийн төлөвлөгчийг харах" permission_manage_team_planner: "Багийн төлөвлөгчийг удирдах" project_module_team_planner_view: "Багийн төлөвлөгчид" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Багийн төлөвлөгч" label_new_team_planner: "Шинэ багийн төлөвлөгч" label_create_new_team_planner: "Шинэ баг төлөвлөгч үүсгэх" label_team_planner_plural: "Багийн төлөвлөгчид" label_assignees: "Томилогдсон хүмүүс" - upsale: - title: "Багийн төлөвлөгч" - description: "Багийн төлөвлөгч-ийн тусламжтайгаар багийнхаа төлөвлөлтийн талаарх бүрэн танилцуулгатай танилцаарай. Огноог өөрчлөх, зөөх эсвэл томилогдсон хүмүүсийг өөрчлөхийн тулд ажлын багцуудыг сунгаж, богиносгож, чирэх, буулгах боломжтой." diff --git a/modules/team_planner/config/locales/crowdin/ms.yml b/modules/team_planner/config/locales/crowdin/ms.yml index a8d068ff59e..c5c833cf3e9 100644 --- a/modules/team_planner/config/locales/crowdin/ms.yml +++ b/modules/team_planner/config/locales/crowdin/ms.yml @@ -6,12 +6,16 @@ ms: permission_view_team_planner: "Lihat perancang pasukan" permission_manage_team_planner: "Urus perancang pasukan" project_module_team_planner_view: "Perancang pasukan" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Perancang pasukan" label_new_team_planner: "Perancang pasukan baharu" label_create_new_team_planner: "Cipta perancang pasukan baharu" label_team_planner_plural: "Perancang pasukan" label_assignees: "Penerima tugasan" - upsale: - title: "Perancang pasukan" - description: "Dapatkan gambaran keseluruhan lengkap perancangan pasukan anda dengan Perancang Pasukan. Renggangkan, pendekkan, dan tarik dan lepas pakej kerja untuk mengubah tarikh, memindahkan mereka, atau menukar penerima tugasan." diff --git a/modules/team_planner/config/locales/crowdin/ne.yml b/modules/team_planner/config/locales/crowdin/ne.yml index ae0db63a872..da76e2ac8cb 100644 --- a/modules/team_planner/config/locales/crowdin/ne.yml +++ b/modules/team_planner/config/locales/crowdin/ne.yml @@ -6,12 +6,16 @@ ne: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/nl.yml b/modules/team_planner/config/locales/crowdin/nl.yml index a3b50c75f29..a7de8b4212b 100644 --- a/modules/team_planner/config/locales/crowdin/nl.yml +++ b/modules/team_planner/config/locales/crowdin/nl.yml @@ -6,12 +6,16 @@ nl: permission_view_team_planner: "Bekijk team planner" permission_manage_team_planner: "Teamplanner beheren" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "Nieuwe teamplanner" label_create_new_team_planner: "Maak een nieuwe teamplanner" label_team_planner_plural: "Team planners" label_assignees: "Toegewezen personen" - upsale: - title: "Team planner" - description: "Krijg een compleet overzicht van de planning van je team met Team Planner. Verleng, verkort en sleep-en-plaats werkpakketten om data te wijzigen, ze te verplaatsen of taakontvangers te veranderen." diff --git a/modules/team_planner/config/locales/crowdin/no.yml b/modules/team_planner/config/locales/crowdin/no.yml index ef969fe7592..5413225251a 100644 --- a/modules/team_planner/config/locales/crowdin/no.yml +++ b/modules/team_planner/config/locales/crowdin/no.yml @@ -6,12 +6,16 @@ permission_view_team_planner: "Vis teamplanlegger" permission_manage_team_planner: "Administrer teamplanlegger" project_module_team_planner_view: "Teamplanleggere" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Teamplanlegger" label_new_team_planner: "Ny teamplanlegger" label_create_new_team_planner: "Opprett ny teamplanlegger" label_team_planner_plural: "Teamplanleggere" label_assignees: "Tildelt" - upsale: - title: "Teamplanlegger" - description: "Få en fullstendig oversikt over ditt teams planlegging med Team Planner. Utvid, forkort og dra-og-slipp arbeidspakker for å endre datoer, flytte dem eller endre deltakere." diff --git a/modules/team_planner/config/locales/crowdin/pl.yml b/modules/team_planner/config/locales/crowdin/pl.yml index 62f6e277d62..223ba93433a 100644 --- a/modules/team_planner/config/locales/crowdin/pl.yml +++ b/modules/team_planner/config/locales/crowdin/pl.yml @@ -6,12 +6,16 @@ pl: permission_view_team_planner: "Wyświetlanie planisty zespołu" permission_manage_team_planner: "Zarządzanie planistą zespołu" project_module_team_planner_view: "Planiści zespołu" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Planista zespołu" label_new_team_planner: "Nowy planista zespołu" label_create_new_team_planner: "Utwórz nowego planistę zespołu" label_team_planner_plural: "Planiści zespołu" label_assignees: "Przypisane osoby" - upsale: - title: "Planista zespołu" - description: "Zdobądź pełny przegląd planowania swojej drużyny za pomocą Planisty zespołu. Rozciągnij, skróć i przeciągnij pakiety robocze, aby modyfikować daty, przenosić je lub zmieniać przypisane osoby." diff --git a/modules/team_planner/config/locales/crowdin/pt-BR.yml b/modules/team_planner/config/locales/crowdin/pt-BR.yml index 7206c4ba8df..92e9d5f2bd1 100644 --- a/modules/team_planner/config/locales/crowdin/pt-BR.yml +++ b/modules/team_planner/config/locales/crowdin/pt-BR.yml @@ -6,12 +6,16 @@ pt-BR: permission_view_team_planner: "Visualizar planejador de equipe" permission_manage_team_planner: "Gerenciar planejador de equipe" project_module_team_planner_view: "Planejador de equipe" + ee: + feature_names: + team_planner_view: "Planejador de equipe" + upsale: + team_planner_view: + title: "Planejador de equipe" + description: "Obtenha uma visão geral completa do planejamento da sua equipe com o Planejador de Equipe. Estique, encurte, e arraste e solte pacotes de trabalho para modificar datas, movê-los ou alterar os responsáveis." team_planner: label_team_planner: "Planejador de equipe" label_new_team_planner: "Novo planejador de equipe" label_create_new_team_planner: "Criar novo planejador de equipe" label_team_planner_plural: "Planejador de equipe" label_assignees: "Atribuídos para" - upsale: - title: "Planejador de equipe" - description: "Obtenha uma visão geral completa do planejamento da sua equipe com o Planejamento de equipe. Estique, encurte, e arraste e solte pacotes de trabalho para modificar datas, movê-los ou alterar os responsáveis." diff --git a/modules/team_planner/config/locales/crowdin/pt-PT.yml b/modules/team_planner/config/locales/crowdin/pt-PT.yml index 4d1150b1f11..06e35b6a768 100644 --- a/modules/team_planner/config/locales/crowdin/pt-PT.yml +++ b/modules/team_planner/config/locales/crowdin/pt-PT.yml @@ -6,12 +6,16 @@ pt-PT: permission_view_team_planner: "Ver planejador de equipe" permission_manage_team_planner: "Gerir planeador de equipa" project_module_team_planner_view: "Planeadores de equipa" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Planeador de equipa" label_new_team_planner: "Novo planeador de equipa" label_create_new_team_planner: "Criar novo planeador de equipa" label_team_planner_plural: "Planeadores de equipa" label_assignees: "Responsáveis" - upsale: - title: "Planeador de equipa" - description: "Obtenha uma visão geral completa do planeamento da sua equipa com o Planeamento de Equipa. Encurte e arraste e solte pacotes de trabalho para modificar datas, mova-as ou altera os responsáveis." diff --git a/modules/team_planner/config/locales/crowdin/ro.yml b/modules/team_planner/config/locales/crowdin/ro.yml index 4fe36642f3f..2a5f0ba21de 100644 --- a/modules/team_planner/config/locales/crowdin/ro.yml +++ b/modules/team_planner/config/locales/crowdin/ro.yml @@ -6,12 +6,16 @@ ro: permission_view_team_planner: "Vezi planificatorul echipei" permission_manage_team_planner: "Gestionează planificatorul de echipe" project_module_team_planner_view: "Planificare echipă" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Planificator echipă" label_new_team_planner: "Planificator echipă nou" label_create_new_team_planner: "Creează planificator echipă nou" label_team_planner_plural: "Planificare echipă" label_assignees: "Responsabili" - upsale: - title: "Planificator echipă" - description: "Obțineți o imagine de ansamblu completă a planificării echipei dumneavoastră cu Team Planner. Întindeți, scurtați și glisați pachetele de lucru pentru a modifica datele, pentru a le muta sau pentru a schimba destinatarii." diff --git a/modules/team_planner/config/locales/crowdin/ru.yml b/modules/team_planner/config/locales/crowdin/ru.yml index 81340f15395..ff78c094481 100644 --- a/modules/team_planner/config/locales/crowdin/ru.yml +++ b/modules/team_planner/config/locales/crowdin/ru.yml @@ -6,12 +6,16 @@ ru: permission_view_team_planner: "Просмотр планировщика команды" permission_manage_team_planner: "Управление планировщиком команды" project_module_team_planner_view: "Командные планировщики" + ee: + feature_names: + team_planner_view: "Командный планировщик" + upsale: + team_planner_view: + title: "Командный планировщик" + description: "Получите полный обзор планирования работы вашей команды с помощью Командного планировщика. Растягивайте, сокращайте и переносите пакеты работ, чтобы изменять даты, перемещать их или менять назначенных." team_planner: label_team_planner: "Командный планировщик" label_new_team_planner: "Новый командный планировщик" label_create_new_team_planner: "Создать новый командный планировщик" label_team_planner_plural: "Командные планировщики" label_assignees: "Исполнители" - upsale: - title: "Командный планировщик" - description: "Получите полный обзор планирования работы вашей команды с помощью Team Planner. Растягивайте, сокращайте и переносите пакеты работ, чтобы изменять даты, перемещать их или менять назначенных." diff --git a/modules/team_planner/config/locales/crowdin/rw.yml b/modules/team_planner/config/locales/crowdin/rw.yml index 600dc817cde..c674d1976a6 100644 --- a/modules/team_planner/config/locales/crowdin/rw.yml +++ b/modules/team_planner/config/locales/crowdin/rw.yml @@ -6,12 +6,16 @@ rw: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/si.yml b/modules/team_planner/config/locales/crowdin/si.yml index 60ed7c4ed1c..54bbdd8ad55 100644 --- a/modules/team_planner/config/locales/crowdin/si.yml +++ b/modules/team_planner/config/locales/crowdin/si.yml @@ -6,12 +6,16 @@ si: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/sk.yml b/modules/team_planner/config/locales/crowdin/sk.yml index 1e6aa2d1254..3ada7e70e7d 100644 --- a/modules/team_planner/config/locales/crowdin/sk.yml +++ b/modules/team_planner/config/locales/crowdin/sk.yml @@ -6,12 +6,16 @@ sk: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/sl.yml b/modules/team_planner/config/locales/crowdin/sl.yml index 09b334d2115..dceb93d3c95 100644 --- a/modules/team_planner/config/locales/crowdin/sl.yml +++ b/modules/team_planner/config/locales/crowdin/sl.yml @@ -6,12 +6,16 @@ sl: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Prevzemniki" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/sr.yml b/modules/team_planner/config/locales/crowdin/sr.yml index c7d872f4e27..357093efe8f 100644 --- a/modules/team_planner/config/locales/crowdin/sr.yml +++ b/modules/team_planner/config/locales/crowdin/sr.yml @@ -6,12 +6,16 @@ sr: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/sv.yml b/modules/team_planner/config/locales/crowdin/sv.yml index 6f7cd5fd687..356c019a2ae 100644 --- a/modules/team_planner/config/locales/crowdin/sv.yml +++ b/modules/team_planner/config/locales/crowdin/sv.yml @@ -6,12 +6,16 @@ sv: permission_view_team_planner: "Visa teamplanerare" permission_manage_team_planner: "Hantera teamplanerare" project_module_team_planner_view: "Team planerare" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planerare" label_new_team_planner: "Ny teamplanerare" label_create_new_team_planner: "Skapa ny teamplanerare" label_team_planner_plural: "Team planerare" label_assignees: "Utförare" - upsale: - title: "Team planerare" - description: "Få en fullständig överblick över ditt teams planering med Team Planner. Tänj ut, förkorta och dra-och-släpp arbetspaket för att ändra datum, flytta dem eller ändra utförare." diff --git a/modules/team_planner/config/locales/crowdin/th.yml b/modules/team_planner/config/locales/crowdin/th.yml index 63c20f256a5..fe8310d33e3 100644 --- a/modules/team_planner/config/locales/crowdin/th.yml +++ b/modules/team_planner/config/locales/crowdin/th.yml @@ -6,12 +6,16 @@ th: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/tr.yml b/modules/team_planner/config/locales/crowdin/tr.yml index 6ab6db15cf5..daea88772a7 100644 --- a/modules/team_planner/config/locales/crowdin/tr.yml +++ b/modules/team_planner/config/locales/crowdin/tr.yml @@ -6,12 +6,16 @@ tr: permission_view_team_planner: "Takım planlayıcıyı görüntüle" permission_manage_team_planner: "Takım planlayıcıyı yönet" project_module_team_planner_view: "Takım planlayıcıları" + ee: + feature_names: + team_planner_view: "Takım planlayıcısı" + upsale: + team_planner_view: + title: "Takım planlayıcısı" + description: "Takım Planlayıcısı ile ekibinizin planlamasına ilişkin eksiksiz bir genel bakış elde edin. Tarihleri değiştirmek, taşımak veya atananları değiştirmek için iş paketlerini genişletin, kısaltın ve sürükleyip bırakın." team_planner: label_team_planner: "Takım planlayıcısı" label_new_team_planner: "Yeni takım planlayıcı " label_create_new_team_planner: "Yeni takım planlayıcısı oluştur" label_team_planner_plural: "Takım planlayıcıları" label_assignees: "Atananlar" - upsale: - title: "Takım planlayıcısı" - description: "Takım Planlayıcısı ile ekibinizin planlamasına ilişkin eksiksiz bir genel bakış elde edin. Tarihleri değiştirmek, taşımak veya atananları değiştirmek için iş paketlerini genişletin, kısaltın ve sürükleyip bırakın." diff --git a/modules/team_planner/config/locales/crowdin/uk.yml b/modules/team_planner/config/locales/crowdin/uk.yml index fa4c9acff4a..78fc261e69c 100644 --- a/modules/team_planner/config/locales/crowdin/uk.yml +++ b/modules/team_planner/config/locales/crowdin/uk.yml @@ -6,12 +6,16 @@ uk: permission_view_team_planner: "Перегляд командного планувальника" permission_manage_team_planner: "Керування командним планувальником" project_module_team_planner_view: "Командні планувальники" + ee: + feature_names: + team_planner_view: "Командний планувальник" + upsale: + team_planner_view: + title: "Командний планувальник" + description: "Командний планувальник надає повний огляд планування вашої команди. Розтягуйте, скорочуйте й перетягуйте пакети робіт, щоб переміщувати їх, змінювати дати або перепризначати виконавців." team_planner: label_team_planner: "Командний планувальник" label_new_team_planner: "Новий командний планувальник" label_create_new_team_planner: "Створити новий командний планувальник" label_team_planner_plural: "Командні планувальники" label_assignees: "Виконавці" - upsale: - title: "Командний планувальник" - description: "Командний планувальник надає повний огляд планування вашої команди. Розтягуйте, скорочуйте та перетягуйте пакети робіт, щоб переміщувати їх, змінювати дати або перепризначати виконавців." diff --git a/modules/team_planner/config/locales/crowdin/uz.yml b/modules/team_planner/config/locales/crowdin/uz.yml index 6bf04b739f0..009cdf762bb 100644 --- a/modules/team_planner/config/locales/crowdin/uz.yml +++ b/modules/team_planner/config/locales/crowdin/uz.yml @@ -6,12 +6,16 @@ uz: permission_view_team_planner: "View team planner" permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/config/locales/crowdin/vi.yml b/modules/team_planner/config/locales/crowdin/vi.yml index af860dbd701..64ada7b243c 100644 --- a/modules/team_planner/config/locales/crowdin/vi.yml +++ b/modules/team_planner/config/locales/crowdin/vi.yml @@ -6,12 +6,16 @@ vi: permission_view_team_planner: "Xem kế hoạch nhóm" permission_manage_team_planner: "Quản lý kế hoạch nhóm" project_module_team_planner_view: "Kế hoạch nhóm" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." team_planner: label_team_planner: "Kế hoạch nhóm" label_new_team_planner: "Kế hoạch nhóm mới" label_create_new_team_planner: "Tạo kế hoạch nhóm mới" label_team_planner_plural: "Các kế hoạch nhóm" label_assignees: "Người được phân công" - upsale: - title: "Kế hoạch nhóm" - description: "Nhận cái nhìn tổng quan về kế hoạch của nhóm bạn với Kế hoạch Nhóm. Kéo dài, rút ngắn và kéo-thả các gói công việc để thay đổi ngày, di chuyển chúng hoặc thay đổi người được phân công." diff --git a/modules/team_planner/config/locales/crowdin/zh-CN.yml b/modules/team_planner/config/locales/crowdin/zh-CN.yml index f1fff3c5273..418a64a14fd 100644 --- a/modules/team_planner/config/locales/crowdin/zh-CN.yml +++ b/modules/team_planner/config/locales/crowdin/zh-CN.yml @@ -6,12 +6,16 @@ zh-CN: permission_view_team_planner: "查看工作组规划器" permission_manage_team_planner: "管理工作组规划器" project_module_team_planner_view: "工作组规划器" + ee: + feature_names: + team_planner_view: "工作组规划器" + upsale: + team_planner_view: + title: "工作组规划器" + description: "通过工作组规划器,您可以获得团队计划的完整概览。您可以拉伸、缩短和拖放工作包来修改日期、移动工作包或更改指定人。" team_planner: label_team_planner: "工作组规划器" label_new_team_planner: "新的工作组规划器" label_create_new_team_planner: "创建新的工作组规划器" label_team_planner_plural: "工作组规划器" label_assignees: "受理人" - upsale: - title: "工作组规划器" - description: "通过工作组规划器,您可以获得团队计划的完整概览。您可以拉伸、缩短和拖放工作包来修改日期、移动工作包或更改受理人。" diff --git a/modules/team_planner/config/locales/crowdin/zh-TW.yml b/modules/team_planner/config/locales/crowdin/zh-TW.yml index 1c11b3fd052..dad5635ab97 100644 --- a/modules/team_planner/config/locales/crowdin/zh-TW.yml +++ b/modules/team_planner/config/locales/crowdin/zh-TW.yml @@ -6,12 +6,16 @@ zh-TW: permission_view_team_planner: "查看團隊規劃" permission_manage_team_planner: "管理「團隊規劃」" project_module_team_planner_view: "查看團隊規劃" + ee: + feature_names: + team_planner_view: "團隊規劃" + upsale: + team_planner_view: + title: "團隊規劃" + description: "使用「團隊規劃器」全面掌握團隊的規劃狀況。您可以延長、縮短或拖放工作項目,以修改日期、移動位置或變更指派對象。" team_planner: label_team_planner: "團隊規劃" label_new_team_planner: "新團隊規劃" label_create_new_team_planner: "建立新的團隊規劃" label_team_planner_plural: "團隊規劃" label_assignees: "執行者" - upsale: - title: "團隊規劃" - description: "使用「團隊規劃」全面閱覽了解您的小組工作內容。 縮放、拖曳工作項目以便調整日期、移動日期或更改執行者。" diff --git a/modules/team_planner/config/locales/en.yml b/modules/team_planner/config/locales/en.yml index 4056518b898..62005bf133c 100644 --- a/modules/team_planner/config/locales/en.yml +++ b/modules/team_planner/config/locales/en.yml @@ -8,12 +8,17 @@ en: permission_manage_team_planner: "Manage team planner" project_module_team_planner_view: "Team planners" + ee: + feature_names: + team_planner_view: "Team planner" + upsale: + team_planner_view: + title: "Team planner" + description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." + team_planner: label_team_planner: "Team planner" label_new_team_planner: "New team planner" label_create_new_team_planner: "Create new team planner" label_team_planner_plural: "Team planners" label_assignees: "Assignees" - upsale: - title: "Team planner" - description: "Get a complete overview of your team’s planning with Team Planner. Stretch, shorten and drag-and-drop work packages to modify dates, move them or change assignees." diff --git a/modules/team_planner/lib/open_project/team_planner/engine.rb b/modules/team_planner/lib/open_project/team_planner/engine.rb index ec80443d031..b0208927ba0 100644 --- a/modules/team_planner/lib/open_project/team_planner/engine.rb +++ b/modules/team_planner/lib/open_project/team_planner/engine.rb @@ -36,7 +36,9 @@ module OpenProject::TeamPlanner author_url: "https://www.openproject.org", bundled: true, settings: {} do - project_module :team_planner_view, dependencies: :work_package_tracking, enterprise_feature: true do + project_module :team_planner_view, + dependencies: :work_package_tracking, + enterprise_feature: "team_planner_view" do permission :view_team_planner, { "team_planner/team_planner": %i[index show upsale overview], "team_planner/menus": %i[show] }, diff --git a/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb b/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb index 711b96a5744..e62b29db25a 100644 --- a/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb +++ b/modules/team_planner/spec/features/onboarding/team_planner_onboarding_tour_spec.rb @@ -32,7 +32,7 @@ require_relative "../../support/onboarding/onboarding_steps" RSpec.describe "team planner onboarding tour", :js, :selenium, - with_ee: %i[team_planner_view], + with_ee: %i[team_planner_view board_view], # We decrease the notification polling interval because some portions # of the JS code rely on something triggering the Angular change detection. # This is usually done by the notification polling, but we don't want to wait diff --git a/modules/team_planner/spec/features/team_planner_upsale_spec.rb b/modules/team_planner/spec/features/team_planner_upsale_spec.rb index f749e01aa91..d2b678084dc 100644 --- a/modules/team_planner/spec/features/team_planner_upsale_spec.rb +++ b/modules/team_planner/spec/features/team_planner_upsale_spec.rb @@ -43,18 +43,18 @@ RSpec.describe "Team planner index", it "redirects routes to upsale" do visit team_planners_path - expect(page).to have_text "Upgrade now" + expect(page).to have_enterprise_upsale_page(:premium) visit project_team_planners_path(project) - expect(page).to have_text "Upgrade now" + expect(page).to have_enterprise_upsale_page(:premium) visit new_project_team_planners_path(project) - expect(page).to have_text "Upgrade now" + expect(page).to have_enterprise_upsale_page(:premium) visit project_team_planner_path(project, id: "new") - expect(page).to have_text "Upgrade now" + expect(page).to have_enterprise_upsale_page(:premium) end end diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb index 194ac410309..8f0b56ed9ce 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb @@ -229,8 +229,6 @@ module ::TwoFactorAuthentication def find_device @device = target_user.otp_devices.find(params[:device_id]) - rescue ActiveRecord::RecordNotFound - render_404 end def find_user diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/users/two_factor_devices_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/users/two_factor_devices_controller.rb index 2f0d521ad25..f839e48c522 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/users/two_factor_devices_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/users/two_factor_devices_controller.rb @@ -105,8 +105,6 @@ module ::TwoFactorAuthentication def find_user @user = User.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render_404 end def target_user diff --git a/modules/two_factor_authentication/app/views/two_factor_authentication/upsale.html.erb b/modules/two_factor_authentication/app/views/two_factor_authentication/upsale.html.erb deleted file mode 100644 index 2998bbf3fd9..00000000000 --- a/modules/two_factor_authentication/app/views/two_factor_authentication/upsale.html.erb +++ /dev/null @@ -1,9 +0,0 @@ -<% html_title(t(:label_administration), t("two_factor_authentication.settings.title")) -%> - -<%= render template: "common/upsale", - locals: { - feature_title: t("two_factor_authentication.upsale.title"), - feature_description: t("two_factor_authentication.upsale.description"), - feature_reference: "2fa", - feature_image: "enterprise/two-factor-authentication.jpg" - } %> diff --git a/modules/two_factor_authentication/config/locales/crowdin/ro.yml b/modules/two_factor_authentication/config/locales/crowdin/ro.yml index 06a4a7bded1..ff3049144f8 100644 --- a/modules/two_factor_authentication/config/locales/crowdin/ro.yml +++ b/modules/two_factor_authentication/config/locales/crowdin/ro.yml @@ -178,7 +178,7 @@ ro: label_expiration_hint: "%{date} sau la deconectare" label_actions: "Acțiuni" label_confirmed: "Confirmat" - button_continue: "Continuă" + button_continue: "Continuaţi" button_make_default: "Marcați ca implicit" label_unverified_phone: "Telefonul mobil nu a fost încă verificat" notice_phone_number_format: "Te rog să introduci numărul în următorul format: +XX XXXXXXXX." diff --git a/modules/two_factor_authentication/config/locales/crowdin/ru.yml b/modules/two_factor_authentication/config/locales/crowdin/ru.yml index a1bbf15b728..b03e8cd0695 100644 --- a/modules/two_factor_authentication/config/locales/crowdin/ru.yml +++ b/modules/two_factor_authentication/config/locales/crowdin/ru.yml @@ -178,7 +178,7 @@ ru: label_expiration_hint: "%{date} или при выходе из системы" label_actions: "Действия" label_confirmed: "Подтвержден" - button_continue: "Продолжить" + button_continue: "Далее" button_make_default: "Задать по умолчанию" label_unverified_phone: "Сотовый телефон еще не подтвержден" notice_phone_number_format: "Введите номер в следующем формате: +XX XXXXXXXX." diff --git a/modules/two_factor_authentication/config/locales/crowdin/uk.yml b/modules/two_factor_authentication/config/locales/crowdin/uk.yml index 0c70cad71e7..3430ee3cac3 100644 --- a/modules/two_factor_authentication/config/locales/crowdin/uk.yml +++ b/modules/two_factor_authentication/config/locales/crowdin/uk.yml @@ -119,7 +119,7 @@ uk: failed_to_delete: "Не вдалося видалити пристрій 2FA." is_default_cannot_delete: "Пристрій позначено як типовий і його не можна видалити через активну політику безпеки. Перед видаленням позначте інший пристрій як стандартний." not_existing: "Для вашого облікового запису не зареєстровано жодного пристрою 2FA." - 2fa_from_input: Введіть код, отриманий на пристрій %{device_name}, щоб підтвердити свою особу. + 2fa_from_input: Введіть код, що надійшов на пристрій %{device_name}, щоб підтвердити свою особу. 2fa_from_webauthn: Укажіть пристрій WebAuthn %{device_name}. Якщо це USB-пристрій, переконайтеся, що його підключено, і торкніться його. Потім натисніть кнопку входу. webauthn: title: "WebAuthn" diff --git a/modules/webhooks/app/controllers/webhooks/outgoing/admin_controller.rb b/modules/webhooks/app/controllers/webhooks/outgoing/admin_controller.rb index 688c61eae9a..95bdb27ebd4 100644 --- a/modules/webhooks/app/controllers/webhooks/outgoing/admin_controller.rb +++ b/modules/webhooks/app/controllers/webhooks/outgoing/admin_controller.rb @@ -58,8 +58,6 @@ module Webhooks def find_webhook @webhook = webhook_class.find(params[:webhook_id]) - rescue ActiveRecord::RecordNotFound - render_404 end def webhook_class diff --git a/modules/xls_export/config/locales/crowdin/zh-CN.yml b/modules/xls_export/config/locales/crowdin/zh-CN.yml index 59230e603ec..31c8d3cdd71 100644 --- a/modules/xls_export/config/locales/crowdin/zh-CN.yml +++ b/modules/xls_export/config/locales/crowdin/zh-CN.yml @@ -13,4 +13,4 @@ zh-CN: xls_with_relations: "带关系的 XLS" xls_export: child_of: 此项的子项 - parent_of: 此项的父级 + parent_of: 此项的父项 diff --git a/publiccode.yml b/publiccode.yml index f985dee2acc..732cd52c3a2 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -184,7 +184,7 @@ description: Zusätzlich bietet der Hersteller in ihrer [Enterprise Cloud](https://www.openproject.org/de/enterprise-edition) ein Hosting in einem Rechenzentrum innerhalb der Europäischen Union an, das gemäß der Datenschutz-Grundverordnung (DS-GVO) erfolgt. OpenProject entspricht den Anforderungen des [BSI - IT-Grundschutzes](https://www.bsi.bund.de/DE/Themen/ITGrundschutz/itgrundschutz_node.html) + IT-Grundschutzes](https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Standards-und-Zertifizierung/IT-Grundschutz/it-grundschutz_node.html) und wird kontinuierlich auf Basis der WCAG 2.1-Richtlinien getestet, wie im [Prüfbericht](https://gitlab.opencode.de/bmi/opendesk/info/-/blob/main/24.03/Barrierefreiheit/Barrierefreiheitsbericht%20-%20Komponente%20Projekte.pdf) diff --git a/script/github_pr_errors b/script/github_pr_errors index 77c4343f447..63651a8f702 100755 --- a/script/github_pr_errors +++ b/script/github_pr_errors @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "rubygems" require "bundler" @@ -14,8 +15,8 @@ require "time" require "yaml" require "httpx" -GITHUB_API_OPENPROJECT_PREFIX = "https://api.github.com/repos/opf/openproject".freeze -GITHUB_HTML_OPENPROJECT_PREFIX = "https://github.com/opf/openproject".freeze +GITHUB_API_OPENPROJECT_PREFIX = "https://api.github.com/repos/opf/openproject" +GITHUB_HTML_OPENPROJECT_PREFIX = "https://github.com/opf/openproject" RAILS_ROOT = Pathname.new(__dir__).dirname EXCLUDED_JOB_NAMES = %w[eslint rubocop].freeze @@ -33,17 +34,18 @@ class Options failed_jobs_logs: [], no_cache: false, run_id: nil, + job_id: nil, verbose: false }.freeze BANNER = <<~BANNER.freeze Usage: #{$0} [options] [url] - Fetches rspec failures from last completed GitHub actions on current - branch, and outputs them on standard output, one by line. + Fetches rspec failures from last completed GitHub actions on current branch, + and outputs them on standard output, one by line. - If given an url, it will fetch the failures from the given url instead of - the tip of the current branch. + If given an url, it will fetch the failures from the given url using the workflow + id and the job id if present instead of the tip of the current branch. Information is printed on standard error to preserve standard output. @@ -129,10 +131,11 @@ class Options def parse_url(url) case url - when %r{^https://github.com/opf/openproject/actions/runs/(\d+)(?:/job/\d+(?:\?.*)?)?$} + when %r{^https://github.com/opf/openproject/actions/runs/(\d+)(?:/job/(\d+)(?:\?.*)?)?$} run_id = $1.to_i - say_verbose("Extracted run id #{run_id} from #{url}") - { run_id: } + job_id = $2&.to_i + say_verbose("Extracted run id #{run_id}#{" and job id #{job_id}" if job_id} from #{url}") + { run_id:, job_id: } else warn "Unrecognized url #{url}" exit 1 @@ -192,7 +195,7 @@ def error_details(error) parts << "Failed to perform API request GET #{response.uri}: #{error}" parts << " #{response_body['message']}" parts << " See #{response_body['documentation_url']}" - parts += error.backtrace.map { " #{_1}" } + parts += error.backtrace.map { " #{it}" } parts.join("\n") end @@ -269,14 +272,56 @@ def get_workflow_run(run_id) end end -class Report - attr_accessor :errors, - :failures_explanation, - :head_branch, - :head_sha, - :commit_message, - :run_started_at, - :merge_branch_sha +class WorkflowRunDecorator + attr_reader :workflow_run + + def initialize(workflow_run) + @workflow_run = workflow_run + end + + def head_branch = workflow_run["head_branch"] + def head_sha = workflow_run["head_sha"] + def run_started_at = Time.parse(workflow_run["run_started_at"]).utc + + def commit_message + workflow_run["head_commit"] + .then { |commit| commit["message"] } + .then { |message| message.split("\n", 2).first } + end +end + +Report = Data.define( + :errors, + :failures_explanation, + :head_branch, + :head_sha, + :commit_message, + :run_started_at, + :run_status, + :failed_job_logs, + :merge_branch_sha +) do + def initialize(errors: [], + failures_explanation: nil, + head_branch: nil, + head_sha: nil, + commit_message: nil, + run_started_at: nil, + run_status: nil, + failed_job_logs: nil, + merge_branch_sha: nil) + super + end + + def with_workflow_run_info(workflow_run) + workflow_run = WorkflowRunDecorator.new(workflow_run) + with( + head_branch: workflow_run.head_branch, + head_sha: workflow_run.head_sha, + commit_message: workflow_run.commit_message, + run_started_at: workflow_run.run_started_at + ) + end end class Error @@ -319,10 +364,11 @@ class JobErrorsFinder logs.each do |log| finder.scan_log(log) end - report.errors = finder.errors - report.failures_explanation = finder.failures_explanation - report.merge_branch_sha = finder.merge_branch_sha - report + report.with( + errors: finder.errors, + failures_explanation: finder.failures_explanation, + merge_branch_sha: finder.merge_branch_sha + ) end def scan_log(log) @@ -375,7 +421,7 @@ class JobErrorsFinder explanations << line end end - explanations.map! { _1[29..] } # Remove leading timestamp (like "2024-02-05T08:37:54.5175930Z") + explanations.map! { it[29..] } # Remove leading timestamp (like "2024-02-05T08:37:54.5175930Z") explanations.reject! do |line| line == "Failures:" || line == "Failed examples:" || @@ -398,7 +444,7 @@ class JobErrorsFinder def find_screenshots(log) log.scan(SCREENSHOT_PATTERN) - .map { JSON.parse _1 } + .map { JSON.parse it } .each do |screenshot_info| id = screenshot_info["test_id"] location = screenshot_info["test_location"] @@ -413,10 +459,10 @@ class JobErrorsFinder tests_groups = log .scan(TESTS_GROUP_PATTERN) .flatten - .map { build_tests_group_from_command(_1) } + .map { build_tests_group_from_command(it) } errors.each do |error| - error.tests_group = tests_groups.find { _1.include_error?(error) } + error.tests_group = tests_groups.find { it.include_error?(error) } end end @@ -451,19 +497,15 @@ class Formatter @compact end - def display_workflow_run_info(report, workflow_run) - report.head_branch = workflow_run["head_branch"] - report.head_sha = workflow_run["head_sha"] - report.commit_message = commit_message(workflow_run) - report.run_started_at = Time.parse(workflow_run["run_started_at"]).utc + def display_workflow_run_info(report) warn " Branch: #{report.head_branch.bold}" warn " Commit SHA: #{report.head_sha.bold}" warn " Commit message: #{report.commit_message.bold}" - warn " Run started at: #{report.run_started_at.localtime.to_s.bold}" - display_pull_request_info(workflow_run) + warn " Last attempted run started at: #{report.run_started_at.localtime.to_s.bold}" end def display_workflow_status(workflow_run) + warn " Run attempt: #{workflow_run['run_attempt']}" warn " #{status_line(workflow_run)}" end @@ -471,10 +513,10 @@ class Formatter warn " #{status_line(job)}" end - def display_report(report) + def display_errors_from_report(report) display_failures_explanation(report.failures_explanation) - display_checkout_test_commit_information(report) if Options.display_rerun_info display_errors(report.errors) + display_checkout_test_commit_information(report) if Options.display_rerun_info end def display_failures_explanation(failures_explanation) @@ -495,10 +537,23 @@ class Formatter end end + def display_pull_request_info(workflow_run) + return unless workflow_run["event"] == "pull_request" + + if pr = workflow_run["pull_requests"].first + pr_number = "##{pr['number']}" + pr_html_url = "#{GITHUB_HTML_OPENPROJECT_PREFIX}/pull/#{pr['number']}" + pr_display_title = "#{workflow_run['display_title']} #{pr_number.white.dark} #{pr_html_url.white.dark}" + warn " Pull Request: #{pr_display_title} " + else + warn " Pull Request: not found; perhaps it is already merged or closed?" + end + end + private def display_errors_compact(errors) - puts errors.map { escaped_location(_1) }.join(" ") + puts errors.map { escaped_location(it) }.join(" ") end def display_errors_detailed(errors) @@ -508,13 +563,13 @@ class Formatter .group_by(&:tests_group) .each do |tests_group, tests_group_errors| display_tests_group_info(tests_group) - tests_group_errors.each { display_error(_1) } + tests_group_errors.each { display_error(it) } display_tests_group_rerun_commands(tests_group) end else errors .sort_by(&:location) - .each { display_error(_1) } + .each { display_error(it) } end end @@ -565,6 +620,7 @@ class Formatter if report.merge_branch_sha warn <<~INSTRUCTIONS.white.dark GitHub Action run merged #{report.merge_branch_sha} into #{report.head_sha} before running. + To be with the exact same source files as CI, use these commands (warning: stash your modifications first): git checkout -B repro_ci_failures #{report.head_sha} git merge --no-edit --no-verify #{report.merge_branch_sha} @@ -575,7 +631,9 @@ class Formatter git checkout #{report.head_sha} INSTRUCTIONS end - warn # empty line + warn <<~INSTRUCTIONS.white.dark + Then run a test group with the rspec command above. + INSTRUCTIONS end def display_tests_group_info(tests_group) @@ -592,25 +650,6 @@ class Formatter warn "" end - def display_pull_request_info(workflow_run) - return unless workflow_run["event"] == "pull_request" - - if pr = workflow_run["pull_requests"].first - pr_number = "##{pr['number']}" - pr_html_url = "#{GITHUB_HTML_OPENPROJECT_PREFIX}/pull/#{pr['number']}" - pr_display_title = "#{workflow_run['display_title']} #{pr_number.white.dark} #{pr_html_url.white.dark}" - warn " Pull Request: #{pr_display_title} " - else - warn " Pull Request: not found; perhaps it is already merged or closed?" - end - end - - def commit_message(workflow_run) - workflow_run["head_commit"] - .then { |commit| commit["message"] } - .then { |message| message.split("\n", 2).first } - end - def status_icon(job) case job["status"] when "queued", "in_progress" @@ -645,21 +684,46 @@ class Formatter end end +def get_jobs_from_job_id(job_id) + warn " Looking for the job with id #{job_id.to_s.bold}" + + # no need to fetch anything as we only use the job id to get the logs + # mock the same structure as the jobs response + [{ "id" => job_id }] +end + +def get_failing_jobs_from_workflow_last_attempt(workflow_run, formatter) + get_jobs(workflow_run) + .then { |jobs_response| jobs_response["jobs"] } + .sort_by { it["name"] } + .each { |job| formatter.display_job_status(job) } + .select { it["conclusion"] == "failure" } + .reject { EXCLUDED_JOB_NAMES.include?(it["name"]) } +end + +def get_relevant_jobs(job_id, workflow_run, formatter) + if job_id + get_jobs_from_job_id(job_id) + else + formatter.display_workflow_status(workflow_run) + get_failing_jobs_from_workflow_last_attempt(workflow_run, formatter) + end +end + def get_failed_jobs_logs_from_github(report, formatter) workflow_run = get_workflow_run(Options.run_id) + report = report.with_workflow_run_info(workflow_run) - formatter.display_workflow_run_info(report, workflow_run) + formatter.display_workflow_run_info(report) + formatter.display_pull_request_info(workflow_run) - formatter.display_workflow_status(workflow_run) - job_logs = get_jobs(workflow_run) - .then { |jobs_response| jobs_response["jobs"] } - .sort_by { _1["name"] } - .each { |job| formatter.display_job_status(job) } - .select { _1["conclusion"] == "failure" } - .reject { EXCLUDED_JOB_NAMES.include?(_1["name"]) } + failed_job_logs = get_relevant_jobs(Options.job_id, workflow_run, formatter) .map { |job| get_log(job) } - [job_logs, job_logs.any? ? "error" : workflow_run["status"]] + report.with( + failed_job_logs:, + run_status: failed_job_logs.any? ? "error" : workflow_run["status"] + ) end def get_failed_jobs_logs_from_args @@ -668,7 +732,11 @@ end def get_failed_jobs_logs(report, formatter) if Options.failed_jobs_logs.any? - [get_failed_jobs_logs_from_args, failed_jobs_logs.none? ? "completed" : "error"] + failed_job_logs = get_failed_jobs_logs_from_args + report.with( + failed_job_logs:, + run_status: failed_job_logs.none? ? "completed" : "error" + ) else get_failed_jobs_logs_from_github(report, formatter) end @@ -680,15 +748,15 @@ end report = Report.new formatter = Formatter.new(compact: Options.compact) -failed_jobs_logs, status = get_failed_jobs_logs(report, formatter) +report = get_failed_jobs_logs(report, formatter) -JobErrorsFinder.scan_logs(report, failed_jobs_logs) +report = JobErrorsFinder.scan_logs(report, report.failed_job_logs) -case status +case report.run_status when "completed" warn "All jobs successful 🎉" when "in_progress" # NOOP when "error" - formatter.display_report(report) + formatter.display_errors_from_report(report) end diff --git a/spec/components/enterprise_edition/banner_component_spec.rb b/spec/components/enterprise_edition/banner_component_spec.rb index a72eb6fce55..621d6bb3755 100644 --- a/spec/components/enterprise_edition/banner_component_spec.rb +++ b/spec/components/enterprise_edition/banner_component_spec.rb @@ -32,32 +32,45 @@ require "rails_helper" RSpec.describe EnterpriseEdition::BannerComponent, type: :component do let(:title) { "Some title" } + let(:expected_title) { title } let(:description) { "Some description" } + let(:expected_description) { description } let(:href) { "https://www.example.org" } - let(:link_title) { "Get more information" } - let(:ee_show_banners) { true } + let(:component_test_selector) { "op-enterprise-banner" } + let(:features) { nil } let(:enforce_available_locales) { I18n.config.enforce_available_locales } let(:i18n_upsale) do { - title:, - link_title:, some_enterprise_feature: { - description: - } + title:, + description:, + features: + }.compact } end let(:static_links) do { - enterprise_docs: { + enterprise_features: { some_enterprise_feature: { href: } } } end + let(:translations) do + { + ee: { + features: { + some_enterprise_feature: "Enterprise feature translation" + }, + upsale: i18n_upsale + } + } + end + let(:component_args) { {} } let(:render_component) do - render_inline(described_class.new(:some_enterprise_feature)) + render_inline(described_class.new(:some_enterprise_feature, **component_args)) end let(:render_component_in_mo) do @@ -67,9 +80,6 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do end before do - allow(EnterpriseToken) - .to receive(:show_banners?) - .and_return(ee_show_banners) allow(OpenProject::Static::Links) .to receive(:links) .and_return(static_links) @@ -78,11 +88,7 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do I18n.backend.store_translations( :mo, - { - ee: { - upsale: i18n_upsale - } - } + translations ) end @@ -95,25 +101,92 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do it "renders the component" do render_component_in_mo - expect(page).to have_test_selector("op-ee-banner-some-enterprise-feature") - expect(page).to have_css ".op-ee-banner--title-container", text: title - expect(page).to have_css ".op-ee-banner--description-container", text: description - expect(page).to have_link link_title, href: + component = find_test_selector(component_test_selector) + + expect(component).to have_text(expected_title) + expect(component).to have_text(expected_description) + expect(component).to have_link("More information", href:) + end + end + + shared_examples_for "does not render the component" do + it "does not render the component" do + render_component_in_mo + + expect(page).not_to have_test_selector(component_test_selector) + expect(page).to have_no_text("Enterprise feature translation") + expect(page).to have_no_text(expected_title) + expect(page).to have_no_text(expected_description) + expect(page).to have_no_link(href:) end end it_behaves_like "renders the component" - context "with a description_html in the i18n file" do + context "when feature is available", with_ee: %i[some_enterprise_feature] do + it_behaves_like "does not render the component" + end + + context "when banners are hidden" do + before do + allow(EnterpriseToken).to receive(:hide_banners?).and_return(true) + end + + it_behaves_like "does not render the component" + end + + context "when banner is dismissed" do + let(:preference) { build_stubbed(:user_preference) } + let(:user) { build_stubbed(:user, preference:) } + let(:dismiss_key) { :some_enterprise_feature } + let(:component_args) { { dismissable: true } } + + before do + login_as(user) + allow(preference) + .to receive(:dismissed_banner?) + .with(dismiss_key) + .and_return(true) + end + + it_behaves_like "does not render the component" + + context "when not dismissable" do + let(:component_args) { { dismissable: false } } + + it_behaves_like "renders the component" + end + + context "when using a custom dismiss_key" do + let(:dismiss_key) { :foo } + let(:component_args) { { dismiss_key:, dismissable: true } } + + it_behaves_like "does not render the component" + end + end + + context "without a title, but a description_html" do let(:i18n_upsale) do { - title:, - link_title:, some_enterprise_feature: { description_html: description } } end + let(:expected_title) { "Enterprise feature translation" } + + it_behaves_like "renders the component" + end + + context "without a title, but a description" do + let(:i18n_upsale) do + { + some_enterprise_feature: { + description: + } + } + end + let(:expected_title) { "Enterprise feature translation" } it_behaves_like "renders the component" end @@ -121,8 +194,6 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do context "with a more specific title in the i18n file" do let(:i18n_upsale) do { - title: "The general title", - link_title:, some_enterprise_feature: { title:, description: @@ -133,17 +204,27 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do it_behaves_like "renders the component" end - context "with a more specific link title in the i18n file" do - let(:i18n_upsale) do + context "with a custom i18n_scope" do + let(:translations) do { - title:, - link_title: "The general link title", - some_enterprise_feature: { - link_title:, - description: + my: { + custom: { + upsale: { + title: "Foo", + description: "Bar" + } + } + }, + ee: { + features: { + some_enterprise_feature: "Enterprise feature translation" + } } } end + let(:expected_title) { "Foo" } + let(:expected_description) { "Bar" } + let(:component_args) { { i18n_scope: "my.custom.upsale" } } it_behaves_like "renders the component" end @@ -151,8 +232,6 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do context "without a description key in the i18n file" do let(:i18n_upsale) do { - title:, - link_title:, some_enterprise_feature: {} } end @@ -165,34 +244,23 @@ RSpec.describe EnterpriseEdition::BannerComponent, type: :component do context "without a link key in the static_link file" do let(:static_links) do { - enterprise_docs: { + enterprise_features: { + default: { + href: "https://example.com" + }, some_enterprise_feature: {} } } end - it "raises an error" do - expect { render_component_in_mo }.to raise_error(RuntimeError) - end - end - - context "if banners are hidden" do - let(:ee_show_banners) { false } - - it "hides the component" do + it "uses the default" do render_component_in_mo - expect(page).to have_no_css ".op-ee-banner" - end - end + component = find_test_selector(component_test_selector) - context "if banners are hidden but skip_render is overwritten" do - let(:ee_show_banners) { false } - let(:render_component) do - render_inline(described_class.new(:some_enterprise_feature, - skip_render: false)) + expect(component).to have_text(expected_title) + expect(component).to have_text(expected_description) + expect(component).to have_link("More information", href: "https://example.com") end - - it_behaves_like "renders the component" end end diff --git a/spec/components/projects/phases/hover_card_component_spec.rb b/spec/components/projects/phases/hover_card_component_spec.rb new file mode 100644 index 00000000000..117d67da0f7 --- /dev/null +++ b/spec/components/projects/phases/hover_card_component_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "rails_helper" + +RSpec.describe Projects::Phases::HoverCardComponent, type: :component do + include Rails.application.routes.url_helpers + + let(:phase) { build_stubbed(:project_phase, :with_gated_definition) } + let(:gate) { "start" } + + subject { described_class.new(phase:, gate:) } + + before do + render_inline(subject) + page.extend TestSelectorFinders + end + + context "for start" do + it "renders successfully" do + page.find_test_selector("phase-gate-hover-card-name", text: phase.start_gate_name) + page.find_test_selector("phase-gate-hover-card-date", text: phase.start_date.strftime("%m/%d/%Y")) + end + + context "without a definition" do + let(:phase) { create(:project_phase) } + + it "renders, but has no content" do + page.find_test_selector("phase-gate-hover-card-name", text: "") + page.find_test_selector("phase-gate-hover-card-date", text: "") + end + end + end + + context "for finish" do + let(:gate) { "finish" } + + it "renders successfully" do + page.find_test_selector("phase-gate-hover-card-name", text: phase.finish_gate_name) + page.find_test_selector("phase-gate-hover-card-date", text: phase.finish_date.strftime("%m/%d/%Y")) + end + + context "without a definition" do + let(:phase) { create(:project_phase) } + + it "renders, but has no content" do + page.find_test_selector("phase-gate-hover-card-name", text: "") + page.find_test_selector("phase-gate-hover-card-date", text: "") + end + end + end +end diff --git a/spec/components/users/hover_card_component_spec.rb b/spec/components/users/hover_card_component_spec.rb index b2d10036046..d3e95187d97 100644 --- a/spec/components/users/hover_card_component_spec.rb +++ b/spec/components/users/hover_card_component_spec.rb @@ -46,11 +46,10 @@ RSpec.describe Users::HoverCardComponent, type: :component do groups login_as(current_user) render_inline(subject) - page.extend TestSelectorFinders end it "renders successfully" do - page.find_test_selector("user-hover-card-name", text: user.name) + find_test_selector("user-hover-card-name", text: user.name) end context "when the user does not exist" do @@ -71,14 +70,14 @@ RSpec.describe Users::HoverCardComponent, type: :component do let(:current_user) { build(:admin) } it "shows the email address of a user" do - page.find_test_selector("user-hover-card-email", text: user.mail) + find_test_selector("user-hover-card-email", text: user.mail) end end end context "when showing the group summary" do it "shows a no results text for users without group memberships" do - g = page.find_test_selector("user-hover-card-groups") + g = find_test_selector("user-hover-card-groups") expect(g).to have_text(I18n.t("users.groups.no_results_title_text")) end @@ -88,7 +87,7 @@ RSpec.describe Users::HoverCardComponent, type: :component do end it "lists the group names for a user" do - g = page.find_test_selector("user-hover-card-groups") + g = find_test_selector("user-hover-card-groups") expect(g).to have_text("Member of #{groups.first.lastname}, #{groups.last.lastname}.") end @@ -98,7 +97,7 @@ RSpec.describe Users::HoverCardComponent, type: :component do let(:another_user) { create(:user) } it "does not show groups" do - g = page.find_test_selector("user-hover-card-groups") + g = find_test_selector("user-hover-card-groups") expect(g).to have_text(I18n.t("users.groups.no_results_title_text")) end @@ -111,7 +110,7 @@ RSpec.describe Users::HoverCardComponent, type: :component do end it "lists some group names with truncation" do - g = page.find_test_selector("user-hover-card-groups") + g = find_test_selector("user-hover-card-groups") expect(g).to have_text("Member of #{groups.slice(0, 4).map(&:lastname).join(', ')} and 4 more.") end @@ -120,7 +119,7 @@ RSpec.describe Users::HoverCardComponent, type: :component do context "when clicking on the Open Profile button" do it "leads to the users profile" do - b = page.find_test_selector("user-hover-card-profile-btn") + b = find_test_selector("user-hover-card-profile-btn") expect(b).to have_text(I18n.t("users.open_profile")) expect(b["href"]).to eq(user_path(user)) @@ -130,7 +129,7 @@ RSpec.describe Users::HoverCardComponent, type: :component do let(:current_user) { build(:admin) } it "leads to editing the users profile" do - b = page.find_test_selector("user-hover-card-profile-btn") + b = find_test_selector("user-hover-card-profile-btn") expect(b).to have_text(I18n.t("users.open_profile")) expect(b["href"]).to eq(edit_user_path(user)) diff --git a/spec/components/work_packages/types/subject_configuration_component_spec.rb b/spec/components/work_packages/types/subject_configuration_component_spec.rb index 18b2900cee1..f4db7176f78 100644 --- a/spec/components/work_packages/types/subject_configuration_component_spec.rb +++ b/spec/components/work_packages/types/subject_configuration_component_spec.rb @@ -42,7 +42,7 @@ RSpec.describe WorkPackages::Types::SubjectConfigurationComponent, type: :compon it "shows no enterprise banner" do render_component - expect(page).not_to have_test_selector("op-ee-banner-automatic-subject-generation") + expect(page).not_to have_test_selector("op-enterprise-banner-work-package-subject-generation") end it "enables mode selectors", :aggregate_failures do @@ -109,7 +109,7 @@ RSpec.describe WorkPackages::Types::SubjectConfigurationComponent, type: :compon it "shows the enterprise banner" do render_component - expect(page).to have_test_selector("op-ee-banner-automatic-subject-generation") + expect(page).to have_enterprise_banner end it "disables only automatic mode selector", :aggregate_failures do @@ -125,7 +125,7 @@ RSpec.describe WorkPackages::Types::SubjectConfigurationComponent, type: :compon it "shows the enterprise banner" do render_component - expect(page).to have_test_selector("op-ee-banner-automatic-subject-generation") + expect(page).to have_enterprise_banner end it "enables mode selectors", :aggregate_failures do diff --git a/spec/contracts/project_life_cycle_steps/base_contract_spec.rb b/spec/contracts/project_life_cycle_steps/base_contract_spec.rb index 36f32dfba27..86c64cd7a72 100644 --- a/spec/contracts/project_life_cycle_steps/base_contract_spec.rb +++ b/spec/contracts/project_life_cycle_steps/base_contract_spec.rb @@ -34,14 +34,12 @@ require "contracts/shared/model_contract_shared_context" RSpec.describe ProjectLifeCycleSteps::BaseContract do include_context "ModelContract shared context" - let(:contract) { described_class.new(project, user) } + let(:contract) { described_class.new(phase, user) } + let(:user) { build_stubbed(:user) } let(:project) { build_stubbed(:project) } + let(:phase) { build_stubbed(:project_phase, project:) } context "with authorized user" do - let(:user) { build_stubbed(:user) } - let(:project) { build_stubbed(:project, available_phases: phases) } - let(:phases) { [] } - before do mock_permissions_for(user) do |mock| mock.allow_in_project(:edit_project_phases, project:) @@ -50,136 +48,9 @@ RSpec.describe ProjectLifeCycleSteps::BaseContract do it_behaves_like "contract is valid" include_examples "contract reuses the model errors" - - describe "validations" do - describe "#consecutive_phases_have_increasing_dates" do - let(:phase1) { build_stubbed(:project_phase, start_date: Date.new(2024, 1, 1), finish_date: Date.new(2024, 1, 1)) } - let(:phase2) { build_stubbed(:project_phase, start_date: Date.new(2024, 2, 1), finish_date: Date.new(2024, 2, 28)) } - let(:phase3) { build_stubbed(:project_phase, start_date: Date.new(2024, 3, 1), finish_date: Date.new(2024, 3, 15)) } - let(:phases) { [phase1, phase2, phase3] } - - context "when no phases are present" do - let(:phases) { [] } - - it_behaves_like "contract is valid" - end - - context "when only one step is present" do - let(:phases) { [phase1] } - - it_behaves_like "contract is valid" - end - - context "when phases have valid and increasing dates" do - let(:phases) { [phase1, phase2, phase3] } - - it_behaves_like "contract is valid" - end - - context "when phases have decreasing dates" do - context "and the erroneous step is Phase 1" do - let(:phases) { [phase3, phase1] } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - - it "adds an error to the decreasing step" do - contract.validate - expect(phase1.errors.symbols_for(:date_range)).to include(:non_continuous_dates) - end - end - end - - context "when phases with identical dates" do - let(:phase2) { build_stubbed(:project_phase, start_date: Date.new(2024, 1, 1), finish_date: Date.new(2024, 1, 1)) } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - end - - context "when phases have touching start and end dates" do - context "when 2 Phases are touching" do - let(:phase2) { build_stubbed(:project_phase, start_date: Date.new(2024, 1, 1), finish_date: Date.new(2024, 1, 1)) } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - - context "when having an empty step in between" do - let(:step_missing_dates) { build_stubbed(:project_phase, start_date: nil, finish_date: nil) } - let(:phases) { [phase1, step_missing_dates, phase2] } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - end - end - - context "when 3 Phases are touching" do - let(:phase2) { build_stubbed(:project_phase, start_date: Date.new(2024, 1, 1), finish_date: Date.new(2024, 1, 1)) } - let(:phase3) { build_stubbed(:project_phase, start_date: Date.new(2024, 1, 1), finish_date: Date.new(2024, 1, 2)) } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - - it "adds error to Phase 2 and 3" do - contract.validate - expect(phase2.errors.symbols_for(:date_range)).to include(:non_continuous_dates) - expect(phase3.errors.symbols_for(:date_range)).to include(:non_continuous_dates) - end - - context "when having an empty step in between" do - let(:step_missing_dates) { build_stubbed(:project_phase, start_date: nil, finish_date: nil) } - let(:phases) { [phase1, phase2, step_missing_dates, phase3] } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - - it "adds error to Phase 2 and 3" do - contract.validate - expect(phase2.errors.symbols_for(:date_range)).to include(:non_continuous_dates) - expect(phase3.errors.symbols_for(:date_range)).to include(:non_continuous_dates) - expect(step_missing_dates.errors.symbols_for(:date_range)).to be_empty - end - end - end - end - - context "when a step has missing start dates" do - let(:step_missing_dates) { build_stubbed(:project_phase, start_date: nil, finish_date: nil) } - - context "and the other phases have increasing dates" do - let(:phases) { [phase1, step_missing_dates, phase2] } - - it_behaves_like "contract is valid" - end - - context "and the other phases have decreasing dates" do - let(:phases) { [phase2, step_missing_dates, phase1] } - - it_behaves_like "contract is invalid", - "available_phases.date_range": :non_continuous_dates - - it "adds an error to the decreasing step" do - contract.validate - expect(phase1.errors.symbols_for(:date_range)).to include(:non_continuous_dates) - end - end - end - end - - describe "triggering validations on the model" do - it "sets the :saving_phases validation context" do - allow(project).to receive(:valid?) - - contract.validate - expect(project).to have_received(:valid?).with(:saving_phases) - end - end - end end context "with unauthorized user" do - let(:user) { build_stubbed(:user) } - it_behaves_like "contract user is unauthorized" end end diff --git a/spec/contracts/work_packages/create_note_contract_spec.rb b/spec/contracts/work_packages/create_note_contract_spec.rb index eeacb2dabb7..91c7c9167da 100644 --- a/spec/contracts/work_packages/create_note_contract_spec.rb +++ b/spec/contracts/work_packages/create_note_contract_spec.rb @@ -28,11 +28,15 @@ # See COPYRIGHT and LICENSE files for more details. require "spec_helper" +require "contracts/shared/model_contract_shared_context" RSpec.describe WorkPackages::CreateNoteContract do + include_context "ModelContract shared context" + + let(:project) { build_stubbed(:project) } let(:work_package) do # As we only want to test the contract, we mock checking whether the work_package is valid - wp = build_stubbed(:work_package) + wp = build_stubbed(:work_package, project:) # we need to clear the changes information because otherwise the # contract will complain about all the changes to read_only attributes wp.send(:clear_changes_information) @@ -41,99 +45,105 @@ RSpec.describe WorkPackages::CreateNoteContract do wp end let(:user) { build_stubbed(:user) } - let(:policy_instance) { instance_double(WorkPackagePolicy) } + let(:permissions) { %i[add_work_package_notes add_comments_with_restricted_visibility] } + + before do + mock_permissions_for(user) do |mock| + mock.allow_in_project(*permissions, project:) + end + end subject(:contract) do - contract = described_class.new(work_package, user) - - contract.send(:"policy=", policy_instance) - - contract + described_class.new(work_package, user) end - describe "note" do - before do - work_package.journal_notes = "blubs" - end - - context "if the user has the permissions" do + describe "validations" do + describe "journal_notes" do before do - allow(policy_instance).to receive(:allowed?).with(work_package, :comment).and_return true - - contract.validate + work_package.journal_notes = "blubs" end - it("is valid") { expect(contract.errors).to be_empty } - end + context "if the user has only the add_work_package_notes permission" do + let(:permissions) { %i[add_work_package_notes] } - context "if the user lacks the permissions" do - before do - allow(policy_instance).to receive(:allowed?).with(work_package, :comment).and_return false - - contract.validate + it_behaves_like "contract is valid" end - it "is invalid" do - expect(contract.errors.symbols_for(:journal_notes)) - .to contain_exactly(:error_unauthorized) + context "if the user has only the edit_work_packages permission" do + let(:permissions) { %i[edit_work_packages] } + + it_behaves_like "contract is valid" + end + + context "if the user lacks the permissions" do + let(:permissions) { [] } + + it_behaves_like "contract is invalid", journal_notes: :error_unauthorized end end - end - describe "journal_restricted" do - before do - allow(policy_instance).to receive(:allowed?).and_return(true) - end - - context "with a blank note" do + describe "journal_restricted" do before do - work_package.journal_notes = "" - work_package.journal_restricted = true - - contract.validate + # Setting the journal_notes to not trigger a :blank error + work_package.journal_notes = "blubs" end context "and journal_restricted is true, and comments_with_restricted_visibility_active? is disabled", with_flag: { comments_with_restricted_visibility_active: false } do - it "is invalid" do - expect(contract.errors.full_messages).to eq(["Comment can't be blank.", "Restricted Journal is not available."]) + before do + work_package.journal_restricted = true end + + it_behaves_like "contract is invalid", journal_restricted: :feature_disabled end context "and journal_restricted is true, and comments_with_restricted_visibility_active? is enabled", with_flag: { comments_with_restricted_visibility_active: true } do - it "is invalid" do - expect(contract.errors.full_messages).to eq(["Comment can't be blank."]) - end - end - end - - context "with a note" do - context "and journal_restricted is true", with_flag: { comments_with_restricted_visibility_active: true } do before do - work_package.journal_notes = "blubs" work_package.journal_restricted = true - - contract.validate end - it("is valid") { expect(contract.errors).to be_empty } + it_behaves_like "contract is valid" + end + + context "and journal_restricted is false, and comments_with_restricted_visibility_active? is disabled", + with_flag: { comments_with_restricted_visibility_active: false } do + before do + work_package.journal_restricted = false + end + + it_behaves_like "contract is valid" + end + + context "with journal_restricted is true, comments_with_restricted_visibility_active? is active but lacking permissions", + with_flag: { comments_with_restricted_visibility_active: true } do + let(:permissions) { super() - [:add_comments_with_restricted_visibility] } + + before do + work_package.journal_restricted = true + end + + it_behaves_like "contract is invalid", journal_restricted: :error_unauthorized + end + + context "with journal_restricted is false, comments_with_restricted_visibility_active? is active and lacking permissions", + with_flag: { comments_with_restricted_visibility_active: true } do + let(:permissions) { super() - [:add_comments_with_restricted_visibility] } + + before do + work_package.journal_restricted = false + end + + it_behaves_like "contract is valid" end end - end - describe "subject" do - before do - work_package.subject = "blubs" + describe "another attribute of work package" do + before do + work_package.subject = "blubs" + end - allow(policy_instance).to receive(:allowed?).and_return true - - contract.validate - end - - it "is invalid" do - expect(contract.errors.symbols_for(:subject)) - .to contain_exactly(:error_readonly) + it_behaves_like "contract is invalid", subject: :error_readonly end end end diff --git a/spec/contracts/work_packages/shared_base_contract.rb b/spec/contracts/work_packages/shared_base_contract.rb index bd987abaa10..7e02af9c51c 100644 --- a/spec/contracts/work_packages/shared_base_contract.rb +++ b/spec/contracts/work_packages/shared_base_contract.rb @@ -29,7 +29,6 @@ RSpec.shared_examples_for "work package contract" do let(:user) { build_stubbed(:user) } let(:other_user) { build_stubbed(:user) } - let(:policy) { double(WorkPackagePolicy, allowed?: true) } subject(:contract) { described_class.new(work_package, user) } @@ -39,12 +38,6 @@ RSpec.shared_examples_for "work package contract" do contract end - before do - allow(WorkPackagePolicy) - .to receive(:new) - .and_return(policy) - end - let(:possible_assignees) { [] } let!(:assignable_assignees_scope) do scope = double "assignable assignees scope" diff --git a/spec/controllers/custom_actions_controller_spec.rb b/spec/controllers/custom_actions_controller_spec.rb index da4a1ec678f..ee28b05c1c7 100644 --- a/spec/controllers/custom_actions_controller_spec.rb +++ b/spec/controllers/custom_actions_controller_spec.rb @@ -46,8 +46,7 @@ RSpec.describe CustomActionsController, with_ee: %i[custom_actions] do end it "renders enterprise_token" do - expect(response) - .to render_template "common/upsale" + expect(response).to render_template "custom_actions/upsale" end end end diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index 041f08e892b..b0a45d0b404 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -36,6 +36,7 @@ RSpec.describe EnterprisesController do subscriber: "Foobar", mail: "foo@example.org", starts_at: Date.today, + version: "1", expires_at: nil } end diff --git a/spec/controllers/shares_controller_spec.rb b/spec/controllers/shares_controller_spec.rb index cad2d376502..2ac3a4dc8c8 100644 --- a/spec/controllers/shares_controller_spec.rb +++ b/spec/controllers/shares_controller_spec.rb @@ -57,8 +57,9 @@ RSpec.describe SharesController do mock_permissions_for(user, &:forbid_everything) end - it "raises a RecordNotFound error" do - expect { make_request }.to raise_error(ActiveRecord::RecordNotFound) + it "returns a 404 status" do + make_request + expect(response).to have_http_status(:not_found) end end @@ -87,8 +88,9 @@ RSpec.describe SharesController do mock_permissions_for(user, &:forbid_everything) end - it "raises a RecordNotFound error" do - expect { make_request }.to raise_error(ActiveRecord::RecordNotFound) + it "returns a 404 status" do + make_request + expect(response).to have_http_status(:not_found) end end diff --git a/spec/controllers/work_packages_controller_spec.rb b/spec/controllers/work_packages_controller_spec.rb index 16093d4d61a..5032ca9e077 100644 --- a/spec/controllers/work_packages_controller_spec.rb +++ b/spec/controllers/work_packages_controller_spec.rb @@ -309,10 +309,46 @@ RSpec.describe WorkPackagesController do requires_permission_in_project do it "render the journal/index template" do call_action - expect(response).to render_template("journals/index") end end + + context "when there are internal comments", with_flag: { comments_with_restricted_visibility: true } do + render_views + + let(:admin) { create(:admin) } + let(:project) do + create(:project, identifier: "test_project", public: false, enabled_comments_with_restricted_visibility: true) + end + + before do + work_package = create(:work_package, id: 5173, project:) + create(:work_package_journal, + journable: work_package, + user: admin, + notes: "internal comment", + restricted: true, + version: 2) + end + + context "and the user does not have permission to see such comments" do + it "does not include internal comments" do + get("show", params: { format: "atom", id: 5173 }) + expect(response.body).not_to include("internal comment") + end + end + + context "and the user has permission to see such comments" do + before do + login_as admin + end + + it "includes internal comments" do + get("show", params: { format: "atom", id: 5173 }) + expect(response.body).to include("internal comment") + end + end + end end describe "redirect deep link", with_settings: { login_required?: true } do diff --git a/spec/features/activities/work_package/activities_spec.rb b/spec/features/activities/work_package/activities_spec.rb index 73ad5d2fd03..80801d5c147 100644 --- a/spec/features/activities/work_package/activities_spec.rb +++ b/spec/features/activities/work_package/activities_spec.rb @@ -34,7 +34,7 @@ require "support/flash/expectations" RSpec.describe "Work package activity", :js, :with_cuprite do include Flash::Expectations - let(:project) { create(:project) } + let(:project) { create(:project, enabled_comments_with_restricted_visibility: true) } let(:admin) { create(:admin) } let(:member_role) do create(:project_role, @@ -419,57 +419,117 @@ RSpec.describe "Work package activity", :js, :with_cuprite do end context "when multiple users are commenting on a workpackage" do - current_user { admin } - let(:work_package) { create(:work_package, project:, author: admin) } + context "when the user has permissions to see restricted comments" do + current_user { admin } + let(:work_package) { create(:work_package, project:, author: admin) } - before do - # set WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS to 1000 - # to speed up the polling interval for test duration - ENV["WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS"] = "1000" + before do + # set WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS to 1000 + # to speed up the polling interval for test duration + ENV["WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS"] = "1000" - # for some reason the journal is set to the "Anonymous" - # although the work_package is created by the admin - # so we need to update the journal to the admin manually to simulate the real world case - work_package.journals.first.update!(user: admin) + # for some reason the journal is set to the "Anonymous" + # although the work_package is created by the admin + # so we need to update the journal to the admin manually to simulate the real world case + work_package.journals.first.update!(user: admin) - wp_page.visit! - wp_page.wait_for_activity_tab + wp_page.visit! + wp_page.wait_for_activity_tab + end + + after do + ENV.delete("WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS") + end + + it "shows the comment of another user without browser reload" do + # simulate member creating a comment + first_journal = create(:work_package_journal, + user: member, + notes: "First comment by member", + journable: work_package, + version: 2) + + # the comment is shown without browser reload + activity_tab.expect_journal_notes(text: "First comment by member") + + # simulate comments made within the polling interval + create(:work_package_journal, user: member, notes: "Second comment by member", journable: work_package, version: 3) + create(:work_package_journal, user: member, notes: "Third comment by member", journable: work_package, version: 4) + + activity_tab.add_comment(text: "First comment by admin") + + activity_tab.expect_comments_order( + [ + "First comment by member", + "Second comment by member", + "Third comment by member", + "First comment by admin" + ] + ) + + first_journal.update!(notes: "First comment by member updated") + + # properly updates the comment when the comment is updated + activity_tab.expect_journal_notes(text: "First comment by member updated") + end end - after do - ENV.delete("WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS") - end + context "when the user does not have permissions to see restricted comments" do + current_user { member } + let(:work_package) { create(:work_package, project:, author: admin) } - it "shows the comment of another user without browser reload" do - # simulate member creating a comment - first_journal = create(:work_package_journal, - user: member, - notes: "First comment by member", - journable: work_package, - version: 2) + before do + # set WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS to 1000 + # to speed up the polling interval for test duration + ENV["WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS"] = "1000" - # the comment is shown without browser reload - activity_tab.expect_journal_notes(text: "First comment by member") + # for some reason the journal is set to the "Anonymous" + # although the work_package is created by the admin + # so we need to update the journal to the admin manually to simulate the real world case + work_package.journals.first.update!(user: admin) - # simulate comments made within the polling interval - create(:work_package_journal, user: member, notes: "Second comment by member", journable: work_package, version: 3) - create(:work_package_journal, user: member, notes: "Third comment by member", journable: work_package, version: 4) + wp_page.visit! + wp_page.wait_for_activity_tab + end - activity_tab.add_comment(text: "First comment by admin") + after do + ENV.delete("WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS") + end - activity_tab.expect_comments_order( - [ - "First comment by member", - "Second comment by member", - "Third comment by member", - "First comment by admin" - ] - ) + it "does not show the comment of another user if they don't have permissions to see it" do + # simulate member creating a comment + create(:work_package_journal, + user: admin, + notes: "First comment by admin", + journable: work_package, + version: 2) - first_journal.update!(notes: "First comment by member updated") + # the comment is shown without browser reload + activity_tab.expect_journal_notes(text: "First comment by admin") - # properly updates the comment when the comment is updated - activity_tab.expect_journal_notes(text: "First comment by member updated") + # simulate comments made within the polling interval + create(:work_package_journal, + user: admin, + notes: "Second comment by admin", + restricted: true, + journable: work_package, + version: 3) + create(:work_package_journal, + user: admin, + notes: "Third comment by admin", + restricted: true, + journable: work_package, + version: 4) + + activity_tab.add_comment(text: "First comment by member") + + activity_tab.expect_comments_order( + [ + "First comment by admin", + "First comment by member" + ] + ) + end end end diff --git a/spec/features/activities/work_package/restricted_visibility_comments_spec.rb b/spec/features/activities/work_package/restricted_visibility_comments_spec.rb index 1b9fbecfc41..b5ed478bace 100644 --- a/spec/features/activities/work_package/restricted_visibility_comments_spec.rb +++ b/spec/features/activities/work_package/restricted_visibility_comments_spec.rb @@ -32,16 +32,17 @@ require "spec_helper" RSpec.describe "Work package comments with restricted visibility", :js, - :with_cuprite, with_flag: { comments_with_restricted_visibility: true } do - let(:project) { create(:project) } - let(:admin) { create(:admin) } - let(:viewer) { create_user_with_restricted_comments_view_permissions } - let(:viewer_with_commenting_permission) { create_user_with_restricted_comments_view_and_write_permissions } - let(:project_admin) { create_user_as_project_admin } + include RestrictedVisibilityCommentsHelpers - let(:work_package) { create(:work_package, project:, author: admin) } - let(:first_comment) do + shared_let(:project) { create(:project, enabled_comments_with_restricted_visibility: true) } + shared_let(:admin) { create(:admin) } + shared_let(:viewer) { create_user_with_restricted_comments_view_permissions } + shared_let(:viewer_with_commenting_permission) { create_user_with_restricted_comments_view_and_write_permissions } + shared_let(:project_admin) { create_user_as_project_admin } + + shared_let(:work_package) { create(:work_package, project:, author: admin) } + shared_let(:first_comment) do create(:work_package_journal, user: admin, notes: "A (restricted) comment by admin", journable: work_package, version: 2, restricted: true) end @@ -52,17 +53,37 @@ RSpec.describe "Work package comments with restricted visibility", context "with an admin user" do current_user { admin } - before do - wp_page.visit! - wp_page.wait_for_activity_tab + context "when the feature is enabled for the project" do + before do + wp_page.visit! + wp_page.wait_for_activity_tab + end + + it "allows adding a comment with restricted visibility" do + activity_tab.expect_input_field + + activity_tab.add_comment(text: "First (restricted) comment by admin", restricted: true) + + activity_tab.expect_journal_notes(text: "First (restricted) comment by admin") + end end - it "allows adding a comment with restricted visibility" do - activity_tab.expect_input_field + context "when the feature is not enabled for the project" do + before do + project.enabled_comments_with_restricted_visibility = false + project.save! - activity_tab.add_comment(text: "First (restricted) comment by admin", restricted: true) + wp_page.visit! + wp_page.wait_for_activity_tab + end - activity_tab.expect_journal_notes(text: "First (restricted) comment by admin") + it "allows adding a comment with restricted visibility" do + activity_tab.expect_input_field + + activity_tab.type_comment("This comment cannot be restricted") + + expect(page).not_to have_test_selector("op-work-package-journal-restricted-comment-checkbox") + end end end @@ -173,36 +194,97 @@ RSpec.describe "Work package comments with restricted visibility", end end - def create_user_as_project_admin - member_role = create(:project_role, - permissions: %i[view_work_packages add_work_package_notes - edit_own_work_package_notes - view_comments_with_restricted_visibility - add_comments_with_restricted_visibility - edit_own_comments_with_restricted_visibility - edit_others_comments_with_restricted_visibility]) - create(:user, firstname: "Project", lastname: "Admin", - member_with_roles: { project => member_role }) - end + describe "mentioning users in comments" do + current_user { project_admin } - def create_user_with_restricted_comments_view_permissions - viewer_role = create(:project_role, permissions: %i[view_work_packages view_comments_with_restricted_visibility]) - create(:user, - firstname: "A", - lastname: "Viewer", - member_with_roles: { project => viewer_role }) - end + shared_let(:user_without_restricted_comments_view_permissions) do + create_user_without_restricted_comments_view_permissions + end - def create_user_with_restricted_comments_view_and_write_permissions - viewer_role_with_commenting_permission = create(:project_role, - permissions: %i[view_work_packages add_work_package_notes - edit_own_work_package_notes - view_comments_with_restricted_visibility - add_comments_with_restricted_visibility - edit_own_comments_with_restricted_visibility]) - create(:user, - firstname: "A", - lastname: "Viewer", - member_with_roles: { project => viewer_role_with_commenting_permission }) + shared_let(:group) { create(:group, firstname: "A", lastname: "Group") } + shared_let(:group_member) do + group_role = create(:project_role) + create(:member, + principal: group, + project:, + roles: [group_role]) + end + + before do + wp_page.visit! + wait_for_reload + expect_angular_frontend_initialized + wp_page.wait_for_activity_tab + end + + context "with restricted comments initially enabled" do + it "restricts mentions to project members with view comments with restricted visibility permission" do + activity_tab.open_new_comment_editor + expect(page).to have_test_selector("op-work-package-journal-form-element") + + activity_tab.check_restricted_visibility_comment_checkbox + activity_tab.refocus_editor + activity_tab.type_comment("@") + + expect(page.all(".mention-list-item").map(&:text)) + .to contain_exactly("Project Admin", "Restricted Viewer", "Restricted ViewerCommenter") + end + end + + context "with restricted comments initially disabled" do + it "allows mentioning project members but they are sanitized when the checkbox is checked" do + activity_tab.type_comment("@") + + expect(page.all(".mention-list-item").map(&:text)) + .to contain_exactly("A Viewer", "Group", "Project Admin", "Restricted Viewer", "Restricted ViewerCommenter") + + page.find(".mention-list-item", text: "A Viewer").click + activity_tab.type_comment("@Restricted") + page.first(".mention-list-item", text: "Restricted Viewer").click + activity_tab.type_comment("@Group") + page.find(".mention-list-item", text: "Group").click + + activity_tab.check_restricted_visibility_comment_checkbox + + page.within_test_selector("op-work-package-journal-form-element") do + expect(page.all("a.mention").map(&:text)) + .to contain_exactly("@Restricted Viewer") + + expect(page).to have_text("@A Viewer") & have_text("@Group") + end + end + end + + context "when editing a restricted comment" do + it "honors mentionable principals" do + activity_tab.within_journal_entry(first_comment) do + page.find_test_selector("op-wp-journal-#{first_comment.id}-action-menu").click + page.find_test_selector("op-wp-journal-#{first_comment.id}-edit").click + + activity_tab.type_comment(" @") + end + + expect(page.all(".mention-list-item").map(&:text)) + .to contain_exactly("Project Admin", "Restricted Viewer", "Restricted ViewerCommenter") + end + end + + context "with a server error" do + before do + allow(WorkPackages::ActivitiesTab::RestrictedMentionsSanitizer).to receive(:sanitize) + .and_raise(RuntimeError, "Something went wrong!!!") + end + + it "shows an error message" do + activity_tab.type_comment("@Restricted") + page.first(".mention-list-item", text: "Restricted Viewer").click + + activity_tab.check_restricted_visibility_comment_checkbox + + page.within_test_selector("op-primer-flash-message") do + expect(page).to have_text("Something went wrong!!!") + end + end + end end end diff --git a/spec/features/custom_styles/tabs_navigation_spec.rb b/spec/features/custom_styles/tabs_navigation_spec.rb index 40dc6ea58ba..007e1daa509 100644 --- a/spec/features/custom_styles/tabs_navigation_spec.rb +++ b/spec/features/custom_styles/tabs_navigation_spec.rb @@ -38,7 +38,8 @@ RSpec.describe "Tabs navigation and content switching on the admin/design page" end it "redirects to upsale page" do - expect(page).to have_css(".upsale-notification") + expect(page).to have_enterprise_upsale_page + expect(page).to have_text "Available only through the Basic enterprise plan" end end diff --git a/spec/features/menu_items/help_menu_spec.rb b/spec/features/menu_items/help_menu_spec.rb index d4d27b0bde4..e6e81372760 100644 --- a/spec/features/menu_items/help_menu_spec.rb +++ b/spec/features/menu_items/help_menu_spec.rb @@ -68,7 +68,6 @@ RSpec.describe "Help menu items", :js do include_context "support links" def fake_an_enterprise_token - allow(EnterpriseToken).to receive(:show_banners?).and_return(false) allow(EnterpriseToken).to receive(:active?).and_return(true) end diff --git a/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb b/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb index 1aa2c64900a..185e8704a06 100644 --- a/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb +++ b/spec/features/notifications/notification_center/notification_center_date_alerts_spec.rb @@ -195,11 +195,13 @@ RSpec.describe "Notification center date alerts", :js, with_settings: { journal_ side_menu.click_item "Date alert" expect(page).to have_current_path(/notifications\/date_alerts/) - expect(page).to have_text "Date alerts is an Enterprise" - expect(page).to have_text "Please upgrade to a paid plan " + expect(page).to have_enterprise_upsale_page(:basic) # It does not allows direct url access visit notifications_center_path(filter: "reason", name: "dateAlert") + + expect(page).to have_current_path(/notifications\/date_alerts/) + expect(page).to have_enterprise_upsale_page(:basic) end end diff --git a/spec/features/notifications/notification_center/split_screen_spec.rb b/spec/features/notifications/notification_center/split_screen_spec.rb index dc216f68abc..2b375b3e37e 100644 --- a/spec/features/notifications/notification_center/split_screen_spec.rb +++ b/spec/features/notifications/notification_center/split_screen_spec.rb @@ -67,7 +67,7 @@ RSpec.describe "Split screen in the notification center", :js do split_screen.switch_to_tab tab: "relations" split_screen.expect_tab :relations relations_tab = Components::WorkPackages::Relations.new(work_package) - relations_tab.expect_no_relation work_package + relations_tab.expect_no_relations # Navigate to full view and back wp_full = split_screen.switch_to_fullscreen diff --git a/spec/features/projects/life_cycle/overview_page/dialog/permission_spec.rb b/spec/features/projects/life_cycle/overview_page/dialog/permission_spec.rb index 982c83ea190..912b1b58cee 100644 --- a/spec/features/projects/life_cycle/overview_page/dialog/permission_spec.rb +++ b/spec/features/projects/life_cycle/overview_page/dialog/permission_spec.rb @@ -38,6 +38,10 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { current_user { user } before do + # Mocking the Project::Phase.visible scope + allow(Project).to receive(:allowed_to).and_call_original + allow(Project).to receive(:allowed_to).with(user, :view_project_phases).and_return(project) + mock_permissions_for(user) do |mock| mock.allow_in_project(*permissions, project:) # any project end @@ -67,7 +71,9 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { it "does not show the edit buttons" do overview_page.within_life_cycles_sidebar do - expect(page).to have_no_css("[data-test-selector='project-life-cycles-edit-button']") + project_life_cycles.each do |lc| + expect(page).to have_no_link(href: edit_project_phase_path(lc)) + end end end end @@ -77,7 +83,9 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { it "shows the edit buttons" do overview_page.within_life_cycles_sidebar do - expect(page).to have_css("[data-test-selector='project-life-cycles-edit-button']") + project_life_cycles.each do |lc| + expect(page).to have_link(href: edit_project_phase_path(lc)) + end end end end diff --git a/spec/features/projects/life_cycle/overview_page/dialog/update_spec.rb b/spec/features/projects/life_cycle/overview_page/dialog/update_spec.rb index b41cf2793a7..e50c89747af 100644 --- a/spec/features/projects/life_cycle/overview_page/dialog/update_spec.rb +++ b/spec/features/projects/life_cycle/overview_page/dialog/update_spec.rb @@ -53,18 +53,14 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { end it "shows all the Project::Phases without a value" do - dialog = overview_page.open_edit_dialog_for_life_cycles + project_life_cycles.each do |life_cycle| + dialog = overview_page.open_edit_dialog_for_life_cycle(life_cycle) + dialog.expect_input(life_cycle.name, value: "") - dialog.expect_input("Initiating", value: "", position: 1) - dialog.expect_input("Planning", value: "", position: 2) - dialog.expect_input("Executing", value: "", position: 3) - dialog.expect_input("Closing", value: "", position: 4) + dialog.submit # Saving the dialog is successful + dialog.expect_closed + end - # Saving the dialog is successful - dialog.submit - dialog.expect_closed - - # Sidebar displays the same empty values project_life_cycles.each do |life_cycle| overview_page.within_life_cycle_container(life_cycle) do expect(page).to have_text "-" @@ -75,31 +71,28 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { context "when all LifeCycleSteps have a value" do it "shows all the Project::Phases and updates them correctly" do - dialog = overview_page.open_edit_dialog_for_life_cycles + # Set a value for life_cycle_initiating + dialog = overview_page.open_edit_dialog_for_life_cycle(life_cycle_initiating) expect_angular_frontend_initialized - project.available_phases.each do |step| - dialog.expect_input_for(step) - end + dialog.expect_input_for(life_cycle_initiating) initiating_dates = [start_date - 1.week, start_date] retry_block do # Retrying due to a race condition between filling the input vs submitting the form preview. original_dates = [life_cycle_initiating.start_date, life_cycle_initiating.finish_date] - dialog.set_date_for(life_cycle_initiating, values: original_dates) + dialog.set_date_for(values: original_dates) page.driver.clear_network_traffic - dialog.set_date_for(life_cycle_initiating, values: initiating_dates) + dialog.set_date_for(values: initiating_dates) - dialog.expect_caption(life_cycle_initiating, text: "Duration: 8 working days") + dialog.expect_caption(text: "Duration: 8 working days") # Ensure that only 1 ajax request is triggered after setting the date range. expect(page.driver.browser.network.traffic.size).to eq(1) end - dialog.clear_date_for(life_cycle_planning) - # Saving the dialog is successful dialog.submit dialog.expect_closed @@ -109,6 +102,18 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { expect(page).to have_text initiating_dates.map { I18n.l(it) }.join("\n-\n") end + # Clear the value of life_cycle_planning + dialog = overview_page.open_edit_dialog_for_life_cycle(life_cycle_planning) + expect_angular_frontend_initialized + + dialog.expect_input_for(life_cycle_planning) + dialog.clear_date + + # Saving the dialog is successful + dialog.submit + dialog.expect_closed + + # Sidebar is refreshed with the updated values ready_for_planning_dates = [ life_cycle_planning.start_date, life_cycle_planning.finish_date @@ -140,66 +145,6 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { "#{I18n.l life_cycle_planning_was.finish_date}") end end - - it "shows the validation errors", with_settings: { journal_aggregation_time_minutes: 0 } do - expect_angular_frontend_initialized - wait_for_network_idle - - dialog = overview_page.open_edit_dialog_for_life_cycles - - expected_text = "Date range can't be earlier than the previous phase's end date." - - # Cycling is required so we always select a different date on the datepicker, - # making sure the change event is triggered. - cycled_days = [0, 1].cycle - - # Retrying due to a race condition between filling the input vs submitting the form preview. - retry_block do - values = [start_date + cycled_days.next.days] * 2 - dialog.set_date_for(life_cycle_planning, values:) - - dialog.expect_validation_message(life_cycle_planning, text: expected_text) - end - - # Saving the dialog fails - dialog.submit - dialog.expect_open - - # The validation message is kept after the unsuccessful save attempt - dialog.expect_validation_message(life_cycle_planning, text: expected_text) - - retry_block do - # The validation message is cleared when date is changed - values = [start_date + 2.days + cycled_days.next.days] * 2 - dialog.set_date_for(life_cycle_planning, values:) - dialog.expect_no_validation_message(life_cycle_planning) - end - - # Saving the dialog is successful - dialog.submit - dialog.expect_closed - - activity_page.visit! - - activity_page.show_details - - life_cycle_planning_was = life_cycle_planning.dup - life_cycle_planning.reload - activity_page.within_journal(number: 1) do - expected_date_from = [ - I18n.l(life_cycle_planning_was.start_date), - I18n.l(life_cycle_planning_was.finish_date) - ].join(" - ") - - expected_date_to = [ - I18n.l(life_cycle_planning.start_date), - I18n.l(life_cycle_planning.finish_date) - ].join(" - ") - activity_page.expect_activity("Planning changed from " \ - "#{expected_date_from} to " \ - "#{expected_date_to}") - end - end end context "when there is an invalid custom field on the project (Regression#60666)" do @@ -211,7 +156,7 @@ RSpec.describe "Edit project phases on project overview page", :js, with_flag: { end it "allows saving and closing the dialog without the custom field validation to interfere" do - dialog = overview_page.open_edit_dialog_for_life_cycles + dialog = overview_page.open_edit_dialog_for_life_cycle(life_cycle_initiating) expect_angular_frontend_initialized diff --git a/spec/features/projects/life_cycle/overview_page/sidebar_spec.rb b/spec/features/projects/life_cycle/overview_page/sidebar_spec.rb index 5c47083dc77..009e410cb34 100644 --- a/spec/features/projects/life_cycle/overview_page/sidebar_spec.rb +++ b/spec/features/projects/life_cycle/overview_page/sidebar_spec.rb @@ -92,6 +92,22 @@ RSpec.describe "Show project life cycles on project overview page", :js, with_fl end end + it "shows a hover card when you hover over a gate" do + overview_page.visit_page + + overview_page.within_life_cycles_sidebar do + page.find_test_selector("phase-#{life_cycle_planning.id}-start-gate").hover + end + + expect(page).to have_test_selector("phase-gate-hover-card-name", text: life_cycle_planning.start_gate_name) + + overview_page.within_life_cycles_sidebar do + page.find_test_selector("phase-#{life_cycle_planning.id}-finish-gate").hover + end + + expect(page).to have_test_selector("phase-gate-hover-card-name", text: life_cycle_planning.finish_gate_name) + end + it "does not show phases not enabled for this project in a sidebar" do life_cycle_executing.toggle!(:active) diff --git a/spec/features/projects/persisted_lists_spec.rb b/spec/features/projects/persisted_lists_spec.rb index f6b740e44f3..e3283e25e08 100644 --- a/spec/features/projects/persisted_lists_spec.rb +++ b/spec/features/projects/persisted_lists_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # -- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -39,6 +41,7 @@ RSpec.describe "Persisted lists on projects index page", shared_let(:custom_field) { create(:text_project_custom_field) } shared_let(:invisible_custom_field) { create(:project_custom_field, admin_only: true) } + shared_let(:list_custom_field) { create(:list_project_custom_field) } shared_let(:project) do create(:project, @@ -52,7 +55,8 @@ RSpec.describe "Persisted lists on projects index page", public: true) project.custom_field_values = { invisible_custom_field.id => "Secret CF", - custom_field.id => "Visible CF" + custom_field.id => "Visible CF", + list_custom_field.id => list_custom_field.possible_values.first.id } project.save project @@ -450,7 +454,6 @@ RSpec.describe "Persisted lists on projects index page", it "keep the query active when applying orders, page and column changes" do projects_page.visit! - # The user can select the list but cannot see another user's list projects_page.set_sidebar_filter(my_projects_list.name) projects_page.expect_no_sidebar_filter(another_users_projects_list.name) @@ -551,6 +554,32 @@ RSpec.describe "Persisted lists on projects index page", development_project) # Because it is on the second page end + it "shows the saved filter values in the filter section" do + my_projects_list.where("project_status_code", "=", Project.status_codes["on_track"]) + my_projects_list.save! + + projects_page.visit! + projects_page.set_sidebar_filter(my_projects_list.name) + projects_page.expect_filter_set("project_status_code", value: "On track") + + projects_page.open_filters + projects_page.set_filter(list_custom_field.column_name, + list_custom_field.name, + "is (OR)", + [list_custom_field.possible_values.first.value]) + + wait_for_reload + projects_page.save_query + + projects_page.set_sidebar_filter(my_projects_list.name) + projects_page.expect_filter_set("project_status_code", value: "On track") + + projects_page.expect_filter_set( + list_custom_field.column_name, + value: list_custom_field.possible_values.first.value + ) + end + it "cannot access another user`s list" do visit projects_path(query_id: another_users_projects_list.id) diff --git a/modules/meeting/spec/requests/api/v3/attachments/meeting_minutes_spec.rb b/spec/features/projects/settings/activities_settings_spec.rb similarity index 55% rename from modules/meeting/spec/requests/api/v3/attachments/meeting_minutes_spec.rb rename to spec/features/projects/settings/activities_settings_spec.rb index 2bc8640fdea..965cb437f49 100644 --- a/modules/meeting/spec/requests/api/v3/attachments/meeting_minutes_spec.rb +++ b/spec/features/projects/settings/activities_settings_spec.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -28,17 +29,29 @@ #++ require "spec_helper" -require "requests/api/v3/attachments/attachment_resource_shared_examples" -RSpec.describe "meeting minutes attachments" do - it_behaves_like "an APIv3 attachment resource" do - let(:attachment_type) { :meeting_content } +RSpec.describe "WorkPackages-Settings-Activities", :js do + let!(:project) { create(:project) } + let(:activities_settings_page) { Pages::Projects::Settings::Activities.new(project) } - let(:create_permission) { :create_meetings } - let(:read_permission) { :view_meetings } - let(:update_permission) { :edit_meetings } + current_user { create(:admin) } - let(:meeting_content) { create(:meeting_minutes, meeting:) } - let(:meeting) { create(:meeting, project:) } + it "enables and disables the settings for the project", with_ee: %i[comments_with_restricted_visibility] do + activities_settings_page.visit! + expect(page).to have_css("#activities-form") + + expect(page).to have_field(:project_enabled_comments_with_restricted_visibility, checked: false) + + check("Enable restricted visibility comments") + click_link_or_button "Save" + + expect_and_dismiss_flash(message: "Successful update.") + expect(page).to have_field(:project_enabled_comments_with_restricted_visibility, checked: true) + + uncheck("Enable restricted visibility comments") + click_link_or_button "Save" + + expect_and_dismiss_flash(message: "Successful update.") + expect(page).to have_field(:project_enabled_comments_with_restricted_visibility, checked: false) end end diff --git a/spec/features/projects/work_packages_settings_spec.rb b/spec/features/projects/settings/work_packages_settings_spec.rb similarity index 79% rename from spec/features/projects/work_packages_settings_spec.rb rename to spec/features/projects/settings/work_packages_settings_spec.rb index c3cf8bd111c..bb22f9bfeb8 100644 --- a/spec/features/projects/work_packages_settings_spec.rb +++ b/spec/features/projects/settings/work_packages_settings_spec.rb @@ -72,11 +72,34 @@ RSpec.describe "Projects", "work packages settings menu", :js do end end - context "when the user does not have access to any tabs" do + context "when the user has access to the activities tab", with_ee: %i[comments_with_restricted_visibility] do let(:permissions) { %i(edit_project view_work_packages) } current_user { create(:user, member_with_permissions: { project => permissions }) } + it "displays the custom fields tab" do + work_packages_settings_page.visit! + expect(page).to have_css(".tabnav-tab", text: "Activity") + expect(page).to have_css("#activities-form") + end + end + + context "when the user has access to activities tab but not to enterprise" do + let(:permissions) { %i(edit_project view_work_packages) } + + current_user { create(:user, member_with_permissions: { project => permissions }) } + + it "shows the enterprise banner" do + work_packages_settings_page.visit! + expect(page).to have_test_selector("op-enterprise-banner") + end + end + + context "when the user does not have access to any tabs" do + let(:permissions) { %i(view_work_packages) } + + current_user { create(:user, member_with_permissions: { project => permissions }) } + it "does not display the menu entry" do general_settings_page.visit! expect(page).to have_no_css(".op-menu--item-title", text: "Work packages") diff --git a/spec/features/types/form_configuration_query_spec.rb b/spec/features/types/form_configuration_query_spec.rb index ffb752241c4..f259596ad40 100644 --- a/spec/features/types/form_configuration_query_spec.rb +++ b/spec/features/types/form_configuration_query_spec.rb @@ -226,6 +226,7 @@ RSpec.describe "form query configuration", :js do expect(column_names).to eq %i[id] form.edit_query_group("Second query") + modal.switch_to "Columns" columns.expect_checked "ID" columns.apply diff --git a/spec/features/work_packages/custom_actions/custom_actions_spec.rb b/spec/features/work_packages/custom_actions/custom_actions_spec.rb index 88e100809b3..6422839b1bf 100644 --- a/spec/features/work_packages/custom_actions/custom_actions_spec.rb +++ b/spec/features/work_packages/custom_actions/custom_actions_spec.rb @@ -312,9 +312,8 @@ RSpec.describe "Custom actions", :js, with_ee: %i[custom_actions] do within(".custom-actions") do # When hovering over the button, the description is displayed - button = find(".custom-action--button", text: "Unassign") - expect(button["title"]) - .to eql "Removes the assignee" + expect(page) + .to have_button("Unassign", title: "Removes the assignee") end wp_page.click_custom_action("Unassign") diff --git a/spec/features/work_packages/details/milestones_spec.rb b/spec/features/work_packages/details/milestones_spec.rb index c50de5a0a4e..3fd234840a1 100644 --- a/spec/features/work_packages/details/milestones_spec.rb +++ b/spec/features/work_packages/details/milestones_spec.rb @@ -11,7 +11,6 @@ RSpec.describe "Milestones full screen v iew", :js do end let(:wp_page) { Pages::FullWorkPackage.new(work_package, project) } - let(:button) { find(".add-work-package", wait: 5) } before do login_as(user) @@ -28,9 +27,7 @@ RSpec.describe "Milestones full screen v iew", :js do end it "shows the button as enabled" do - expect(button).not_to be_disabled - - button.click + click_button("Create", class: "add-work-package") expect(page).to have_css(".menu-item", text: type.name.upcase) end end @@ -45,7 +42,7 @@ RSpec.describe "Milestones full screen v iew", :js do end it "shows the button as correctly disabled" do - expect(button["disabled"]).to be_truthy + expect(page).to have_button("Create", class: "add-work-package", disabled: true) end end end diff --git a/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb b/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb index 6b430f9fbf7..378829a4ce8 100644 --- a/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb +++ b/spec/features/work_packages/display_representations/switch_display_representations_on_mobile_spec.rb @@ -46,7 +46,6 @@ RSpec.describe "Switching work package view on mobile", :js, :selenium do before do wp_1 wp_2 - allow(EnterpriseToken).to receive(:show_banners?).and_return(false) login_as(user) wp_table.visit! diff --git a/spec/features/work_packages/export_spec.rb b/spec/features/work_packages/export_spec.rb index dbbd962b71e..010db9763a1 100644 --- a/spec/features/work_packages/export_spec.rb +++ b/spec/features/work_packages/export_spec.rb @@ -102,10 +102,19 @@ RSpec.describe "work package export", :js, :selenium do end end - def open_export_dialog! - wp_table.visit_query query + def open_export_dialog!(query_target = :query) + case query_target + when :query + wp_table.visit_query query + when :default + wp_table.visit_with_params "" + else + raise ArgumentError, "query_target must be :query or :default" + end + work_packages_page.ensure_loaded settings_menu.open_and_choose I18n.t("js.toolbar.settings.export") + expect(page).to have_css("#op-work-packages-export-dialog", wait: 5) click_on export_type sleep 0.1 end @@ -115,6 +124,26 @@ RSpec.describe "work package export", :js, :selenium do expect(page).to have_no_button(I18n.t("export.dialog.submit"), wait: 1) end + def close_export_dialog! + expect(page).to have_button(I18n.t("js.button_close")) + click_on I18n.t("js.button_close") + expect(page).to have_no_button(I18n.t("js.button_close")) + end + + def export_and_reopen_dialog!(query_target = :query) + export! + close_export_dialog! + open_export_dialog!(query_target) + end + + def expect_selected_columns(columns = %w[]) + selected_columns = page.within(".op-draggable-autocomplete--selected") do + all(".op-draggable-autocomplete--item-text").map(&:text) + end + + expect(selected_columns).to eq(columns) + end + context "with Query options" do let(:export_type) { I18n.t("export.dialog.format.options.pdf.label") } let(:expected_mime_type) { :pdf } @@ -182,6 +211,7 @@ RSpec.describe "work package export", :js, :selenium do let(:expected_params) { default_expected_params } before do + query.export_settings.delete_all open_export_dialog! end @@ -204,6 +234,91 @@ RSpec.describe "work package export", :js, :selenium do end end + context "with a saved query" do + let!(:query) { create(:query, name: "saved settings query", user: current_user, project:) } + let(:export_type) { I18n.t("export.dialog.format.options.csv.label") } + let(:expected_mime_type) { :csv } + let(:expected_columns) { %w[ID Subject Type Status Assignee Priority] } + let(:expected_params) do + default_expected_params.merge( + { + title: "saved settings query", + show_descriptions: "true", + columns: %w[subject type status assigned_to priority] + } + ) + end + let(:query_columns) do + query.displayable_columns.filter_map { |c| c.name.to_s if expected_columns.include?(c.name.to_s) } + end + + before do + open_export_dialog! + end + + it "saves the export settings" do + # Ensure that the option to save export settings is there and both checkboxes are unchecked + expect(page.find_test_selector("op-work-packages-export-dialog-form-save-export-settings")).not_to be_checked + expect(page.find_test_selector("show-descriptions-csv")).not_to be_checked + expect_selected_columns(expected_columns) + + # Save settings and include descriptions + check I18n.t("export.dialog.save_export_settings.label") + check I18n.t("export.dialog.xls.include_descriptions.label") + # Remove the first column + page.within(".op-draggable-autocomplete--selected") do + first(".op-draggable-autocomplete--remove-item").click + end + + export_and_reopen_dialog! + # Last settings are remembered + expect(page.find_test_selector("show-descriptions-csv")).to be_checked + expect(page.find_test_selector("op-work-packages-export-dialog-form-save-export-settings")).to be_checked + expect_selected_columns(expected_columns - ["ID"]) + + # Uncheck both checkboxes again (do not include descriptions, do not save changes) + uncheck I18n.t("export.dialog.save_export_settings.label") + uncheck I18n.t("export.dialog.xls.include_descriptions.label") + # Remove the last column + page.within(".op-draggable-autocomplete--selected") do + all(".op-draggable-autocomplete--remove-item").last.click + end + # Adjust expectation and export + expected_params[:show_descriptions] = "false" + expected_params[:columns].pop + export_and_reopen_dialog! + + # Last saved settings are restored + expect(page.find_test_selector("show-descriptions-csv")).to be_checked + expect(page.find_test_selector("op-work-packages-export-dialog-form-save-export-settings")).to be_checked + expect_selected_columns(expected_columns - ["ID"]) + end + end + + context "with an unsaved query" do + let(:export_type) { I18n.t("export.dialog.format.options.csv.label") } + let(:expected_mime_type) { :csv } + let(:expected_params) { default_expected_params.merge({ title: "All open", show_descriptions: "true" }) } + + before do + open_export_dialog!(:default) + end + + it "does not offer to save export settings" do + # There is no save option + expect(page).not_to have_test_selector("op-work-packages-export-dialog-form-save-export-settings") + # show_descriptions is unchecked by default + expect(page.find_test_selector("show-descriptions-csv")).not_to be_checked + + # Check show_descriptions and export, then reopen dialog + check I18n.t("export.dialog.xls.include_descriptions.label") + export_and_reopen_dialog!(:default) + + # show_descriptions is still unchecked + expect(page.find_test_selector("show-descriptions-csv")).not_to be_checked + end + end + context "with XLS export" do let(:export_type) { I18n.t("export.dialog.format.options.xls.label") } let(:expected_mime_type) { :xls } @@ -299,12 +414,25 @@ RSpec.describe "work package export", :js, :selenium do end context "with long text fields selection" do - let(:expected_params) { default_params_report.merge({ long_text_fields: "description 43" }) } + let(:expected_params) { default_params_report.merge({ long_text_fields: "description #{cf_text_b.id}" }) } it "exports a pdf report with all remaining custom fields" do - find("span.op-draggable-autocomplete--item-text", text: "Long text custom field") - .sibling(".op-draggable-autocomplete--remove-item").click - export! + # Remove one custom field + page.within(".op-angular-component[data-id='\"ltf-select-export-pdf-report\"']") do + find(".op-draggable-autocomplete--item-text", text: "Long text custom field") + .sibling(".op-draggable-autocomplete--remove-item").click + end + + # Save export settings, export and reopen dialog + check I18n.t("export.dialog.save_export_settings.label") + export_and_reopen_dialog! + choose export_sub_type + + selected_long_fields = page.within(".op-angular-component[data-id='\"ltf-select-export-pdf-report\"']") do + all(".op-draggable-autocomplete--item-text").map(&:text) + end + # The removed field has been saved + expect(selected_long_fields).to contain_exactly("Description", cf_text_b.name) end end diff --git a/spec/features/work_packages/highlighting_spec.rb b/spec/features/work_packages/highlighting_spec.rb index 41763c187a9..da9902953c6 100644 --- a/spec/features/work_packages/highlighting_spec.rb +++ b/spec/features/work_packages/highlighting_spec.rb @@ -53,7 +53,6 @@ RSpec.describe "Work Package highlighting fields", # Ensure Rails and Capybara caches are cleared Rails.cache.clear Capybara.reset! - allow(EnterpriseToken).to receive(:show_banners?).and_return(false) login_as(user) wp_table.visit_query query wp_table.expect_work_package_listed wp_1, wp_2 diff --git a/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb b/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb index fca8da623db..29d1584c42a 100644 --- a/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb +++ b/spec/features/work_packages/table/baseline/baseline_rendering_spec.rb @@ -426,7 +426,7 @@ RSpec.describe "baseline rendering", baseline_modal.expect_closed baseline_modal.toggle_drop_modal baseline_modal.expect_open - expect(page).to have_css(".op-baseline--enterprise-title") + expect(page).to have_enterprise_banner # only yesterday is selectable page.select("a specific date", from: "op-baseline-filter") expect(page).to have_no_select("op-baseline-filter", selected: "a specific date") diff --git a/spec/helpers/enterprise_helper_spec.rb b/spec/helpers/enterprise_helper_spec.rb index dd7f0b7093e..ba768e1ea37 100644 --- a/spec/helpers/enterprise_helper_spec.rb +++ b/spec/helpers/enterprise_helper_spec.rb @@ -52,7 +52,7 @@ RSpec.describe EnterpriseHelper do let(:token) { instance_double(EnterpriseToken, features: %i[baseline_comparison virus_scanning]) } it "returns translated names of features" do - expect(helper.enterprise_plan_additional_features(token)).to eq("Attachment Virus Scanning, Baseline Comparison") + expect(helper.enterprise_plan_additional_features(token)).to eq("Antivirus Scanning, Baseline Comparisons") end end @@ -63,7 +63,7 @@ RSpec.describe EnterpriseHelper do end it "returns translated names of features and removes unknowns" do - expect(helper.enterprise_plan_additional_features(token)).to eq("Work Package Sharing") + expect(helper.enterprise_plan_additional_features(token)).to eq("Share work packages with external users") end end end diff --git a/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb b/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb index c8d21fde89e..0b35102cb5b 100644 --- a/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb +++ b/spec/lib/api/v3/activities/activity_eager_loading_wrapper_spec.rb @@ -70,16 +70,6 @@ RSpec.describe API::V3::Activities::ActivityEagerLoadingWrapper, with_settings: expect(meeting.journals).to be_wrappable end - it "can wrap MeetingAgenda journals" do - meeting_agenda = create(:meeting_agenda, meeting:) - expect(meeting_agenda.journals).to be_wrappable - end - - it "can wrap MeetingMinutes journals" do - meeting_minutes = create(:meeting_minutes, meeting:) - expect(meeting_minutes.journals).to be_wrappable - end - it "can wrap Budget journals" do budget = create(:budget, project:, author: user) expect(budget.journals).to be_wrappable diff --git a/spec/lib/api/v3/configuration/configuration_representer_spec.rb b/spec/lib/api/v3/configuration/configuration_representer_spec.rb index 4ca2e451442..8e96007a38b 100644 --- a/spec/lib/api/v3/configuration/configuration_representer_spec.rb +++ b/spec/lib/api/v3/configuration/configuration_representer_spec.rb @@ -245,6 +245,24 @@ RSpec.describe API::V3::Configuration::ConfigurationRepresenter do end end end + + describe "availableFeatures" do + context "without any features" do + it "is an empty array" do + expect(subject) + .to be_json_eql([].to_json) + .at_path("availableFeatures") + end + end + + context "with certain features allowed", with_ee: %i[some_value foobar] do + it "is an array of strings of those flags" do + expect(subject) + .to be_json_eql(%w(some_value foobar).to_json) + .at_path("availableFeatures") + end + end + end end describe "_embedded" do diff --git a/spec/models/enterprise_token_spec.rb b/spec/models/enterprise_token_spec.rb index 1475a548211..c2e121627bf 100644 --- a/spec/models/enterprise_token_spec.rb +++ b/spec/models/enterprise_token_spec.rb @@ -1,14 +1,44 @@ +# 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. +#++ + require "spec_helper" RSpec.describe EnterpriseToken do let(:object) { OpenProject::Token.new domain: Setting.host_name } - let(:ee_manager_visible) { true } + let(:ee_hide_banners) { true } - subject { EnterpriseToken.new(encoded_token: "foo") } + subject { described_class.new(encoded_token: "foo") } before do RequestStore.delete :current_ee_token - allow(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(ee_manager_visible) + allow(OpenProject::Configuration).to receive(:ee_hide_banners?).and_return(ee_hide_banners) end describe ".active?" do @@ -39,81 +69,20 @@ RSpec.describe EnterpriseToken do end end - describe ".show_banners?" do - before do - allow(described_class).to receive(:allows_to?).with(:active_feature).and_return(true) - allow(described_class).to receive(:allows_to?).with(:inactive_feature).and_return(false) - end - + describe ".hide_banners?" do context "when ee manager is visible" do - let(:ee_manager_visible) { true } + let(:ee_hide_banners) { true } - context "when token is active" do - before { allow(described_class).to receive(:active?).and_return(true) } - - it "returns false when requesting without a feature" do - expect(described_class).not_to be_show_banners - end - - it "returns false when requesting with a feature that is enabled in the token" do - expect(described_class).not_to be_show_banners(feature: :active_feature) - end - - it "returns true when requesting with a feature that is disabled in the token" do - expect(described_class).to be_show_banners(feature: :inactive_feature) - end - end - - context "when token is inactive" do - before { allow(described_class).to receive(:active?).and_return(false) } - - it "returns true when requesting without a feature" do - expect(described_class).to be_show_banners - end - - it "returns true when requesting with a feature that is enabled in the token" do - expect(described_class).to be_show_banners(feature: :active_feature) - end - - it "returns true when requesting with a feature that is disabled in the token" do - expect(described_class).to be_show_banners(feature: :inactive_feature) - end + it "returns true" do + expect(described_class).to be_hide_banners end end context "when ee manager is not visible" do - let(:ee_manager_visible) { false } + let(:ee_hide_banners) { false } - context "when token is active" do - before { allow(described_class).to receive(:active?).and_return(true) } - - it "returns false when requesting without a feature" do - expect(described_class).not_to be_show_banners - end - - it "returns false when requesting with a feature that is enabled in the token" do - expect(described_class).not_to be_show_banners(feature: :active_feature) - end - - it "returns false when requesting with a feature that is disabled in the token" do - expect(described_class).not_to be_show_banners(feature: :inactive_feature) - end - end - - context "when token is inactive" do - before { allow(described_class).to receive(:active?).and_return(false) } - - it "returns false when requesting without a feature" do - expect(described_class).not_to be_show_banners - end - - it "returns false when requesting with a feature that is enabled in the token" do - expect(described_class).not_to be_show_banners(feature: :active_feature) - end - - it "returns false when requesting with a feature that is disabled in the token" do - expect(described_class).not_to be_show_banners(feature: :inactive_feature) - end + it "returns false" do + expect(described_class).not_to be_hide_banners end end end @@ -157,39 +126,38 @@ RSpec.describe EnterpriseToken do describe "existing token" do before do - allow_any_instance_of(EnterpriseToken).to receive(:token_object).and_return(object) + allow_any_instance_of(described_class).to receive(:token_object).and_return(object) # rubocop:disable RSpec/AnyInstance subject.save!(validate: false) end context "when inner token is active" do it "has an active token" do - expect(object).to receive(:expired?).and_return(false) - expect(EnterpriseToken.count).to eq(1) - expect(EnterpriseToken.current).to eq(subject) - expect(EnterpriseToken.current.encoded_token).to eq("foo") - expect(EnterpriseToken.show_banners?).to be(false) + allow(object).to receive(:expired?).and_return(false) + expect(described_class.count).to eq(1) + expect(described_class.current).to eq(subject) + expect(described_class.current.encoded_token).to eq("foo") # Deleting it updates the current token - EnterpriseToken.current.destroy! + described_class.current.destroy! - expect(EnterpriseToken.count).to eq(0) - expect(EnterpriseToken.current).to be_nil + expect(described_class.count).to eq(0) + expect(described_class.current).to be_nil end it "delegates to the token object" do allow(object).to receive_messages( subscriber: "foo", mail: "bar", - starts_at: Date.today, - issued_at: Date.today, + starts_at: Time.zone.today, + issued_at: Time.zone.today, expires_at: "never", restrictions: { foo: :bar } ) expect(subject.subscriber).to eq("foo") expect(subject.mail).to eq("bar") - expect(subject.starts_at).to eq(Date.today) - expect(subject.issued_at).to eq(Date.today) + expect(subject.starts_at).to eq(Time.zone.today) + expect(subject.issued_at).to eq(Time.zone.today) expect(subject.expires_at).to eq("never") expect(subject.restrictions).to eq(foo: :bar) end @@ -198,38 +166,40 @@ RSpec.describe EnterpriseToken do let(:service_double) { Authorization::EnterpriseService.new(subject) } before do - expect(Authorization::EnterpriseService) - .to receive(:new).twice.with(subject).and_return(service_double) + allow(Authorization::EnterpriseService) + .to receive(:new) + .with(subject) + .and_return(service_double) end it "forwards to EnterpriseTokenService for checks" do - expect(service_double) + allow(service_double) .to receive(:call) .with(:forbidden_action) - .and_return double("ServiceResult", result: false) - expect(service_double) + .and_return ServiceResult.success(result: false) + allow(service_double) .to receive(:call) .with(:allowed_action) - .and_return double("ServiceResult", result: true) + .and_return ServiceResult.success(result: true) - expect(EnterpriseToken.allows_to?(:forbidden_action)).to be false - expect(EnterpriseToken.allows_to?(:allowed_action)).to be true + expect(described_class.allows_to?(:forbidden_action)).to be false + expect(described_class.allows_to?(:allowed_action)).to be true end end end context "when inner token is expired" do before do - expect(object).to receive(:expired?).and_return(true) + allow(object).to receive(:expired?).and_return(true) end it "has an expired token" do - expect(EnterpriseToken.current).to eq(subject) - expect(EnterpriseToken.show_banners?).to be(true) + expect(described_class.current).to eq(subject) + expect(described_class).not_to be_active end end - context "updating it with an invalid token" do + context "when updating it with an invalid token" do it "fails validations" do subject.encoded_token = "bar" expect(subject.save).to be_falsey @@ -239,22 +209,22 @@ RSpec.describe EnterpriseToken do describe "no token" do it do - expect(EnterpriseToken.current).to be_nil - expect(EnterpriseToken.show_banners?).to be(true) + expect(described_class.current).to be_nil + expect(described_class).not_to be_active end end describe "invalid token" do it "appears as if no token is shown" do - expect(EnterpriseToken.current).to be_nil - expect(EnterpriseToken.show_banners?).to be(true) + expect(described_class.current).to be_nil + expect(described_class).not_to be_active end end - describe "Configuration file has `ee_manager_visible` set to false" do + describe "Configuration file has `ee_hide_banners` set to false" do it "does not show banners promoting EE" do - expect(OpenProject::Configuration).to receive(:ee_manager_visible?).and_return(false) - expect(EnterpriseToken.show_banners?).to be_falsey + allow(OpenProject::Configuration).to receive(:ee_hide_banners?).and_return(false) + expect(described_class).not_to be_hide_banners end end end diff --git a/spec/models/export_settings/export_setting_spec.rb b/spec/models/export_settings/export_setting_spec.rb new file mode 100644 index 00000000000..504767d11a5 --- /dev/null +++ b/spec/models/export_settings/export_setting_spec.rb @@ -0,0 +1,147 @@ +# 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. +#++ + +require "spec_helper" + +RSpec.shared_examples "a successful creation" do |format| + it "creates successfully with format #{format}" do + expect(described_class.create(query:, settings:, format:)).to be_persisted + end +end + +RSpec.describe ExportSetting do + let(:query) { create(:query, name: "some query") } + let(:format) { "csv" } + let(:settings) { { "columns" => %w[id subject], "show_descriptions" => "true" } } + + it "returns its settings with symbol keys" do + export_settings = described_class.create(query:, settings:, format:) + + expect(export_settings.settings).to eq({ columns: %w[id subject], show_descriptions: "true" }) + + export_settings.settings = { "foo" => "bar" } + expect(export_settings.settings).to eq({ foo: "bar" }) + end + + describe "creating" do + formats = %w[csv xls pdf_report pdf_table pdf_gantt] + + formats.each do |format| + include_examples "a successful creation", format + end + + it "fails with an invalid format" do + expect(described_class.create(query:, settings:, format: "invalid_format")).not_to be_persisted + end + + it "fails without format" do + expect(described_class.create(query:, settings:)).not_to be_persisted + end + + it "fails without settings" do + expect(described_class.create(query:, format:)).not_to be_persisted + end + + it "fails without query" do + expect(described_class.create(settings:, format:)).not_to be_persisted + end + + it "fails when there already is this query/format combination" do + expect(described_class.create(query:, settings:, format: "csv")).to be_persisted + + duplicate = described_class.create(query:, settings:, format: "csv") + expect(duplicate).not_to be_persisted + expect(duplicate.errors[:format]).to include("there already is an export setting for this query with this format") + + new_query = create(:query, name: "another query") + expect(described_class.create(query: new_query, settings:, format: "csv")).to be_persisted + end + end + + describe "#true?" do + let(:settings) do + { + string_true: "true", + real_true: true, + string_one_true: "1", + one_true: 1, + string_false: "false", + real_false: false, + string_zero_false: "0", + zero_false: 0, + truthy_string: "any string is truthy", + truthy_array: [] + } + end + let(:instance) { described_class.new(query:, settings:, format:) } + + it "returns true for a value that looks like it should be true" do + true_keys = %i[ + string_true + real_true + string_one_true + one_true + ] + + true_keys.each do |key| + expect(instance.true?(key)).to be true + end + end + + it "returns false for a value that looks like it should be false" do + false_keys = %i[ + string_false + real_false + string_zero_false + zero_false + ] + + false_keys.each do |key| + expect(instance.true?(key)).to be false + end + end + + it "returns false for values that are truthy in Ruby" do + truthy_keys = %i[truthy_string truthy_array] + + truthy_keys.each do |key| + expect(instance.true?(key)).to be false + end + end + + it "returns false for keys that do not exist" do + expect(instance.true?(:non_existent_key)).to be false + end + + it "returns a default value for non existent keys" do + expect(instance.true?(:non_existent_key, default: true)).to be true + end + end +end diff --git a/spec/models/queries/principals/filters/restricted_mentionable_on_work_package_filter_spec.rb b/spec/models/queries/principals/filters/restricted_mentionable_on_work_package_filter_spec.rb new file mode 100644 index 00000000000..98b52ab5c23 --- /dev/null +++ b/spec/models/queries/principals/filters/restricted_mentionable_on_work_package_filter_spec.rb @@ -0,0 +1,128 @@ +# 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. +# ++ + +require "spec_helper" + +RSpec.describe Queries::Principals::Filters::RestrictedMentionableOnWorkPackageFilter do + it_behaves_like "basic query filter" do + include RestrictedVisibilityCommentsHelpers + + let(:class_key) { :restricted_mentionable_on_work_package } + let(:type) { :list_optional } + let(:human_name) { "restricted mentionable" } + + shared_let(:project) { create(:project) } + shared_let(:work_package) { create(:work_package, project:) } + shared_let(:other_work_package) { create(:work_package, project:) } + + shared_let(:user_without_restricted_comments_view_permissions) { create_user_without_restricted_comments_view_permissions } + shared_let(:user_with_restricted_comments_view_permissions) { create_user_with_restricted_comments_view_permissions } + shared_let(:user_with_restricted_comments_view_and_write_permissions) do + create_user_with_restricted_comments_view_and_write_permissions + end + + let(:user) { user_with_restricted_comments_view_permissions } + + before { allow(User).to receive(:current).and_return(user) } + + describe "#validate" do + it "is valid with a single work package id" do + instance.values = [work_package.id.to_s] + expect(instance).to be_valid + end + + it "is invalid with multiple work package ids" do + instance.values = [work_package.id.to_s, other_work_package.id.to_s] + expect(instance).not_to be_valid + expect(instance.errors.messages).to eq(values: ["must be a single work package"]) + end + + it "is invalid with no work package id" do + instance.values = [] + expect(instance).not_to be_valid + expect(instance.errors.messages).to eq(values: ["can't be blank.", "filter has invalid values."]) + end + end + + describe "#scope" do + subject { instance.apply_to(Principal.visible(user)) } + + let(:values) { [work_package.id.to_s] } + + let(:instance) do + described_class.create!.tap do |filter| + filter.values = values + filter.operator = operator + end + end + + context "with an = operator" do + let(:operator) { "=" } + + it "returns all mentionable principals on the work package and its project" do + expect(subject) + .to contain_exactly(user_with_restricted_comments_view_permissions, + user_with_restricted_comments_view_and_write_permissions) + end + + context "with users and groups" do + let(:group_member1) { create(:user) } + let(:group_member2) { create(:user) } + let(:group_role) { create(:project_role, permissions: %i[view_work_packages view_comments_with_restricted_visibility]) } + let(:group) do + create(:group, members: [group_member1, group_member2]) do |group| + Members::CreateService + .new(user: User.system, contract_class: EmptyContract) + .call(project:, principal: group, roles: [group_role]) + end + end + + it "returns all mentionable principals including group and group members" do + expect(subject) + .to contain_exactly(user_with_restricted_comments_view_permissions, + user_with_restricted_comments_view_and_write_permissions, + group, + group_member1, + group_member2) + end + end + end + + context "with a ! operator" do + let(:operator) { "!" } + + it "returns all non-mentionable users on the work package and its project" do + expect(subject) + .to contain_exactly(user_without_restricted_comments_view_permissions) + end + end + end + end +end diff --git a/spec/models/query/query_export_settings_spec.rb b/spec/models/query/query_export_settings_spec.rb new file mode 100644 index 00000000000..01a20f10afc --- /dev/null +++ b/spec/models/query/query_export_settings_spec.rb @@ -0,0 +1,60 @@ +# 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. +#++ + +require "spec_helper" + +RSpec.describe "query export settings" do # rubocop:disable RSpec/DescribeClass + let(:query) { create(:query) } + + describe "export_settings_for" do + it "creates a new export setting for the given format" do + setting = query.export_settings_for("csv") + + expect(setting).to be_a_new_record + end + + context "with an existing export setting" do + let!(:export_setting) { ExportSetting.create(query_id: query.id, settings: { some_key: :some_value }, format: "xls") } + + it "is returned" do + setting = query.export_settings_for("xls") + + expect(setting).to be_persisted + expect(setting.settings).to eq({ some_key: "some_value" }) + end + + it "is not returned for a different format" do + setting = query.export_settings_for("pdf_report") + + expect(setting).to be_a_new_record + end + end + end +end diff --git a/spec/models/types/pattern_resolver_spec.rb b/spec/models/types/pattern_resolver_spec.rb index a1bd36834e6..2a9d5dccfcb 100644 --- a/spec/models/types/pattern_resolver_spec.rb +++ b/spec/models/types/pattern_resolver_spec.rb @@ -46,7 +46,7 @@ RSpec.describe Types::PatternResolver do it "resolves the pattern" do expect(subject.resolve(work_package)) - .to eq("#{work_package.id} | NA | #{work_package.created_at.to_date.iso8601}") + .to eq("#{work_package.id} | N/A | #{work_package.created_at.to_date.iso8601}") end end diff --git a/spec/models/types/patterns/token_property_mapper_spec.rb b/spec/models/types/patterns/token_property_mapper_spec.rb index 8a4c9b8b318..0001ad606c5 100644 --- a/spec/models/types/patterns/token_property_mapper_spec.rb +++ b/spec/models/types/patterns/token_property_mapper_spec.rb @@ -41,12 +41,13 @@ RSpec.describe Types::Patterns::TokenPropertyMapper do shared_let(:work_package_parent) do create(:work_package, project:, category:, start_date: Date.yesterday, estimated_hours: 120, - due_date: 3.months.from_now, assigned_to: parent_assignee) + remaining_hours: 80, due_date: 3.months.from_now, assigned_to: parent_assignee) end shared_let(:work_package) do create(:work_package, responsible:, project:, category:, due_date: 1.month.from_now, assigned_to: assignee, - parent: work_package_parent, start_date: Time.zone.today, estimated_hours: 30) + parent: work_package_parent, start_date: Time.zone.today, estimated_hours: 30, + remaining_hours: 25) end shared_let(:string_custom_field) do @@ -66,6 +67,12 @@ RSpec.describe Types::Patterns::TokenPropertyMapper do end end + shared_let(:not_activated_custom_field) do + create(:boolean_wp_custom_field).tap do |custom_field| + work_package.type.custom_fields << custom_field + end + end + described_class::TOKEN_PROPERTY_MAP.each_pair do |key, details| it "the token named #{key} resolves successfully" do expect { details[:fn].call(work_package) }.not_to raise_error @@ -78,6 +85,12 @@ RSpec.describe Types::Patterns::TokenPropertyMapper do expect(function.call(work_package)).to eq(%w[A B]) end + it "must return nil if custom field is not activated in project" do + function = described_class.new.fetch :"custom_field_#{not_activated_custom_field.id}" + expect { function.call(work_package) }.not_to raise_error + expect(function.call(work_package)).to be_nil + end + it "returns all possible tokens" do cf = string_custom_field tokens = described_class.new.tokens_for_type(work_package.type) diff --git a/spec/models/work_package/work_package_acts_as_journalized_spec.rb b/spec/models/work_package/work_package_acts_as_journalized_spec.rb index 9a211518c20..1ba39f407cc 100644 --- a/spec/models/work_package/work_package_acts_as_journalized_spec.rb +++ b/spec/models/work_package/work_package_acts_as_journalized_spec.rb @@ -903,10 +903,45 @@ RSpec.describe WorkPackage do end context "when comments_with_restricted_visibility is enabled", with_flag: { comments_with_restricted_visibility: true } do - context "when the user cannot see restricted journals" do + context "and setting is enabled for the project" do before do + work_package.project.enabled_comments_with_restricted_visibility = true + work_package.project.save! + end + + context "when the user cannot see restricted journals" do + before do + mock_permissions_for(user) do |mock| + mock.allow_in_work_package :view_work_packages, work_package: + end + end + + it "does not return the restricted journal" do + expect(journals.map(&:id)).not_to include(restricted_note.id) + expect(journals.map(&:id)).to include(unrestricted_note.id) + end + end + + context "when the user can see restricted journals" do + before do + mock_permissions_for(user) do |mock| + mock.allow_in_project(:view_comments_with_restricted_visibility, project: work_package.project) + end + end + + it "returns all journals" do + expect(journals.map(&:id)).to include(restricted_note.id, unrestricted_note.id) + end + end + end + + context "and setting is disabled for the project" do + before do + work_package.project.enabled_comments_with_restricted_visibility = false + work_package.project.save! + mock_permissions_for(user) do |mock| - mock.allow_in_work_package :view_work_packages, work_package: + mock.allow_in_project(:view_comments_with_restricted_visibility, project: work_package.project) end end @@ -915,18 +950,6 @@ RSpec.describe WorkPackage do expect(journals.map(&:id)).to include(unrestricted_note.id) end end - - context "when the user can see restricted journals" do - before do - mock_permissions_for(user) do |mock| - mock.allow_in_project(:view_comments_with_restricted_visibility, project: work_package.project) - end - end - - it "returns all journals" do - expect(journals.map(&:id)).to include(restricted_note.id, unrestricted_note.id) - end - end end context "when comments_with_restricted_visibility is disabled", with_flag: { comments_with_restricted_visibility: false } do diff --git a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb index 723b141eedf..a5e2f5b4e1e 100644 --- a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb +++ b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb @@ -99,6 +99,7 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do let(:image_path) { Rails.root.join("spec/fixtures/files/image.png") } let(:priority) { create(:priority_normal) } let(:image_attachment) { Attachment.new author: user, file: File.open(image_path) } + let(:image_attachment_elsewhere) { Attachment.new author: user, file: File.open(image_path) } let(:attachments) { [image_attachment] } let(:cf_long_text_description) { "**foo** *faa*" } let(:cf_empty_long_text_description) { "" } @@ -137,6 +138,7 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do

    Foo

    + DESCRIPTION end let(:work_package) do @@ -258,6 +260,11 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do images: } end + before do + image_attachment.save + image_attachment_elsewhere.save + end + describe "with a request for a PDF" do describe "with rich text and images" do it "contains correct data" do @@ -274,17 +281,16 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do ].flatten.join(" ") expect(result).to eq(expected_result) expect(result).not_to include("DisabledCustomField") - expect(pdf[:images].length).to eq(2) + expect(pdf[:images].length).to eq(3) end end - describe "with a faulty image" do + describe "with faulty images" do before do # simulate a null pointer exception # https://appsignal.com/openproject-gmbh/sites/62a6d833d2a5e482c1ef825d/exceptions/incidents/2326/samples/62a6d833d2a5e482c1ef825d-848752493603098719217252846401 # where attachment data is in the database but the file is missing, corrupted or not accessible - allow(image_attachment).to receive(:file) - .and_return(nil) + allow_any_instance_of(Attachment).to receive(:file).and_return(nil) # rubocop:disable RSpec/AnyInstance end it "still finishes the export" do diff --git a/spec/permissions/edit_project_life_cycles_spec.rb b/spec/permissions/edit_project_life_cycles_spec.rb index b120ae757eb..ff923074d63 100644 --- a/spec/permissions/edit_project_life_cycles_spec.rb +++ b/spec/permissions/edit_project_life_cycles_spec.rb @@ -34,11 +34,11 @@ RSpec.describe Overviews::OverviewsController, "edit_project_life_cycles permiss include PermissionSpecs # render dialog with inputs for editing project attributes with edit_project permission - check_permission_required_for("overviews/overviews#project_life_cycles_dialog", :edit_project_phases) + check_permission_required_for("overviews/project_phases#edit", :edit_project_phases) # render form with inputs for editing project attributes with edit_project permission - check_permission_required_for("overviews/overviews#project_life_cycles_form", :edit_project_phases) + check_permission_required_for("overviews/project_phases#preview", :edit_project_phases) # update project attributes with edit_project permission, deeper permission check via contract in place - check_permission_required_for("overviews/overviews#update_project_life_cycles", :edit_project_phases) + check_permission_required_for("overviews/project_phases#update", :edit_project_phases) end diff --git a/spec/policies/work_package_policy_spec.rb b/spec/policies/work_package_policy_spec.rb deleted file mode 100644 index abc4a8d4f05..00000000000 --- a/spec/policies/work_package_policy_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -#++ - -require "spec_helper" - -RSpec.describe WorkPackagePolicy, type: :controller do - let(:user) { build_stubbed(:user) } - let(:project) { build_stubbed(:project) } - let(:work_package) { build_stubbed(:work_package, project:) } - - describe "#allowed?" do - let(:subject) { described_class.new(user) } - - context "for edit" do - it "is false if the user has no permissions" do - mock_permissions_for(user, &:forbid_everything) - expect(subject).not_to be_allowed(work_package, :edit) - end - - it "is true if the user has the edit_work_package permission" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :edit_work_packages, project: - end - - expect(subject).to be_allowed(work_package, :edit) - end - - it "is true if the user has the edit_work_package permission on the work packge" do - mock_permissions_for(user) do |mock| - mock.allow_in_work_package :edit_work_packages, work_package: - end - - expect(subject).to be_allowed(work_package, :edit) - end - - it "is false if the user has only the add_work_package_notes permission" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :add_work_package_notes, project: - end - - expect(subject).not_to be_allowed(work_package, :edit) - end - - it "is false if the user has the permissions but the work package is unpersisted" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :edit_work_packages, :add_work_package_notes, project: - end - - allow(work_package).to receive(:persisted?).and_return false - - expect(subject).not_to be_allowed(work_package, :edit) - end - end - - context "for manage_subtasks" do - it "is true if the user has the manage_subtasks permission in the project" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :manage_subtasks, project: - end - - expect(subject).to be_allowed(work_package, :manage_subtasks) - end - end - - context "for comment" do - it "is false if the user lacks permission" do - expect(subject).not_to be_allowed(work_package, :comment) - end - - it "is true if the user has the add_work_package_notes permission" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :add_work_package_notes, project: - end - expect(subject).to be_allowed(work_package, :comment) - end - - it "is true if the user has the add_work_package_notes permission on the work package" do - mock_permissions_for(user) do |mock| - mock.allow_in_work_package :add_work_package_notes, work_package: - end - expect(subject).to be_allowed(work_package, :comment) - end - - it "is true if the user has the edit_work_packages permission" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :edit_work_packages, project: - end - - expect(subject).to be_allowed(work_package, :comment) - end - - it "is false if the user has the edit_work_packages permission but the work_package is unpersisted" do - mock_permissions_for(user) do |mock| - mock.allow_in_project :edit_work_packages, project: - end - allow(work_package).to receive(:persisted?).and_return false - - expect(subject).not_to be_allowed(work_package, :comment) - end - end - end -end diff --git a/spec/requests/admin/attachments/virus_scanning_settings_spec.rb b/spec/requests/admin/attachments/virus_scanning_settings_spec.rb index 354bf3b68ed..c218c170a57 100644 --- a/spec/requests/admin/attachments/virus_scanning_settings_spec.rb +++ b/spec/requests/admin/attachments/virus_scanning_settings_spec.rb @@ -130,9 +130,10 @@ RSpec.describe "Attachments virus scanning", end describe "without ee" do - it "redirects to upsale" do + it "renders upsale" do get "/admin/settings/virus_scanning" - expect(response.body).to have_text "Virus scanning is an Enterprise add-on", normalize_ws: true + expect(response.body).to have_text "Virus scanning" + expect(response.body).to have_text "Available only through the Corporate enterprise plan." end end end diff --git a/spec/requests/api/v3/activities_by_work_package_resource_spec.rb b/spec/requests/api/v3/activities_by_work_package_resource_spec.rb index 3695d192104..d777e6f70c5 100644 --- a/spec/requests/api/v3/activities_by_work_package_resource_spec.rb +++ b/spec/requests/api/v3/activities_by_work_package_resource_spec.rb @@ -80,6 +80,11 @@ RSpec.describe API::V3::Activities::ActivitiesByWorkPackageAPI do # rubocop:disa version: 2) end + before do + project.enabled_comments_with_restricted_visibility = true + project.save! + end + context "and user has the permission to see it" do it "includes restricted activities" do get api_v3_paths.work_package_activities work_package.id diff --git a/spec/routing/project_settings_routing_spec.rb b/spec/routing/project_settings_routing_spec.rb index a2587c92c29..88880864c68 100644 --- a/spec/routing/project_settings_routing_spec.rb +++ b/spec/routing/project_settings_routing_spec.rb @@ -106,5 +106,19 @@ RSpec.describe Projects::SettingsController do controller: "projects/settings/work_packages/types", action: "update", project_id: "123" ) end + + it do + expect(get("/projects/123/settings/work_packages/activities")) + .to route_to( + controller: "projects/settings/work_packages/activities", action: "show", project_id: "123" + ) + end + + it do + expect(patch("/projects/123/settings/work_packages/activities")) + .to route_to( + controller: "projects/settings/work_packages/activities", action: "update", project_id: "123" + ) + end end end diff --git a/spec/seeders/root_seeder_standard_edition_spec.rb b/spec/seeders/root_seeder_standard_edition_spec.rb index efdf57f0bce..8eec7722960 100644 --- a/spec/seeders/root_seeder_standard_edition_spec.rb +++ b/spec/seeders/root_seeder_standard_edition_spec.rb @@ -92,16 +92,18 @@ RSpec.describe RootSeeder, expect(RecurringMeeting.count).to eq 1 # The template is created. - expect(StructuredMeeting.where(template: true).count).to eq 1 - expect(StructuredMeeting.where(template: true).first.duration).to eq 1.0 - expect(StructuredMeeting.where(template: true).first.agenda_items.count).to eq 9 - expect(StructuredMeeting.where(template: true).first.agenda_items.sum(:duration_in_minutes)).to eq 60 + expect(Meeting.templated.count).to eq 1 + template = Meeting.templated.first + expect(template.duration).to eq 1.0 + expect(template.agenda_items.count).to eq 9 + expect(template.agenda_items.sum(:duration_in_minutes)).to eq 60 # The first instance from that template is also created with the same data. - expect(StructuredMeeting.where(template: false).count).to eq 1 - expect(StructuredMeeting.where(template: false).first.duration).to eq 1.0 - expect(StructuredMeeting.where(template: false).first.agenda_items.count).to eq 9 - expect(StructuredMeeting.where(template: false).first.agenda_items.sum(:duration_in_minutes)).to eq 60 + expect(Meeting.where(template: false).count).to eq 1 + instance = Meeting.not_templated.first + expect(instance.duration).to eq 1.0 + expect(instance.agenda_items.count).to eq 9 + expect(instance.agenda_items.sum(:duration_in_minutes)).to eq 60 end it "creates different types of queries" do diff --git a/spec/services/notifications/create_from_model_service_work_package_spec.rb b/spec/services/notifications/create_from_model_service_work_package_spec.rb index 483ec35e607..ee3e6ddaac0 100644 --- a/spec/services/notifications/create_from_model_service_work_package_spec.rb +++ b/spec/services/notifications/create_from_model_service_work_package_spec.rb @@ -38,6 +38,7 @@ RSpec.describe Notifications::CreateFromModelService, include_context "with CreateFromJournalJob context" let(:permissions) { [:view_work_packages] } + let(:admin) { create(:admin) } let(:author) { user_property == :author ? recipient : other_user } let(:user_property) { nil } let(:work_package) do @@ -51,7 +52,7 @@ RSpec.describe Notifications::CreateFromModelService, if %i[responsible assigned_to].include?(user_property) create(:work_package, - **wp_attributes.merge(user_property => recipient)) + **wp_attributes, user_property => recipient) elsif user_property == :watcher create(:work_package, **wp_attributes) do |wp| @@ -100,6 +101,11 @@ RSpec.describe Notifications::CreateFromModelService, work_package.save(validate: false) work_package.journals.last end + let(:journal_2_with_internal_comment) do + work_package.add_journal(user: admin, notes: "need to know basis", restricted: true) + work_package.save(validate: false) + work_package.journals.last + end before do # make sure no other calls are made due to WP creation/update @@ -112,7 +118,7 @@ RSpec.describe Notifications::CreateFromModelService, let(:user_property) { :assigned_to } let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(assignee: true)) + build(:notification_setting, **notification_settings_all_false, assignee: true) ] end @@ -159,7 +165,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when assignee has all in app notifications enabled but only assignee for mail" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(assignee: true)) + build(:notification_setting, **notification_settings_all_false, assignee: true) ] end @@ -197,13 +203,37 @@ RSpec.describe Notifications::CreateFromModelService, it_behaves_like "creates no notification" end + + context "when assignee does not have access to restricted journal" do + let(:author) { admin } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates no notification" + end + + context "when assignee has access to restricted journal" do + let(:author) { admin } + let(:permissions) { %i[view_work_packages view_comments_with_restricted_visibility] } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates notification" do + let(:notification_channel_reasons) do + { + read_ian: false, + reason: :assigned, + mail_alert_sent: nil, + mail_reminder_sent: false + } + end + end + end end context "when user is responsible" do let(:user_property) { :responsible } let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(responsible: true)) + build(:notification_setting, **notification_settings_all_false, responsible: true) ] end @@ -250,6 +280,30 @@ RSpec.describe Notifications::CreateFromModelService, it_behaves_like "creates no notification" end + + context "when responsible does not have access to restricted journal" do + let(:author) { admin } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates no notification" + end + + context "when responsible has access to restricted journal" do + let(:author) { admin } + let(:permissions) { %i[view_work_packages view_comments_with_restricted_visibility] } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates notification" do + let(:notification_channel_reasons) do + { + read_ian: false, + reason: :responsible, + mail_alert_sent: nil, + mail_reminder_sent: false + } + end + end + end end context "when user is watcher" do @@ -274,7 +328,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when watcher has in app notifications disabled" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(watched: true)) + build(:notification_setting, **notification_settings_all_false, watched: true) ] end @@ -316,6 +370,30 @@ RSpec.describe Notifications::CreateFromModelService, it_behaves_like "creates no notification" end + + context "when watcher does not have access to restricted journal" do + let(:author) { admin } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates no notification" + end + + context "when watcher has access to restricted journal" do + let(:author) { admin } + let(:permissions) { %i[view_work_packages view_comments_with_restricted_visibility] } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates notification" do + let(:notification_channel_reasons) do + { + read_ian: false, + reason: :watched, + mail_alert_sent: nil, + mail_reminder_sent: false + } + end + end + end end context "when user is notified about everything" do @@ -414,13 +492,37 @@ RSpec.describe Notifications::CreateFromModelService, it_behaves_like "creates no notification" end + + context "when recipient does not have access to restricted journal" do + let(:author) { admin } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates no notification" + end + + context "when recipient has access to restricted journal" do + let(:author) { admin } + let(:permissions) { %i[view_work_packages view_comments_with_restricted_visibility] } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates notification" do + let(:notification_channel_reasons) do + { + read_ian: false, + reason: :commented, + mail_alert_sent: nil, + mail_reminder_sent: false + } + end + end + end end context "when the work package is shared with the user" do let(:user_property) { :shared } let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(shared: true)) + build(:notification_setting, **notification_settings_all_false, shared: true) ] end @@ -461,14 +563,37 @@ RSpec.describe Notifications::CreateFromModelService, it_behaves_like "creates no notification" end + + context "when the shared user does not have access to restricted journal" do + let(:author) { admin } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates no notification" + end + + context "when the shared user has access to restricted journal" do + let(:author) { admin } + let(:permissions) { %i[view_work_packages view_comments_with_restricted_visibility] } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates notification" do + let(:notification_channel_reasons) do + { + read_ian: false, + reason: :shared, + mail_alert_sent: nil, + mail_reminder_sent: false + } + end + end + end end context "when a work package is created" do context "when the user configured to be notified on work package creation" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_created: true)) + build(:notification_setting, **notification_settings_all_false, work_package_created: true) ] end @@ -487,8 +612,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user configured to be notified on work package status changes" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_processed: true)) + build(:notification_setting, **notification_settings_all_false, work_package_processed: true) ] end @@ -498,8 +622,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user configured to be notified on work package priority changes" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_prioritized: true)) + build(:notification_setting, **notification_settings_all_false, work_package_prioritized: true) ] end @@ -523,8 +646,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user has commented notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_commented: true)) + build(:notification_setting, **notification_settings_all_false, work_package_commented: true) ] end @@ -549,6 +671,30 @@ RSpec.describe Notifications::CreateFromModelService, it_behaves_like "creates no notification" end + + context "when the user does not have access to restricted journal" do + let(:author) { admin } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates no notification" + end + + context "when the user has access to restricted journal" do + let(:author) { admin } + let(:permissions) { %i[view_work_packages view_comments_with_restricted_visibility] } + let(:journal) { journal_2_with_internal_comment } + + it_behaves_like "creates notification" do + let(:notification_channel_reasons) do + { + read_ian: false, + reason: :commented, + mail_alert_sent: nil, + mail_reminder_sent: false + } + end + end + end end context "when the journal has no note" do @@ -557,8 +703,7 @@ RSpec.describe Notifications::CreateFromModelService, context "with the user having commented notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_commented: true)) + build(:notification_setting, **notification_settings_all_false, work_package_commented: true) ] end @@ -572,8 +717,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user has processed notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_processed: true)) + build(:notification_setting, **notification_settings_all_false, work_package_processed: true) ] end @@ -606,8 +750,7 @@ RSpec.describe Notifications::CreateFromModelService, context "with the user having processed notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_processed: true)) + build(:notification_setting, **notification_settings_all_false, work_package_processed: true) ] end @@ -621,8 +764,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user has prioritized notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_prioritized: true)) + build(:notification_setting, **notification_settings_all_false, work_package_prioritized: true) ] end @@ -655,8 +797,7 @@ RSpec.describe Notifications::CreateFromModelService, context "with the user having prioritized notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_prioritized: true)) + build(:notification_setting, **notification_settings_all_false, work_package_prioritized: true) ] end @@ -670,8 +811,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user has scheduled notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_scheduled: true)) + build(:notification_setting, **notification_settings_all_false, work_package_scheduled: true) ] end @@ -704,8 +844,7 @@ RSpec.describe Notifications::CreateFromModelService, context "with the user having scheduled notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_scheduled: true)) + build(:notification_setting, **notification_settings_all_false, work_package_scheduled: true) ] end @@ -719,8 +858,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user has scheduled notifications activated" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false - .merge(work_package_scheduled: true)) + build(:notification_setting, **notification_settings_all_false, work_package_scheduled: true) ] end @@ -772,7 +910,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when user is mentioned" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(mentioned: true)) + build(:notification_setting, **notification_settings_all_false, mentioned: true) ] end @@ -794,7 +932,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the user disabled mention notifications" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(mentioned: false)) + build(:notification_setting, **notification_settings_all_false, mentioned: false) ] end @@ -965,7 +1103,7 @@ RSpec.describe Notifications::CreateFromModelService, context "when the recipient turned off mention notifications" do let(:recipient_notification_settings) do [ - build(:notification_setting, **notification_settings_all_false.merge(mentioned: false)) + build(:notification_setting, **notification_settings_all_false, mentioned: false) ] end diff --git a/spec/services/principals/replace_references_service_call_integration_spec.rb b/spec/services/principals/replace_references_service_call_integration_spec.rb index 5daffad06e5..612e667b35b 100644 --- a/spec/services/principals/replace_references_service_call_integration_spec.rb +++ b/spec/services/principals/replace_references_service_call_integration_spec.rb @@ -195,28 +195,6 @@ RSpec.describe Principals::ReplaceReferencesService, "#call", type: :model do end end - context "with MeetingContent" do - it_behaves_like "rewritten record", - :meeting_agenda, - :author_id do - let(:attributes) do - { type: "'MeetingAgenda'" } - end - end - - it_behaves_like "rewritten record", - :meeting_minutes, - :author_id do - let(:attributes) do - { type: "'MeetingMinutes'" } - end - end - - it_behaves_like "rewritten record", - :journal_meeting_content_journal, - :author_id - end - context "with MeetingParticipant" do it_behaves_like "rewritten record", :meeting_participant, diff --git a/spec/services/users/register_user_service_spec.rb b/spec/services/users/register_user_service_spec.rb index 9a8cfa2f145..24ef0a7d179 100644 --- a/spec/services/users/register_user_service_spec.rb +++ b/spec/services/users/register_user_service_spec.rb @@ -27,7 +27,7 @@ require "spec_helper" -RSpec.describe Users::RegisterUserService do +RSpec.describe Users::RegisterUserService, with_ee: %i[sso_auth_providers] do let(:user) { build(:user) } let(:instance) { described_class.new(user) } let(:call) { instance.call } @@ -83,9 +83,6 @@ RSpec.describe Users::RegisterUserService do before do allow(user).to receive(:activate) allow(user).to receive(:save).and_return true - - # required so that the azure provider is visible (ee feature) - allow(EnterpriseToken).to receive(:show_banners?).and_return false end it "tries to activate that user regardless of settings" do diff --git a/spec/services/users/replace_mentions_service_integration_spec.rb b/spec/services/users/replace_mentions_service_integration_spec.rb index 6256fb25a41..83a5c2e538a 100644 --- a/spec/services/users/replace_mentions_service_integration_spec.rb +++ b/spec/services/users/replace_mentions_service_integration_spec.rb @@ -404,14 +404,6 @@ RSpec.describe Users::ReplaceMentionsService, "integration" do it_behaves_like "rewritten mention", :document, :description end - context "for meeting_contents text" do - it_behaves_like "rewritten mention", :meeting_agenda, :text - end - - context "for meeting_content_journals text" do - it_behaves_like "rewritten mention", :journal_meeting_content_journal, :text - end - context "for messages content" do it_behaves_like "rewritten mention", :message, :content end diff --git a/spec/services/work_packages/activities_tab/restricted_mentions_sanitizer_spec.rb b/spec/services/work_packages/activities_tab/restricted_mentions_sanitizer_spec.rb new file mode 100644 index 00000000000..cbf6473d41d --- /dev/null +++ b/spec/services/work_packages/activities_tab/restricted_mentions_sanitizer_spec.rb @@ -0,0 +1,100 @@ +# 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. +#++ + +require "spec_helper" + +RSpec.describe WorkPackages::ActivitiesTab::RestrictedMentionsSanitizer do + include RestrictedVisibilityCommentsHelpers + + shared_let(:project) { create(:project) } + shared_let(:admin_but_non_member) { create(:admin) } + shared_let(:viewer) { create_user_without_restricted_comments_view_permissions } + shared_let(:user_with_restricted_comments_view_and_write_permissions) do + create_user_with_restricted_comments_view_and_write_permissions + end + shared_let(:project_admin) { create_user_as_project_admin } + shared_let(:work_package) { create(:work_package, project:) } + + let(:input) do + <<~HTML + @#{admin_but_non_member.firstname} wrote: + + > Well Done! + + @#{viewer.firstname} wrote: + + > @#{user_with_restricted_comments_view_and_write_permissions.firstname} wrote: + > + > > @#{project_admin.firstname} wrote: + > > + > > > FooBar + > > + > > @Firstname wrote: + > > + > > > BooBar + HTML + end + + let(:expected_output) do + <<~HTML + @#{admin_but_non_member.firstname} wrote: + + > Well Done! + + @#{viewer.firstname} wrote: + + > @#{user_with_restricted_comments_view_and_write_permissions.firstname} wrote: + > + > > @#{project_admin.firstname} wrote: + > > + > > > FooBar + > > + > > @Firstname wrote: + > > + > > > BooBar + HTML + end + + subject { described_class.new(work_package, input).call } + + before { allow(User).to receive(:current).and_return(user_with_restricted_comments_view_and_write_permissions) } + + it "sanitizes the notes" do + expect(subject).to eq(expected_output) + end + + context "when the notes are empty" do + let(:input) { "" } + + it "returns an empty string" do + expect(subject).to eq("") + end + end +end diff --git a/spec/support/components/admin/type_configuration_form.rb b/spec/support/components/admin/type_configuration_form.rb index 15ac948bb4e..0ccceec8fe6 100644 --- a/spec/support/components/admin/type_configuration_form.rb +++ b/spec/support/components/admin/type_configuration_form.rb @@ -166,11 +166,16 @@ module Components end def edit_query_group(name) - SeleniumHubWaiter.wait unless using_cuprite? + if using_cuprite? + wait_for_reload + else + SeleniumHubWaiter.wait + end group = find_group(name) group.find(".type-form-query-group--edit-button").click - wait_for_reload if using_cuprite? + # Wait for the modal to appear. + expect(page).to have_css(".wp-table--configuration-modal") end def add_attribute_group(name, expect: true) diff --git a/spec/support/components/common/filters.rb b/spec/support/components/common/filters.rb index dd48c47b44b..f218f3585fa 100644 --- a/spec/support/components/common/filters.rb +++ b/spec/support/components/common/filters.rb @@ -39,11 +39,16 @@ module Components expect(page).to have_css(".op-filters-form", visible: :hidden) end - def expect_filter_set(filter_name) + def expect_filter_set(filter_name, value: nil) if filter_name == "name_and_identifier" expect(page.find_by_id(filter_name).value).not_to be_empty + elsif value + within("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :hidden) do + expect(page).to have_css(".advanced-filters--filter-value", text: value, visible: :all) + end else - expect(page).to have_css("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :hidden) + expect(page) + .to have_css("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :hidden) end end @@ -159,7 +164,7 @@ module Components end when "between" if send_keys - find_field("from_value").send_keysvalues.first + find_field("from_value").send_keys values.first find_field("to_value").send_keys values.second else fill_in "from_value", with: values.first @@ -217,6 +222,10 @@ module Components filter[:"data-filter-type"] == "date" end + def date_time_filter?(filter) + filter[:"data-filter-type"] == "datetime_past" + end + def boolean_filter?(_filter) false end diff --git a/spec/support/components/projects/project_life_cycles/edit_dialog.rb b/spec/support/components/projects/project_life_cycles/edit_dialog.rb index 17db68ba13b..3fd8484254e 100644 --- a/spec/support/components/projects/project_life_cycles/edit_dialog.rb +++ b/spec/support/components/projects/project_life_cycles/edit_dialog.rb @@ -49,17 +49,18 @@ module Components close if close_after_yield end - def clear_date_for(step) - find("input[id^='project_available_phases_attributes_#{step.position - 1}']").set "" + def clear_date + find("input[id^='project_phase_date_range']").set "" + find_by_id("edit-project-life-cycles-dialog-title").click end - def set_date_for(step, values:) - dialog_selector = "##{::ProjectLifeCycles::Sections::EditDialogComponent::DIALOG_ID}" + def set_date_for(values:) + dialog_selector = "##{Overviews::ProjectPhases::EditDialogComponent::DIALOG_ID}" datepicker = Components::RangeDatepicker.new(dialog_selector) datepicker.open( - "input[id^='project_available_phases_attributes_#{step.position - 1}']" + "input[id^='project_phase_date_range']" ) values.each do |date| @@ -98,12 +99,12 @@ module Components expect(page).to have_css(async_content_container_css_selector) end - def expect_input(label, value:, position:) + def expect_input(label, value:) within_async_content do expect(page).to have_field( label, with: value, - name: "project[available_phases_attributes][#{position - 1}][date_range]" + name: "project_phase[date_range]" ) end end @@ -111,32 +112,32 @@ module Components def expect_input_for(step) value = "#{step.start_date.strftime('%Y-%m-%d')} - #{step.finish_date.strftime('%Y-%m-%d')}" - expect_input(step.name, value:, position: step.position) + expect_input(step.name, value:) end - def expect_caption(step, text: nil, present: true) + def expect_caption(text: nil, present: true) selector = 'span[id^="caption"]' - expect_selector_for(step, selector:, text:, present:) + expect_selector_for(selector:, text:, present:) end - def expect_no_caption(step) - expect_caption(step, present: false) + def expect_no_caption + expect_caption(present: false) end - def expect_validation_message(step, text: nil, present: true) + def expect_validation_message(text: nil, present: true) selector = 'div[id^="validation"]' - expect_selector_for(step, selector:, text:, present:) + expect_selector_for(selector:, text:, present:) end - def expect_no_validation_message(step) - expect_validation_message(step, present: false) + def expect_no_validation_message + expect_validation_message(present: false) end private - def expect_selector_for(step, selector:, text: nil, present: true) + def expect_selector_for(selector:, text: nil, present: true) within_async_content do - input_id = "#project_available_phases_attributes_#{step.position - 1}_date_range" + input_id = "#project_phase_date_range" parent = find(input_id).ancestor("primer-datepicker-field") if present diff --git a/spec/support/components/work_packages/activities.rb b/spec/support/components/work_packages/activities.rb index 57f49354219..7c050740762 100644 --- a/spec/support/components/work_packages/activities.rb +++ b/spec/support/components/work_packages/activities.rb @@ -183,9 +183,14 @@ module Components page.find_test_selector("op-open-work-package-journal-form-trigger").click end + def refocus_editor + ckeditor.refocus + expect_focus_on_editor + end + def expect_focus_on_editor page.within_test_selector("op-work-package-journal-form-element") do - expect(page).to have_css(".ck-content:focus") + expect(page).to have_css(".ck-content:focus", wait: 10) end end @@ -201,7 +206,11 @@ module Components end def type_comment(text) - open_new_comment_editor if page.find_test_selector("op-open-work-package-journal-form-trigger") + begin + open_new_comment_editor if page.find_test_selector("op-open-work-package-journal-form-trigger") + rescue Capybara::ElementNotFound + # If the editor is already open, we don't need to open it again + end # Wait for the editor form to be present and ready wait_for { page }.to have_test_selector("op-work-package-journal-form-element") @@ -242,10 +251,7 @@ module Components page.within_test_selector("op-work-package-journal-form-element") do get_editor_form_field_element.set_value(text) - if restricted - expect(page).to have_test_selector("op-work-package-journal-restricted-comment-checkbox") - page.check("Restrict visibility") - end + check_restricted_visibility_comment_checkbox if restricted page.find_test_selector("op-submit-work-package-journal-form").click if save end @@ -300,6 +306,11 @@ module Components expect(page).to have_test_selector("op-work-package-journal-form-element") end + def check_restricted_visibility_comment_checkbox + expect(page).to have_test_selector("op-work-package-journal-restricted-comment-checkbox") + page.check("Restrict visibility") + end + def dismiss_comment_editor_with_esc page.find_test_selector("op-work-package-journal-form-element").send_keys(:escape) end diff --git a/spec/support/components/work_packages/relations.rb b/spec/support/components/work_packages/relations.rb index 03f305c70f1..0c6cbc1e215 100644 --- a/spec/support/components/work_packages/relations.rb +++ b/spec/support/components/work_packages/relations.rb @@ -56,11 +56,21 @@ module Components end end + def expect_tab_is_loaded + # Search the current window in order to avoid within scope restrictions + within_window(page.current_window) do + within("wp-relations-tab") do + expect(page).to have_no_css("op-content-loader") + end + end + end + def expect_add_relation_button expect(page).to have_test_selector("new-relation-action-menu") end def expect_no_add_relation_button + expect_tab_is_loaded # Make sure the tab is loaded before checking non existing elements expect(page).not_to have_test_selector("new-relation-action-menu") end @@ -83,6 +93,7 @@ module Components end def expect_no_row(relatable) + expect_tab_is_loaded # Make sure the tab is loaded before checking non existing elements actual_relatable = find_relatable(relatable) expect(page).not_to have_test_selector("op-relation-row-visible-#{actual_relatable.id}"), "expected no relation row for work package " \ @@ -282,6 +293,10 @@ module Components expect_no_row(relatable) end + def expect_no_relations + expect(page).to have_test_selector("no-relations-blankslate", text: "This work package does not have any relations yet.") + end + def add_parent(query, work_package) # Open the parent edit SeleniumHubWaiter.wait @@ -303,6 +318,7 @@ module Components end def expect_no_parent + expect_tab_is_loaded # Make sure the tab is loaded before checking non existing elements expect(page).not_to have_test_selector "op-wp-breadcrumb-parent", wait: 10 end diff --git a/spec/support/finders/test_selector.rb b/spec/support/finders/test_selector_finders.rb similarity index 89% rename from spec/support/finders/test_selector.rb rename to spec/support/finders/test_selector_finders.rb index 775ce19151c..8aed726042e 100644 --- a/spec/support/finders/test_selector.rb +++ b/spec/support/finders/test_selector_finders.rb @@ -33,11 +33,13 @@ module TestSelectorFinders end def find_test_selector(value, **) - find(:test_id, value, **) + target = respond_to?(:page) ? page : self + target.find(:test_id, value, **) end def within_test_selector(value, **, &) - within(:test_id, value, **, &) + target = respond_to?(:page) ? page : self + target.within(:test_id, value, **, &) end # expect(page).to have_test_selector('foo') @@ -57,4 +59,5 @@ RSpec.configure do |config| Capybara::Session.include(TestSelectorFinders) Capybara::DSL.extend(TestSelectorFinders) config.include TestSelectorFinders, type: :feature + config.include TestSelectorFinders, type: :component end diff --git a/spec/support/matchers/has_enterprise_banner.rb b/spec/support/matchers/has_enterprise_banner.rb index c2bcb78ed2c..fc0ef4fc8f2 100644 --- a/spec/support/matchers/has_enterprise_banner.rb +++ b/spec/support/matchers/has_enterprise_banner.rb @@ -32,7 +32,12 @@ RSpec::Matchers.define :have_enterprise_banner do |**args| include TestSelectorFinders match do |page| - page.has_selector?(test_selector("op-enterprise-banner"), **args) + if args[:plan] + expected_text = I18n.t("ee.upsale.plan_text_html", plan: args[:plan].capitalize) + page.find(test_selector("op-enterprise-banner"), **args, text: expected_text) + else + page.find(test_selector("op-enterprise-banner"), **args) + end end match_when_negated do |page| @@ -40,10 +45,16 @@ RSpec::Matchers.define :have_enterprise_banner do |**args| end failure_message do - "expected page to have Enterprise edition banner" + <<~MESSAGE + Expected page to have Enterprise edition banner, but it does not: + #{@error} + MESSAGE end failure_message_when_negated do - "expected page not to have Enterprise edition banner" + <<~MESSAGE + Expected page not to have Enterprise edition banner, but it does: + #{@error} + MESSAGE end end diff --git a/modules/meeting/spec/models/meeting_minutes_spec.rb b/spec/support/matchers/has_enterprise_upsale_page.rb similarity index 61% rename from modules/meeting/spec/models/meeting_minutes_spec.rb rename to spec/support/matchers/has_enterprise_upsale_page.rb index 500657d1bf8..7d27f2bc601 100644 --- a/modules/meeting/spec/models/meeting_minutes_spec.rb +++ b/spec/support/matchers/has_enterprise_upsale_page.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -27,40 +28,33 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require_relative "../spec_helper" +RSpec::Matchers.define :have_enterprise_upsale_page do |**args| + include TestSelectorFinders -RSpec.describe "MeetingMinutes" do - before do - @min = build(:meeting_minutes) + match do |page| + if args[:plan] + expected_text = I18n.t("ee.upsale.plan_text_html", plan: args[:plan].capitalize) + page.find(test_selector("op-enterprise-upsale-page"), **args, text: expected_text) + else + page.find(test_selector("op-enterprise-upsale-page"), **args) + end end - # meeting minutes are editable when the meeting agenda is locked - describe "#editable?" do - before do - @mee = build(:meeting) - @min.meeting = @mee - end + match_when_negated do |page| + page.has_no_selector?(test_selector("op-enterprise-upsale-page"), **args) + end - describe "with no agenda present" do - it "is not editable" do - expect(@min.editable?).to be_falsey - end - end + failure_message do + <<~MESSAGE + Expected page to have Enterprise edition upsale page, but it does not: + #{@error} + MESSAGE + end - describe "with an agenda present" do - before do - @a = build(:meeting_agenda) - @mee.agenda = @a - end - - it "is not editable when the agenda is open" do - expect(@min.editable?).to be_falsey - end - - it "is editable when the agenda is closed" do - @a.lock! - expect(@min.editable?).to be_truthy - end - end + failure_message_when_negated do + <<~MESSAGE + Expected page not to have Enterprise edition upsale page, but it does: + #{@error} + MESSAGE end end diff --git a/spec/support/pages/projects/index.rb b/spec/support/pages/projects/index.rb index 6408756d4a2..010116f6945 100644 --- a/spec/support/pages/projects/index.rb +++ b/spec/support/pages/projects/index.rb @@ -158,26 +158,6 @@ module Pages end end - def expect_filters_container_toggled - expect(page).to have_css(".op-filters-form") - end - - def expect_filters_container_hidden - expect(page).to have_css(".op-filters-form", visible: :hidden) - end - - def expect_filter_set(filter_name) - if filter_name == "name_and_identifier" - expect(page.find_by_id(filter_name).value).not_to be_empty - else - expect(page).to have_css("li[data-filter-name='#{filter_name}']:not(.hidden)", visible: :hidden) - end - end - - def expect_filter_count(count) - expect(page).to have_css('[data-test-selector="filters-button-counter"]', text: count) - end - def expect_filter_available(filter_name) expect(page).to have_select("add_filter_select", with_options: [filter_name]) end @@ -256,25 +236,12 @@ module Pages wait_for_reload end - def set_filter(name, human_name, human_operator = nil, values = [], send_keys: false) - if name == "name_and_identifier" - set_simple_filter(name, values, send_keys:) - else - set_advanced_filter(name, human_name, human_operator, values, send_keys:) - end - end - - def set_simple_filter(_name, values, send_keys: false) - return unless values.any? - - set_name_and_identifier_filter(values, send_keys:) # This is the only one simple filter at the moment. - end - def set_advanced_filter(name, human_name, human_operator = nil, values = [], send_keys: false) selected_filter = select_filter(name, human_name) - apply_operator(name, human_operator) within(selected_filter) do + apply_operator(name, human_operator) + return unless values.any? if boolean_filter?(name) @@ -285,126 +252,11 @@ module Pages elsif date_filter?(selected_filter) || date_time_filter?(selected_filter) select(human_operator, from: "operator") wait_for_network_idle - set_date_filter(human_operator, values, send_keys:) + set_created_at_filter(human_operator, values, send_keys:) end end end - def expect_autocomplete_options_for(custom_field, options, grouping: nil, results_selector: "body") - selected_filter = select_filter(custom_field.column_name, custom_field.name) - - within(selected_filter) do - find('[data-filter-autocomplete="true"]').click - end - - Array(options).each do |option| - expect_ng_option(selected_filter, option, grouping:, results_selector:) - end - end - - def expect_user_autocomplete_options_for(custom_field, expected_options) - selected_filter = select_filter(custom_field.column_name, custom_field.name) - - within(selected_filter) do - find('[data-filter-autocomplete="true"]').click - end - options = visible_user_auto_completer_options - - expect(options).to eq(expected_options) - end - - def apply_operator(name, human_operator) - select(human_operator, from: "operator") unless boolean_filter?(name) - end - - def select_filter(name, human_name) - select human_name, from: "add_filter_select" - page.find("li[data-filter-name='#{name}']") - end - - def remove_filter(name) - if name == "name_and_identifier" - page.find_by_id("name_and_identifier").find(:xpath, "following-sibling::button").click - else - page.find("li[data-filter-name='#{name}'] .filter_rem").click - end - end - - def set_toggle_filter(values) - should_active = values.first == "yes" - is_active = page.has_selector? '[data-test-selector="spot-switch-handle"][data-qa-active]' - - if should_active != is_active - page.find('[data-test-selector="spot-switch-handle"]').click - end - - if should_active - expect(page).to have_css('[data-test-selector="spot-switch-handle"][data-qa-active]') - else - expect(page).to have_css('[data-test-selector="spot-switch-handle"]:not([data-qa-active])') - end - end - - def set_name_and_identifier_filter(values, send_keys: false) - if send_keys - find_field("name_and_identifier").send_keys values.first - else - fill_in "name_and_identifier", with: values.first - end - end - - def set_date_filter(human_operator, values, send_keys: false) - case human_operator - when "on", "less than days ago", "more than days ago", "days ago" - if send_keys - find_field("value").send_keys values.first - else - fill_in "value", with: values.first - end - when "between" - if send_keys - find_field("from_value").send_keys values.first - find_field("to_value").send_keys values.second - else - fill_in "from_value", with: values.first - fill_in "to_value", with: values.second - end - end - end - - def set_autocomplete_filter(values, clear: true) - element = find('[data-filter-autocomplete="true"]') - - ng_select_clear(element, raise_on_missing: false) if clear - - Array(values).each do |query| - select_autocomplete element, - query:, - results_selector: "body" - end - end - - def set_list_filter(values) - value_select = find('.single-select select[name="value"]') - value_select.select values.first - end - - def open_filters - retry_block do - toggle_filters_section - expect(page).to have_css(".op-filters-form.-expanded") - page.find_field("Add filter", visible: true) - end - end - - def filters_toggle - page.find('[data-test-selector="filter-component-toggle"]') - end - - def toggle_filters_section - filters_toggle.click - end - def set_columns(*columns) open_configure_view @@ -649,7 +501,7 @@ module Pages end def project_in_first_row(column_text_separator: "\n") - first_row = within("#projects-table") { find(".op-project-row-component", match: :first) } + first_row = within("#projects-table") { first(".op-project-row-component") } Project.find_by!(name: first_row.text.split(column_text_separator).first) end @@ -671,18 +523,6 @@ module Pages %w[active member_of favored public templated].include?(filter.to_s) end - def autocomplete_filter?(filter) - filter.has_css?('[data-filter-autocomplete="true"]', wait: 0) - end - - def date_filter?(filter) - filter[:"data-filter-type"] == "date" - end - - def date_time_filter?(filter) - filter[:"data-filter-type"] == "datetime_past" - end - def submenu Components::Submenu.new end diff --git a/modules/meeting/app/services/meeting_contents/update_service.rb b/spec/support/pages/projects/settings/activities.rb similarity index 74% rename from modules/meeting/app/services/meeting_contents/update_service.rb rename to spec/support/pages/projects/settings/activities.rb index 0436452db9f..b8c70b9d1cc 100644 --- a/modules/meeting/app/services/meeting_contents/update_service.rb +++ b/spec/support/pages/projects/settings/activities.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -#-- copyright + +# -- copyright # OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH +# Copyright (C) 2010-2024 the OpenProject GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. @@ -25,22 +26,26 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -#++ +# ++ -module MeetingContents - class UpdateService < ::BaseServices::Update - include Attachments::ReplaceAttachments +require "support/pages/page" - def persist(call) - content = call.result +module Pages + module Projects + module Settings + class Activities < Pages::Page + attr_accessor :project - if content.lock_version_changed? - call.errors.add(:base, I18n.t(:notice_locking_conflict)) - call.success = false - return call + def initialize(project) + super() + + @project = project + end + + def path + "/projects/#{project.identifier}/settings/work_packages/activities" + end end - - super end end end diff --git a/spec/support/pages/projects/show.rb b/spec/support/pages/projects/show.rb index 708d1525b9c..1d52305260f 100644 --- a/spec/support/pages/projects/show.rb +++ b/spec/support/pages/projects/show.rb @@ -85,9 +85,9 @@ module Pages wait_for_size_animation_completion("[data-test-selector='async-dialog-content']") end - def open_edit_dialog_for_life_cycles + def open_edit_dialog_for_life_cycle(life_cycle) within_life_cycles_sidebar do - page.find("[data-test-selector='project-life-cycles-edit-button']").click + page.find("[data-test-selector='project-life-cycle-edit-button-#{life_cycle.id}']").click end Components::Projects::ProjectLifeCycles::EditDialog.new.tap(&:expect_open) diff --git a/spec/support/shared/with_ee.rb b/spec/support/shared/with_ee.rb index d754d85ea75..4c4f900e454 100644 --- a/spec/support/shared/with_ee.rb +++ b/spec/support/shared/with_ee.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + #-- copyright # OpenProject is an open source project management software. # Copyright (C) the OpenProject GmbH @@ -48,13 +50,22 @@ RSpec.configure do |config| if allowed.present? allowed = aggregate_parent_array(example, allowed.to_set) + token_double = instance_double(EnterpriseToken) + token_object_double = instance_double(OpenProject::Token) allow(EnterpriseToken).to receive(:allows_to?).and_call_original + allow(token_object_double).to receive(:has_feature?).and_return(false) allowed.each do |enterprise_feature| allow(EnterpriseToken).to receive(:allows_to?).with(enterprise_feature).and_return(true) + allow(token_object_double).to receive(:has_feature?).with(enterprise_feature).and_return(true) end - # Also disable banners to signal the frontend we're on EE - allow(EnterpriseToken).to receive(:show_banners?).and_return(allowed.empty?) + # Also signal available features + allow(EnterpriseToken).to receive(:current).and_return(token_double) + allow(token_double) + .to receive_messages(token_object: token_object_double, + available_features: allowed.to_a, + expired?: false, + restrictions: {}) end end end diff --git a/spec/support/work_packages/activities_tab/restricted_visibility_comments_helpers.rb b/spec/support/work_packages/activities_tab/restricted_visibility_comments_helpers.rb new file mode 100644 index 00000000000..c5b51e39af3 --- /dev/null +++ b/spec/support/work_packages/activities_tab/restricted_visibility_comments_helpers.rb @@ -0,0 +1,73 @@ +# 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. +#++ +require "spec_helper" + +module RestrictedVisibilityCommentsHelpers + def create_user_without_restricted_comments_view_permissions + viewer_role = create(:project_role, permissions: %i[view_work_packages]) + create(:user, + firstname: "A", + lastname: "Viewer", + member_with_roles: { project => viewer_role }) + end + + def create_user_as_project_admin + member_role = create(:project_role, + permissions: %i[view_work_packages add_work_package_notes + edit_own_work_package_notes + view_comments_with_restricted_visibility + add_comments_with_restricted_visibility + edit_own_comments_with_restricted_visibility + edit_others_comments_with_restricted_visibility]) + create(:user, firstname: "Project", lastname: "Admin", + member_with_roles: { project => member_role }) + end + + def create_user_with_restricted_comments_view_permissions + viewer_role = create(:project_role, permissions: %i[view_work_packages view_comments_with_restricted_visibility]) + create(:user, + firstname: "Restricted", + lastname: "Viewer", + member_with_roles: { project => viewer_role }) + end + + def create_user_with_restricted_comments_view_and_write_permissions + viewer_role_with_commenting_permission = create(:project_role, + permissions: %i[view_work_packages add_work_package_notes + edit_own_work_package_notes + view_comments_with_restricted_visibility + add_comments_with_restricted_visibility + edit_own_comments_with_restricted_visibility]) + create(:user, + firstname: "Restricted", + lastname: "ViewerCommenter", + member_with_roles: { project => viewer_role_with_commenting_permission }) + end +end