diff --git a/.github/workflows/create-merge-release-into-dev-pr.yml b/.github/workflows/create-merge-release-into-dev-pr.yml index 9f41f323cae5..2b95176e07e7 100644 --- a/.github/workflows/create-merge-release-into-dev-pr.yml +++ b/.github/workflows/create-merge-release-into-dev-pr.yml @@ -16,7 +16,7 @@ jobs: steps: - id: find_latest_release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENPROJECT_CI_GH_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} run: | BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \ @@ -79,11 +79,19 @@ jobs: echo "Successfully merged $RELEASE_BRANCH into $BASE_BRANCH and pushed" else # Close all previous PRs with label - for pr_number in $(gh pr list --label create-merge-release-into-dev-pr --json number --jq='.[].number'); do - gh pr close "$pr_number" + pr_numbers=$(gh pr list --label create-merge-release-into-dev-pr --json number --jq='.[].number') + for pr_number in $pr_numbers; do + gh pr close "$pr_number" --delete-branch done - TEMP_BRANCH="$RELEASE_BRANCH-$(date "+%Y%m%d%H%M%S")" + pr_body=$( + echo 'Created by GitHub action' + for pr_number in $pr_numbers; do + echo "Replaces #$pr_number" + done + ) + + TEMP_BRANCH="merge-$RELEASE_BRANCH-$(date "+%Y%m%d%H%M%S")" git branch "$TEMP_BRANCH" "$RELEASE_BRANCH" @@ -93,10 +101,10 @@ jobs: --base "$BASE_BRANCH" \ --head "$TEMP_BRANCH" \ --title "Merge $RELEASE_BRANCH into $BASE_BRANCH" \ - --body 'Created by GitHub action' \ + --body "$pr_body" \ --label create-merge-release-into-dev-pr echo "Created a PR to merge $RELEASE_BRANCH ($TEMP_BRANCH) into $BASE_BRANCH" fi fi env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.OPENPROJECT_CI_GH_TOKEN }} diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 8b30f3873e49..1d49b64e64ba 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: - ruby-version: '3.2.3' + ruby-version: '3.3.1' - uses: MeilCli/danger-action@v5 with: danger_file: 'Dangerfile' diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index ab812ead60bd..0f3251d781a0 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -44,9 +44,9 @@ jobs: uses: runs-on/cache@v4 with: path: cache/bundle - key: gem-${{ hashFiles('Gemfile.lock') }} + key: gem-${{ hashFiles('.ruby-version') }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | - gem- + gem-${{ hashFiles('.ruby-version') }}- - name: Cache NPM uses: runs-on/cache@v4 with: diff --git a/.rubocop.yml b/.rubocop.yml index ba1a5d9139bb..c70f37c8780c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,7 @@ require: - rubocop-rails - rubocop-rspec + - rubocop-rspec_rails - ./lib_static/rubocop/cop/open_project/add_preview_for_view_component.rb - ./lib_static/rubocop/cop/open_project/no_do_end_block_with_rspec_capybara_matcher_in_expect.rb - ./lib_static/rubocop/cop/open_project/use_service_result_factory_methods.rb @@ -21,7 +22,7 @@ inherit_mode: - Exclude AllCops: - TargetRubyVersion: 3.2 + TargetRubyVersion: 3.3 # Enable any new cops in new versions by default NewCops: enable Exclude: diff --git a/.ruby-version b/.ruby-version index b347b11eac8a..bea438e9ade7 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.3 +3.3.1 diff --git a/Gemfile b/Gemfile index d4ca8bb65b84..f9f8b335961f 100644 --- a/Gemfile +++ b/Gemfile @@ -50,7 +50,7 @@ gem "doorkeeper", "~> 5.6.6" # Maintain our own omniauth due to relative URL root issues # see upstream PR: https://github.com/omniauth/omniauth/pull/903 gem "omniauth", git: "https://github.com/opf/omniauth", ref: "fe862f986b2e846e291784d2caa3d90a658c67f0" -gem "request_store", "~> 1.6.0" +gem "request_store", "~> 1.7.0" gem "warden", "~> 1.2" gem "warden-basic_auth", "~> 0.2.1" @@ -115,6 +115,8 @@ gem "ruby-duration", "~> 3.2.0" # released. gem "mail", "= 2.8.1" +gem "csv", "~> 3.3" + # provide compatible filesystem information for available storage gem "sys-filesystem", "~> 1.4.0", require: false @@ -138,7 +140,7 @@ gem "rack-attack", "~> 6.7.0" gem "secure_headers", "~> 6.5.0" # Browser detection for incompatibility checks -gem "browser", "~> 5.3.0" +gem "browser", "~> 6.0.0" # Providing health checks gem "okcomputer", "~> 1.18.1" @@ -157,10 +159,11 @@ gem "airbrake", "~> 13.0.0", require: false gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "8f14736a88ad0064d2a97be108fe7061ffbcee91" gem "prawn", "~> 2.4" +gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved. # prawn implicitly depends on matrix gem no longer in ruby core with 3.1 gem "matrix", "~> 0.4.2" -gem "meta-tags", "~> 2.20.0" +gem "meta-tags", "~> 2.21.0" gem "paper_trail", "~> 15.1.0" @@ -170,7 +173,7 @@ group :production do # we use dalli as standard memcache client # requires memcached 1.4+ gem "dalli", "~> 3.2.0" - gem "redis", "~> 5.1.0" + gem "redis", "~> 5.2.0" end gem "i18n-js", "~> 4.2.3" @@ -185,7 +188,7 @@ gem "rack-timeout", "~> 0.6.3", require: "rack/timeout/base" gem "nokogiri", "~> 1.16.0" -gem "carrierwave", "~> 1.3.1" +gem "carrierwave", "~> 1.3.4" gem "carrierwave_direct", "~> 2.1.0" gem "fog-aws" @@ -214,7 +217,7 @@ gem "appsignal", "~> 3.0", require: false gem "view_component" # Lookbook -gem "lookbook", "~> 2.2.1" +gem "lookbook", "~> 2.3.0" # Require factory_bot for usage with openproject plugins testing gem "factory_bot", "~> 6.4.0", require: false @@ -264,7 +267,7 @@ group :test do gem "capybara-screenshot", "~> 1.0.17" gem "cuprite", "~> 0.15.0" gem "selenium-devtools" - gem "selenium-webdriver", "~> 4.18.0" + gem "selenium-webdriver", "~> 4.20" gem "fuubar", "~> 2.5.0" gem "timecop", "~> 0.9.0" @@ -383,4 +386,4 @@ end gem "openproject-octicons", "~>19.10.0" gem "openproject-octicons_helper", "~>19.10.0" -gem "openproject-primer_view_components", "~>0.28.1" +gem "openproject-primer_view_components", "~>0.29.1" diff --git a/Gemfile.lock b/Gemfile.lock index 274a23ee0274..2eda3f308153 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,7 +74,7 @@ PATH remote: modules/avatars specs: openproject-avatars (1.0.0) - fastimage (~> 2.2.0) + fastimage (~> 2.3.0) gravatar_image_tag (~> 1.2.0) PATH @@ -206,7 +206,7 @@ PATH remote: modules/two_factor_authentication specs: openproject-two_factor_authentication (1.0.0) - aws-sdk-sns (~> 1.72.0) + aws-sdk-sns (~> 1.74.0) messagebird-rest (~> 1.4.2) rotp (~> 6.1) webauthn (~> 3.0) @@ -331,7 +331,7 @@ GEM airbrake-ruby (6.2.2) rbtree3 (~> 0.6) android_key_attestation (0.3.0) - appsignal (3.6.4) + appsignal (3.7.4) rack ast (2.4.2) attr_required (1.0.2) @@ -341,21 +341,21 @@ GEM activerecord (>= 4.0.0, < 7.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.907.0) - aws-sdk-core (3.191.6) + aws-partitions (1.925.0) + aws-sdk-core (3.194.2) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.78.0) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-kms (1.80.0) + aws-sdk-core (~> 3, >= 3.193.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.146.1) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-s3 (1.149.1) + aws-sdk-core (~> 3, >= 3.194.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) - aws-sdk-sns (1.72.0) - aws-sdk-core (~> 3, >= 3.191.0) + aws-sdk-sns (1.74.0) + aws-sdk-core (~> 3, >= 3.193.0) aws-sigv4 (~> 1.1) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) @@ -379,13 +379,13 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.7) + bigdecimal (3.1.8) bindata (2.5.0) bootsnap (1.18.3) msgpack (~> 1.2) brakeman (6.1.2) racc - browser (5.3.1) + browser (6.0.0) builder (3.2.4) byebug (11.1.3) capybara (3.40.0) @@ -400,11 +400,11 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy - carrierwave (1.3.2) + carrierwave (1.3.4) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) - ssrf_filter (~> 1.0) + ssrf_filter (~> 1.0, < 1.1.0) carrierwave_direct (2.1.0) carrierwave (>= 1.0.0) fog-aws @@ -434,8 +434,9 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.16.0) + css_parser (1.17.1) addressable + csv (3.3.0) cuprite (0.15) capybara (~> 3.0) ferrum (~> 0.14.0) @@ -459,9 +460,9 @@ GEM representable (>= 3.1.1, < 4) doorkeeper (5.6.9) railties (>= 5) - dotenv (3.1.0) - dotenv-rails (3.1.0) - dotenv (= 3.1.0) + dotenv (3.1.2) + dotenv-rails (3.1.2) + dotenv (= 3.1.2) railties (>= 6.1) drb (2.2.1) dry-container (0.11.0) @@ -522,7 +523,7 @@ GEM faraday (>= 1, < 3) faraday-net_http (3.1.0) net-http - fastimage (2.2.7) + fastimage (2.3.1) ferrum (0.14) addressable (~> 2.5) concurrent-ruby (~> 1.1) @@ -549,8 +550,8 @@ GEM friendly_id (5.5.1) activerecord (>= 4.0.0) front_matter_parser (1.0.1) - fugit (1.10.1) - et-orbi (~> 1, >= 1.2.7) + fugit (1.11.0) + et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) fuubar (2.5.1) rspec-core (~> 3.0) @@ -578,7 +579,7 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - google-apis-gmail_v1 (0.39.0) + google-apis-gmail_v1 (0.40.0) google-apis-core (>= 0.14.0, < 2.a) google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) @@ -614,17 +615,16 @@ GEM http-2-next (1.0.3) http_parser.rb (0.6.0) httpclient (2.8.3) - httpx (1.2.3) + httpx (1.2.4) http-2-next (>= 1.0.3) - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) i18n-js (4.2.3) glob (>= 0.4.0) i18n - i18n-tasks (1.0.13) + i18n-tasks (1.0.14) activesupport (>= 4.0.2) ast (>= 2.1.0) - better_html (>= 1.0, < 3.0) erubi highline (>= 2.0.0) i18n @@ -638,8 +638,8 @@ GEM ice_nine (0.11.2) interception (0.5) io-console (0.7.2) - irb (1.12.0) - rdoc + irb (1.13.1) + rdoc (>= 4.0.0) reline (>= 0.4.2) iso8601 (0.13.0) jmespath (1.6.2) @@ -651,7 +651,7 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects - json-schema (4.2.0) + json-schema (4.3.0) addressable (>= 2.8) json_schemer (2.2.1) base64 @@ -667,10 +667,10 @@ GEM ladle (1.0.1) open4 (~> 1.0) language_server-protocol (3.17.0.3) - launchy (3.0.0) + launchy (3.0.1) addressable (~> 2.8) childprocess (~> 5.0) - lefthook (1.6.9) + lefthook (1.6.10) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (2.0.0) @@ -693,7 +693,7 @@ GEM loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) - lookbook (2.2.2) + lookbook (2.3.0) activemodel css_parser htmlbeautifier (~> 1.3) @@ -714,15 +714,15 @@ GEM markly (0.10.0) matrix (0.4.2) messagebird-rest (1.4.2) - meta-tags (2.20.0) + meta-tags (2.21.0) actionpack (>= 6.0.0, < 7.2) - method_source (1.0.0) + method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2024.0305) mini_magick (4.12.0) mini_mime (1.1.5) - mini_portile2 (2.8.5) + mini_portile2 (2.8.6) minitest (5.22.3) msgpack (1.7.2) multi_json (1.15.0) @@ -744,7 +744,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.1) - nokogiri (1.16.3) + nokogiri (1.16.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.16.3) @@ -772,7 +772,7 @@ GEM actionview openproject-octicons (= 19.10.0) railties - openproject-primer_view_components (0.28.1) + openproject-primer_view_components (0.29.1) actionview (>= 5.0.0) activesupport (>= 5.0.0) openproject-octicons (>= 19.9.0) @@ -788,12 +788,12 @@ GEM activerecord (>= 6.1) request_store (~> 1.4) parallel (1.24.0) - parallel_tests (4.6.1) + parallel_tests (4.7.1) parallel - parser (3.3.0.5) + parser (3.3.1.0) ast (~> 2.4.1) racc - pdf-core (0.10.0) + pdf-core (0.9.0) pdf-inspector (1.3.0) pdf-reader (>= 1.0, < 3.0.a) pdf-reader (2.12.0) @@ -807,10 +807,9 @@ GEM activesupport (> 2.2.1) nokogiri (~> 1.10, >= 1.10.4) rubyzip (>= 1.2.0) - prawn (2.5.0) - matrix (~> 0.4) - pdf-core (~> 0.10.0) - ttfunk (~> 1.8) + prawn (2.4.0) + pdf-core (~> 0.9.0) + ttfunk (~> 1.7) prawn-table (0.2.2) prawn (>= 1.3.0, < 3.0.0) pry (0.14.2) @@ -915,24 +914,24 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - rb_sys (0.9.91) + rb_sys (0.9.97) rbtree3 (0.7.1) rdoc (6.6.3.1) psych (>= 4.0.0) recaptcha (5.16.0) redcarpet (3.6.0) - redis (5.1.0) - redis-client (>= 0.17.0) - redis-client (0.21.0) + redis (5.2.0) + redis-client (>= 0.22.0) + redis-client (0.22.1) connection_pool regexp_parser (2.9.0) - reline (0.5.0) + reline (0.5.6) io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - request_store (1.6.0) + request_store (1.7.0) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) @@ -967,7 +966,7 @@ GEM rspec-retry (0.6.2) rspec-core (> 3.3) rspec-support (3.13.1) - rubocop (1.63.1) + rubocop (1.63.5) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -978,8 +977,8 @@ GEM rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.31.2) - parser (>= 3.3.0.4) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) rubocop-capybara (2.20.0) rubocop (~> 1.41) rubocop-factory_bot (2.25.1) @@ -996,12 +995,12 @@ GEM rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.29.1) + rubocop-rspec (2.29.2) rubocop (~> 1.40) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) rubocop-rspec_rails (~> 2.28) - rubocop-rspec_rails (2.28.2) + rubocop-rspec_rails (2.28.3) rubocop (~> 1.40) ruby-duration (3.2.3) activesupport (>= 3.0.0) @@ -1024,9 +1023,9 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.12.0) secure_headers (6.5.0) - selenium-devtools (0.123.0) + selenium-devtools (0.124.0) selenium-webdriver (~> 4.2) - selenium-webdriver (4.18.1) + selenium-webdriver (4.20.1) base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) @@ -1040,13 +1039,13 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simpleidn (0.2.1) + simpleidn (0.2.2) unf (~> 0.1.4) smart_properties (1.17.0) spreadsheet (1.3.1) bigdecimal ruby-ole - spring (4.2.0) + spring (4.2.1) spring-commands-rspec (1.0.4) spring (>= 0.9.1) spring-commands-rubocop (0.4.0) @@ -1059,7 +1058,7 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - ssrf_filter (1.1.2) + ssrf_filter (1.0.8) stackprof (0.2.26) store_attribute (1.2.0) activerecord (>= 6.0) @@ -1077,7 +1076,7 @@ GEM table_print (1.5.7) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - test-prof (1.3.2) + test-prof (1.3.3) text-hyphen (1.5.0) thor (1.3.1) thread_safe (0.3.6) @@ -1088,8 +1087,7 @@ GEM openssl (> 2.0) openssl-signature_algorithm (~> 1.0) trailblazer-option (0.1.2) - ttfunk (1.8.0) - bigdecimal (~> 3.1) + ttfunk (1.7.0) turbo-rails (2.0.5) actionpack (>= 6.0.0) activejob (>= 6.0.0) @@ -1113,7 +1111,7 @@ GEM activemodel (>= 3.0.0) public_suffix vcr (6.2.0) - view_component (3.11.0) + view_component (3.12.1) activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) @@ -1178,12 +1176,12 @@ DEPENDENCIES bcrypt (~> 3.1.6) bootsnap (~> 1.18.0) brakeman (~> 6.1.0) - browser (~> 5.3.0) + browser (~> 6.0.0) budgets! capybara (~> 3.40.0) capybara-screenshot (~> 1.0.17) capybara_accessible_selectors! - carrierwave (~> 1.3.1) + carrierwave (~> 1.3.4) carrierwave_direct (~> 2.1.0) climate_control closure_tree (~> 7.4.0) @@ -1191,6 +1189,7 @@ DEPENDENCIES commonmarker (~> 1.0.3) compare-xml (~> 0.66) costs! + csv (~> 3.3) cuprite (~> 0.15.0) daemons dalli (~> 3.2.0) @@ -1234,11 +1233,11 @@ DEPENDENCIES letter_opener_web listen (~> 3.9.0) lograge (~> 0.14.0) - lookbook (~> 2.2.1) + lookbook (~> 2.3.0) mail (= 2.8.1) matrix (~> 0.4.2) md_to_pdf! - meta-tags (~> 2.20.0) + meta-tags (~> 2.21.0) mini_magick (~> 4.12.0) multi_json (~> 1.15.0) my_page! @@ -1268,7 +1267,7 @@ DEPENDENCIES openproject-octicons (~> 19.10.0) openproject-octicons_helper (~> 19.10.0) openproject-openid_connect! - openproject-primer_view_components (~> 0.28.1) + openproject-primer_view_components (~> 0.29.1) openproject-recaptcha! openproject-reporting! openproject-storages! @@ -1303,8 +1302,8 @@ DEPENDENCIES rails-controller-testing (~> 1.0.2) rails-i18n (~> 7.0.0) rdoc (>= 2.4.2) - redis (~> 5.1.0) - request_store (~> 1.6.0) + redis (~> 5.2.0) + request_store (~> 1.7.0) responders (~> 3.0) retriable (~> 3.1.1) rinku (~> 2.0.4) @@ -1325,7 +1324,7 @@ DEPENDENCIES sanitize (~> 6.1.0) secure_headers (~> 6.5.0) selenium-devtools - selenium-webdriver (~> 4.18.0) + selenium-webdriver (~> 4.20) semantic (~> 1.6.1) shoulda-context (~> 2.0) shoulda-matchers (~> 6.0) @@ -1343,6 +1342,7 @@ DEPENDENCIES table_print (~> 1.5.6) test-prof (~> 1.3.0) timecop (~> 0.9.0) + ttfunk (~> 1.7.0) turbo-rails (~> 2.0.0) turbo_tests! typed_dag (~> 2.0.2) @@ -1357,7 +1357,7 @@ DEPENDENCIES with_advisory_lock (~> 5.1.0) RUBY VERSION - ruby 3.2.3p157 + ruby 3.3.1p55 BUNDLED WITH - 2.5.5 + 2.5.10 diff --git a/app/components/_index.sass b/app/components/_index.sass index eca561ec0d4f..df5d51df72f6 100644 --- a/app/components/_index.sass +++ b/app/components/_index.sass @@ -4,3 +4,4 @@ @import "open_project/common/attribute_component" @import "filters_component" @import "projects/settings/project_custom_field_sections/index_component" +@import "projects/row_component" diff --git a/app/components/admin/attachments_settings_header_component.html.erb b/app/components/admin/attachments_settings_header_component.html.erb new file mode 100644 index 000000000000..52ccc429b460 --- /dev/null +++ b/app/components/admin/attachments_settings_header_component.html.erb @@ -0,0 +1,53 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 2012-2024 the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<% helpers.html_title t(:label_administration), @title %> + +<%= render(Primer::OpenProject::PageHeader.new(border_bottom: 0)) do |header| %> + <% header.with_title { t(:"attributes.attachments") } %> + <% header.with_breadcrumbs([{ href: admin_index_path, text: t("label_administration") }, + { href: admin_settings_storages_path, text: t("project_module_storages") }, + t(:"attributes.attachments")]) %> +<% end %> + +<%= render(Primer::Alpha::TabNav.new(label: "label")) do |component| + component.with_tab(selected: @selected == 1, href: admin_settings_attachments_path) do |tab| + tab.with_text { t("settings.general") } + end + component.with_tab(selected: @selected == 2, href: admin_settings_virus_scanning_path) do |tab| + tab.with_icon(icon: :"op-enterprise-addons") unless EnterpriseToken.allows_to?("virus_scanning") + tab.with_text { t(:"settings.antivirus.title") } + end + if User.current.admin? && (EnterpriseToken.allows_to?(:virus_scanning) || Attachment.status_quarantined.any?) + component.with_tab(selected: @selected == 3, href: admin_quarantined_attachments_path) do |tab| + tab.with_text { t(:"antivirus_scan.quarantined_attachments.title") } + end + end +end +%> diff --git a/app/components/admin/attachments_settings_header_component.rb b/app/components/admin/attachments_settings_header_component.rb new file mode 100644 index 000000000000..f9a955648a2e --- /dev/null +++ b/app/components/admin/attachments_settings_header_component.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module Admin + class AttachmentsSettingsHeaderComponent < ApplicationComponent + def initialize(title:, selected:) + raise 'selected must 1, 2 or 3' if [1, 2, 3].exclude?(selected) + @title = title + @selected = selected + end + end +end diff --git a/app/components/admin/quarantined_attachments/index_page_header_component.html.erb b/app/components/admin/quarantined_attachments/index_page_header_component.html.erb deleted file mode 100644 index 994462d08155..000000000000 --- a/app/components/admin/quarantined_attachments/index_page_header_component.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= - render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title { t('antivirus_scan.quarantined_attachments.title') } - header.with_breadcrumbs(breadcrumb_items) - end -%> diff --git a/app/components/admin/virus_scanning/index_page_header_component.html.erb b/app/components/admin/virus_scanning/index_page_header_component.html.erb deleted file mode 100644 index 44a4a26f3f3c..000000000000 --- a/app/components/admin/virus_scanning/index_page_header_component.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= render(Primer::OpenProject::PageHeader.new) do |header| %> - <% header.with_title do %> - <%= t('settings.antivirus.title') %> - <% end %> - <% header.with_breadcrumbs(breadcrumb_items)%> -<% end %> diff --git a/app/components/concerns/op_turbo/streamable.rb b/app/components/concerns/op_turbo/streamable.rb index d0d76118d96a..000bdc1088c8 100644 --- a/app/components/concerns/op_turbo/streamable.rb +++ b/app/components/concerns/op_turbo/streamable.rb @@ -43,7 +43,7 @@ def wrapper_key included do def render_as_turbo_stream(view_context:, action: :update) case action - when :update + when :update, :dialog @inner_html_only = true template = render_in(view_context) when :replace @@ -56,7 +56,7 @@ def render_as_turbo_stream(view_context:, action: :update) raise ArgumentError, "Unsupported action #{action}" end - unless wrapped? + if action != :dialog && !wrapped? raise MissingComponentWrapper, "Wrap your component in a `component_wrapper` block in order to use turbo-stream methods" end diff --git a/app/components/filters_component.html.erb b/app/components/filters_component.html.erb index f17e15685497..084613d8785d 100644 --- a/app/components/filters_component.html.erb +++ b/app/components/filters_component.html.erb @@ -1,5 +1,6 @@
+ <%= helpers.op_icon('icon-info1') %> + <%= t(:label_projects_disk_usage_information, + count: Project.count, + used_disk_space: number_to_human_size(Project.total_projects_size, precision: 2)) %> +
diff --git a/app/components/projects/storage_information_component.rb b/app/components/projects/disk_usage_information_component.rb similarity index 95% rename from app/components/projects/storage_information_component.rb rename to app/components/projects/disk_usage_information_component.rb index e42c11bfa2f2..6a6984e64fe9 100644 --- a/app/components/projects/storage_information_component.rb +++ b/app/components/projects/disk_usage_information_component.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. # ++ -class Projects::StorageInformationComponent < ApplicationComponent +class Projects::DiskUsageInformationComponent < ApplicationComponent options :current_user def render? diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb index d1ddafbc2a9a..6d9c1e479e8c 100644 --- a/app/components/projects/index_page_header_component.html.erb +++ b/app/components/projects/index_page_header_component.html.erb @@ -1,26 +1,24 @@ -<%= - render(Primer::OpenProject::PageHeader.new) do |header| - if show_state? +<% if show_state? %> + <%= + render(Primer::OpenProject::PageHeader.new) do |header| header.with_title(data: { 'test-selector': 'project-query-name'}) { page_title } - header.with_breadcrumbs(breadcrumb_items) - - if query_saveable? - header.with_action_text { t('lists.can_be_saved_as') } + header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal) - header.with_action_link(mobile_icon: nil, # Do not show on mobile as it is already part of the menu - mobile_label: nil, - href: new_projects_query_path, - data: { - controller: "params-from-query", - 'application-target': "dynamic", - 'params-from-query-allowed-value': '["filters", "columns"]' - }) do - render(Primer::Beta::Octicon.new(icon: "op-save", - align_self: :center, - "aria-label": I18n.t("button_save_as"), - mr: 1) - ) + content_tag(:span, t("button_save_as")) - end + if can_save? + header_save_action( + header:, + message: t("lists.can_be_saved"), + label: t("button_save"), + href: projects_query_path(query), + method: :patch + ) + elsif can_save_as? + header_save_action( + header:, + message: t("lists.can_be_saved_as"), + label: t("button_save_as"), + href: new_projects_query_path + ) end header.with_action_menu(menu_arguments: { @@ -51,20 +49,12 @@ item.with_leading_visual_icon(icon: 'tasklist') end - if query_saveable? - menu.with_item( - label: t('button_save_as'), - href: new_projects_query_path, - content_arguments: { - data: { - controller: "params-from-query", - 'application-target': "dynamic", - 'params-from-query-allowed-value': '["filters", "columns"]' - } - } - ) do |item| - item.with_leading_visual_icon(icon: :'op-save') - end + if can_save? + menu_save_item(menu:, label: t('button_save'), href: projects_query_path(query), method: :patch) + end + + if may_save_as? + menu_save_item(menu:, label: t('button_save_as'), href: new_projects_query_path) end menu.with_item( @@ -91,9 +81,15 @@ end end end + end + %> - - else + <%= render(Projects::ConfigureViewModalComponent.new(query:)) %> + <%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %> + <%= render(Projects::ExportListModalComponent.new(query:)) %> +<% else %> + <%= + render(Primer::OpenProject::PageHeader.new) do |header| header.with_title(data: { 'test-selector': 'project-query-name'}) do primer_form_with(model: query, url: projects_queries_path, @@ -101,7 +97,7 @@ data: { controller: "params-from-query", 'application-target': "dynamic", - 'params-from-query-allowed-value': '["filters", "columns"]' + 'params-from-query-allowed-value': '["filters", "columns", "query_id", "sortBy"]' }, id: 'project-save-form') do |f| render(Queries::Projects::Create.new(f)) @@ -109,11 +105,5 @@ end header.with_breadcrumbs(breadcrumb_items) end - end -%> - -<% if show_state? %> - <%= render(Projects::ConfigureViewModalComponent.new(query:)) %> - <%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %> - <%= render(Projects::ExportListModalComponent.new(query:)) %> + %> <% end %> diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb index ebba3e8a5e4f..c49a6dc05405 100644 --- a/app/components/projects/index_page_header_component.rb +++ b/app/components/projects/index_page_header_component.rb @@ -69,9 +69,11 @@ def page_title query.name || t(:label_project_plural) end - def query_saveable? - current_user.logged? && query.name.blank? - end + def may_save_as? = current_user.logged? + + def can_save_as? = may_save_as? && query.changed? + + def can_save? = can_save_as? && query.persisted? && query.user == current_user def show_state? state == :show @@ -97,4 +99,46 @@ def current_breadcrumb_element page_title end end + + def header_save_action(header:, message:, label:, href:, method: nil) + header.with_action_text { message } + + header.with_action_link( + mobile_icon: nil, # Do not show on mobile as it is already part of the menu + mobile_label: nil, + href:, + data: { + method:, + controller: "params-from-query", + "application-target": "dynamic", + "params-from-query-allowed-value": '["filters", "columns", "sortBy", "query_id"]' + }.compact + ) do + render( + Primer::Beta::Octicon.new( + icon: "op-save", + align_self: :center, + "aria-label": label, + mr: 1 + ) + ) + content_tag(:span, label) + end + end + + def menu_save_item(menu:, label:, href:, method: nil) + menu.with_item( + label:, + href:, + content_arguments: { + data: { + method:, + controller: "params-from-query", + "application-target": "dynamic", + "params-from-query-allowed-value": '["filters", "columns", "sortBy", "query_id"]' + }.compact + } + ) do |item| + item.with_leading_visual_icon(icon: :"op-save") + end + end end diff --git a/app/components/projects/projects_filters_component.rb b/app/components/projects/projects_filters_component.rb index 6ffc4af774a3..450e2545aa77 100644 --- a/app/components/projects/projects_filters_component.rb +++ b/app/components/projects/projects_filters_component.rb @@ -47,7 +47,8 @@ def allowed_filter?(filter) Queries::Projects::Filters::CreatedAtFilter, Queries::Projects::Filters::LatestActivityAtFilter, Queries::Projects::Filters::NameAndIdentifierFilter, - Queries::Projects::Filters::TypeFilter + Queries::Projects::Filters::TypeFilter, + Queries::Projects::Filters::FavoredFilter ] allowlist << Queries::Filters::Shared::CustomFields::Base if EnterpriseToken.allows_to?(:custom_fields_in_projects_list) diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index d395e3111a3d..6dd80c99c2cd 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -29,6 +29,8 @@ #++ module Projects class RowComponent < ::RowComponent + delegate :favored_project_ids, to: :table + def project model.first end @@ -42,6 +44,27 @@ def hierarchy "" end + def favored + render(Primer::Beta::IconButton.new( + icon: currently_favored? ? "star-fill" : "star", + scheme: :invisible, + mobile_icon: currently_favored? ? "star-fill" : "star", + size: :medium, + tag: :a, + tooltip_direction: :e, + href: helpers.build_favorite_path(project, format: :html), + data: { method: currently_favored? ? :delete : :post }, + classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite", + label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite), + aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) }, + test_selector: 'project-list-favorite-button' + )) + end + + def currently_favored? + @currently_favored ||= favored_project_ids.include?(project.id) + end + def column_value(column) if custom_field_column?(column) custom_field_column(column) @@ -138,13 +161,17 @@ def public end def row_css_class - classes = %w[basics context-menu--reveal] + classes = %w[basics context-menu--reveal op-project-row-component] classes << project_css_classes classes << row_css_level_classes classes.join(" ") end + def row_css_id + "project-#{project.id}" + end + def row_css_level_classes if level > 0 "idnt idnt-#{level}" @@ -180,36 +207,29 @@ def additional_css_class(column) end def button_links - return [] if more_menu_items.empty? - - if more_menu_items.one? - more_menu_items.first => {label:, **button_options} - - [render(Primer::Beta::IconButton.new(**button_options, - size: :small, - tag: :a, - scheme: button_options[:scheme] == :default ? :invisible : button_options[:scheme], - "aria-label": label, - test_selector: "project-list-row--single-action"))] + if more_menu_items.empty? + [] else - [ - render(Primer::Alpha::ActionMenu.new(test_selector: "project-list-row--action-menu")) do |menu| - menu.with_show_button(scheme: :invisible, - size: :small, - icon: :"kebab-horizontal", - "aria-label": t(:label_open_menu), - tooltip_direction: :w) - more_menu_items.each do |action_options| - action_options => {scheme:, label:, icon:, **button_options} - menu.with_item(scheme:, - label:, - test_selector: "project-list-row--action-menu-item", - content_arguments: button_options) do |item| - item.with_leading_visual_icon(icon:) - end - end + [action_menu] + end + end + + def action_menu + render(Primer::Alpha::ActionMenu.new(test_selector: "project-list-row--action-menu")) do |menu| + menu.with_show_button(scheme: :invisible, + size: :small, + icon: :"kebab-horizontal", + "aria-label": t(:label_open_menu), + tooltip_direction: :w) + more_menu_items.each do |action_options| + action_options => { scheme:, label:, icon:, **button_options } + menu.with_item(scheme:, + label:, + test_selector: "project-list-row--action-menu-item", + content_arguments: button_options) do |item| + item.with_leading_visual_icon(icon:) end - ] + end end end @@ -217,12 +237,42 @@ def more_menu_items @more_menu_items ||= [more_menu_subproject_item, more_menu_settings_item, more_menu_activity_item, + more_menu_favorite_item, + more_menu_unfavorite_item, more_menu_archive_item, more_menu_unarchive_item, more_menu_copy_item, more_menu_delete_item].compact end + def more_menu_favorite_item + return if currently_favored? + + { + scheme: :default, + icon: "star", + href: helpers.build_favorite_path(project, format: :html), + data: { method: :post }, + label: I18n.t(:button_favorite), + aria: { label: I18n.t(:button_favorite) }, + } + end + + def more_menu_unfavorite_item + return unless currently_favored? + + { + scheme: :default, + icon: "star-fill", + size: :medium, + href: helpers.build_favorite_path(project, format: :html), + data: { method: :delete }, + classes: "op-primer--star-icon", + label: I18n.t(:button_unfavorite), + aria: { label: I18n.t(:button_unfavorite) }, + } + end + def more_menu_subproject_item if User.current.allowed_in_project?(:add_subprojects, project) { diff --git a/app/components/projects/row_component.sass b/app/components/projects/row_component.sass new file mode 100644 index 000000000000..aec8e467e332 --- /dev/null +++ b/app/components/projects/row_component.sass @@ -0,0 +1,6 @@ +.op-project-row-component + &--favorite + opacity: 0 + + &:hover &--favorite + opacity: 1 diff --git a/app/components/projects/storage_information_component.html.erb b/app/components/projects/storage_information_component.html.erb deleted file mode 100644 index 92a6c1658bce..000000000000 --- a/app/components/projects/storage_information_component.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -- <%= helpers.op_icon('icon-info1') %> - <%= t(:label_projects_storage_information, - count: Project.count, - storage: number_to_human_size(Project.total_projects_size, precision: 2)) %> -
diff --git a/app/components/projects/table_component.html.erb b/app/components/projects/table_component.html.erb index 728f841b1a77..1b95df7fbb5a 100644 --- a/app/components/projects/table_component.html.erb +++ b/app/components/projects/table_component.html.erb @@ -64,7 +64,11 @@ See COPYRIGHT and LICENSE files for more details.