Merge branch dev into feature/36233-openproject-dark-mode-mvp

This commit is contained in:
Behrokh Satarnejad
2024-06-03 11:35:29 +02:00
2168 changed files with 47402 additions and 30750 deletions
+2
View File
@@ -33,3 +33,5 @@ frontend/node_modules
node_modules
# travis
vendor/bundle
/public/assets
/config/frontend_assets.manifest.json
+2 -1
View File
@@ -5,8 +5,9 @@
21a696ef9b170e14ad2daf53364a4c2113822c2f
# Update copyright information for 2024
c795874f7f281297bbd3bad2fdb58b24cb4ce624
# switch to double quotes
# rubocop autocorrections
f3c99ee5dded81ad55f2b6f3706216d5fa765677
5c72ea0046a6b5230bf456f55a296ed6fd579535
9e4934cd0a468f46d8f0fc0f11ebc2d4216f789c
6678cab48d443b5782fa93b171d62093819ee4fc
fa5d03eae00bc8931f99598a74ffd76e0cbca3da
+1 -1
View File
@@ -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'
+100 -48
View File
@@ -3,7 +3,7 @@ name: Docker
on:
# build dev daily
schedule:
- cron: '20 2 * * *' # Daily at 02:20
- cron: "20 2 * * *" # Daily at 02:20
push:
tags:
@@ -21,7 +21,7 @@ env:
REGISTRY_IMAGE: openproject/openproject
jobs:
extract_version:
setup:
runs-on: ubuntu-latest
steps:
- name: Extract version
@@ -36,6 +36,11 @@ jobs:
elif [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
TAG_REF=${{ inputs.tag }}
CHECKOUT_REF=${{ inputs.tag }}
if [ -z "$TAG_REF" ]; then
TAG_REF=dev
CHECKOUT_REF=dev
fi
else
echo "Unsupported event"
exit 1
@@ -50,75 +55,110 @@ jobs:
echo "Version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "checkout_ref=$CHECKOUT_REF" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
- name: Cache NPM
uses: runs-on/cache@v4
with:
path: |
frontend/node_modules
node_modules
key: nodejs-x64-${{ hashFiles('**/package-lock.json') }}
restore-keys: nodejs-x64-
- name: Cache angular
uses: runs-on/cache@v4
with:
path: frontend/.angular
key: angular-${{ github.ref }}
restore-keys: angular-
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: |
package-lock.json
frontend/package-lock.json
- name: Precompile assets
run: |
./docker/prod/setup/precompile-assets.sh
# public/assets will be saved as artifact, so temporarily copying config file there as well
cp config/frontend_assets.manifest.json public/assets/frontend_assets.manifest.json
- uses: actions/upload-artifact@v3
with:
path: public/
name: public-assets-${{ github.sha }}
outputs:
version: ${{ steps.extract_version.outputs.version }}
checkout_ref: ${{ steps.extract_version.outputs.checkout_ref }}
build:
needs:
- extract_version
- setup
if: github.repository == 'opf/openproject'
runs-on: runs-on,runner=32cpu-linux-x64,run-id=${{ github.run_id }}
runs-on:
labels:
- runs-on
- ssh=false
- run-id=${{ github.run_id }}
- ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
bim_support: true
target: slim
runner: runner=4cpu-linux-x64
- platform: linux/arm64/v8
bim_support: false
target: slim
runner: runner=4cpu-linux-arm64
- platform: linux/amd64
bim_support: true
target: all-in-one
runner: runner=4cpu-linux-x64
- platform: linux/arm64/v8
bim_support: false
target: all-in-one
runner: runner=4cpu-linux-arm64
- platform: linux/ppc64le
bim_support: false
target: all-in-one
- platform: linux/arm64/v8
bim_support: false
target: all-in-one
runner: runner=4cpu-linux-x64
steps:
- name: Extract version
id: extract_version
run: |
if [[ ${{ github.event_name }} == 'push' ]]; then
TAG_REF=${GITHUB_REF#refs/tags/}
CHECKOUT_REF=$GITHUB_REF
elif [[ ${{ github.event_name }} == 'schedule' ]]; then
TAG_REF=dev
CHECKOUT_REF=refs/heads/dev
elif [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
TAG_REF=${{ inputs.tag }}
CHECKOUT_REF=${{ inputs.tag }}
else
echo "Unsupported event"
exit 1
fi
if [ -z "$TAG_REF" ] || [ -z "$CHECKOUT_REF" ]; then
echo "No TAG_REF or CHECKOUT_REF set. Aborting"
exit 1
fi
VERSION=${TAG_REF#v}
echo "Version: $VERSION"
echo "::set-output name=version::$VERSION"
echo "::set-output name=checkout_ref::$CHECKOUT_REF"
- name: Checkout
with:
ref: ${{ steps.extract_version.outputs.checkout_ref }}
ref: ${{ needs.setup.outputs.checkout_ref }}
uses: actions/checkout@v4
- name: Prepare docker files
run: |
cp ./docker/prod/Dockerfile ./Dockerfile
# Add build information
echo "${{ steps.extract_version.outputs.checkout_ref }}" > PRODUCT_VERSION
echo "https://github.com/opf/openproject/commits/${{ steps.extract_version.outputs.checkout_ref }}" > PRODUCT_URL
- name: Download precompiled public assets
uses: actions/download-artifact@v3
with:
name: public-assets-${{ github.sha }}
path: public/
- name: Setup precompiled assets
run: |
ls -al public/
mv public/assets/frontend_assets.manifest.json config/frontend_assets.manifest.json
ls -al config/frontend_assets.manifest.json
# allow precompiled assets to be injected into docker image
echo '' >> .dockerignore
echo '!/public/assets' >> .dockerignore
echo '!/config/frontend_assets.manifest.json' >> .dockerignore
- name: Add build information
run: |
echo "${{ needs.setup.outputs.checkout_ref }}" > PRODUCT_VERSION
echo "https://github.com/opf/openproject/commits/${{ needs.setup.outputs.checkout_ref }}" > PRODUCT_URL
date -u +"%Y-%m-%dT%H:%M:%SZ" > RELEASE_DATE
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -132,7 +172,7 @@ jobs:
org.opencontainers.image.documentation=https://www.openproject.org/docs/
org.opencontainers.image.vendor=OpenProject GmbH
tags: |
type=semver,pattern={{version}},value=${{ needs.extract_version.outputs.version }}
type=semver,pattern={{version}},value=${{ needs.setup.outputs.version }}
images: |
${{ env.REGISTRY_IMAGE }}
- name: Build image
@@ -148,6 +188,8 @@ jobs:
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
- name: Test
# We only test the native container. If that fails the builds for the others
# will be cancelled as well.
@@ -176,6 +218,8 @@ jobs:
BIM_SUPPORT=${{ matrix.bim_support }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
- name: Export digest
run: |
mkdir -p /tmp/digests
@@ -194,7 +238,7 @@ jobs:
matrix:
target: [slim, all-in-one]
needs:
- extract_version
- setup
- build
steps:
- name: Download digests
@@ -209,7 +253,7 @@ jobs:
if [ "$suffix" = "-all-in-one" ]; then suffix="" ; fi
echo "suffix=$suffix" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
@@ -223,12 +267,12 @@ jobs:
latest=false
suffix=${{ steps.set_suffix.outputs.suffix }}
tags: |
type=semver,pattern={{version}},value=${{ needs.extract_version.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.extract_version.outputs.version }}
type=semver,pattern={{major}},value=${{ needs.extract_version.outputs.version }}
type=semver,pattern={{version}},value=${{ needs.setup.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.setup.outputs.version }}
type=semver,pattern={{major}},value=${{ needs.setup.outputs.version }}
type=raw,value=dev,priority=200,enable={{is_default_branch}}
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -240,3 +284,11 @@ jobs:
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
needs: [setup, build, merge]
if: ${{ always() && contains(needs.*.result, 'failure') }}
uses: ./.github/workflows/email-notification.yml
secrets: inherit
with:
subject: "Docker build failed"
body: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+38
View File
@@ -0,0 +1,38 @@
name: email-notification
on:
workflow_call:
inputs:
subject:
type: string
required: true
body:
type: string
required: true
from:
type: string
default: Github CI <github-ci@openproject.com>
to:
type: string
default: operations@openproject.com
secrets:
OPS_MAIL_SMTP_TOKEN:
required: true
jobs:
notify:
name: Notify
runs-on: ubuntu-latest
steps:
- name: Send mail
uses: dawidd6/action-send-mail@v3
with:
subject: ${{ inputs.subject }}
body: ${{ inputs.body }}
from: ${{ inputs.from }}
to: ${{ inputs.to }}
secure: false
server_port: 587
server_address: smtp.postmarkapp.com
username: PM-T-outbound-bw6Xf6uoKnU76g1RcuydgZ
password: ${{secrets.OPS_MAIL_SMTP_TOKEN }}
+4 -8
View File
@@ -2,11 +2,6 @@ name: rubocop
on:
pull_request:
branches:
- dev
- release/*
paths:
- '**.rb'
jobs:
rubocop:
@@ -15,11 +10,12 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
fetch-depth: 2 # we are comparing PR merge head with base
- uses: ruby/setup-ruby@v1
- uses: opf/action-rubocop@v2
- uses: opf/action-rubocop@master
with:
github_token: ${{ secrets.github_token }}
rubocop_version: gemfile
rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile
rubocop_extensions: rubocop-inflector:gemfile rubocop-performance:gemfile rubocop-rails:gemfile rubocop-rspec:gemfile
reporter: github-pr-check
only_changed: true
+2 -2
View File
@@ -44,9 +44,9 @@ jobs:
uses: runs-on/cache@v4
with:
path: cache/bundle
key: gem-${{ hashFiles('Gemfile.lock') }}
key: gem-bookworm-${{ hashFiles('.ruby-version') }}-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
gem-
gem-bookworm-${{ hashFiles('.ruby-version') }}-
- name: Cache NPM
uses: runs-on/cache@v4
with:
+1 -1
View File
@@ -22,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:
+1 -1
View File
@@ -1 +1 @@
3.2.3
3.3.1
+16 -13
View File
@@ -36,7 +36,7 @@ ruby File.read(".ruby-version").strip
gem "actionpack-xml_parser", "~> 2.0.0"
gem "activemodel-serializers-xml", "~> 1.0.1"
gem "activerecord-import", "~> 1.6.0"
gem "activerecord-import", "~> 1.7.0"
gem "activerecord-session_store", "~> 2.1.0"
gem "ox"
gem "rails", "~> 7.1.3"
@@ -46,11 +46,11 @@ gem "ffi", "~> 1.15"
gem "rdoc", ">= 2.4.2"
gem "doorkeeper", "~> 5.6.6"
gem "doorkeeper", "~> 5.7.0"
# 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"
@@ -83,7 +83,7 @@ gem "htmldiff"
gem "stringex", "~> 2.8.5"
# CommonMark markdown parser with GFM extension
gem "commonmarker", "~> 1.0.3"
gem "commonmarker", "~> 1.1.3"
# HTML pipeline for transformations on text formatter output
# such as sanitization or additional features
@@ -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,6 +159,7 @@ 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"
@@ -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"
@@ -181,11 +184,11 @@ gem "sprockets-rails", "~> 3.4.2"
gem "puma", "~> 6.4"
gem "puma-plugin-statsd", "~> 2.0"
gem "rack-timeout", "~> 0.6.3", require: "rack/timeout/base"
gem "rack-timeout", "~> 0.7.0", 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.19.0"
gem "selenium-webdriver", "~> 4.20"
gem "fuubar", "~> 2.5.0"
gem "timecop", "~> 0.9.0"
@@ -381,6 +384,6 @@ gemfiles.each do |file|
send(:eval_gemfile, file) if File.readable?(file)
end
gem "openproject-octicons", "~>19.10.0"
gem "openproject-octicons_helper", "~>19.10.0"
gem "openproject-primer_view_components", "~>0.29.0"
gem "openproject-octicons", "~>19.13.0"
gem "openproject-octicons_helper", "~>19.13.0"
gem "openproject-primer_view_components", "~>0.32.1"
+160 -162
View File
@@ -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.75.0)
messagebird-rest (~> 1.4.2)
rotp (~> 6.1)
webauthn (~> 3.0)
@@ -225,36 +225,36 @@ PATH
GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
actioncable (7.1.3.2)
actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2)
Ascii85 (1.1.1)
actioncable (7.1.3.3)
actionpack (= 7.1.3.3)
activesupport (= 7.1.3.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.3.2)
actionpack (= 7.1.3.2)
activejob (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
actionmailbox (7.1.3.3)
actionpack (= 7.1.3.3)
activejob (= 7.1.3.3)
activerecord (= 7.1.3.3)
activestorage (= 7.1.3.3)
activesupport (= 7.1.3.3)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.3.2)
actionpack (= 7.1.3.2)
actionview (= 7.1.3.2)
activejob (= 7.1.3.2)
activesupport (= 7.1.3.2)
actionmailer (7.1.3.3)
actionpack (= 7.1.3.3)
actionview (= 7.1.3.3)
activejob (= 7.1.3.3)
activesupport (= 7.1.3.3)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
actionpack (7.1.3.2)
actionview (= 7.1.3.2)
activesupport (= 7.1.3.2)
actionpack (7.1.3.3)
actionview (= 7.1.3.3)
activesupport (= 7.1.3.3)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -265,33 +265,33 @@ GEM
actionpack-xml_parser (2.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
actiontext (7.1.3.2)
actionpack (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
actiontext (7.1.3.3)
actionpack (= 7.1.3.3)
activerecord (= 7.1.3.3)
activestorage (= 7.1.3.3)
activesupport (= 7.1.3.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.3.2)
activesupport (= 7.1.3.2)
actionview (7.1.3.3)
activesupport (= 7.1.3.3)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.3.2)
activesupport (= 7.1.3.2)
activejob (7.1.3.3)
activesupport (= 7.1.3.3)
globalid (>= 0.3.6)
activemodel (7.1.3.2)
activesupport (= 7.1.3.2)
activemodel (7.1.3.3)
activesupport (= 7.1.3.3)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (7.1.3.2)
activemodel (= 7.1.3.2)
activesupport (= 7.1.3.2)
activerecord (7.1.3.3)
activemodel (= 7.1.3.3)
activesupport (= 7.1.3.3)
timeout (>= 0.4.0)
activerecord-import (1.6.0)
activerecord-import (1.7.0)
activerecord (>= 4.2)
activerecord-nulldb-adapter (1.0.1)
activerecord (>= 5.2.0, < 7.2)
@@ -302,13 +302,13 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 4)
railties (>= 6.1)
activestorage (7.1.3.2)
actionpack (= 7.1.3.2)
activejob (= 7.1.3.2)
activerecord (= 7.1.3.2)
activesupport (= 7.1.3.2)
activestorage (7.1.3.3)
actionpack (= 7.1.3.3)
activejob (= 7.1.3.3)
activerecord (= 7.1.3.3)
activesupport (= 7.1.3.3)
marcel (~> 1.0)
activesupport (7.1.3.2)
activesupport (7.1.3.3)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -331,7 +331,7 @@ GEM
airbrake-ruby (6.2.2)
rbtree3 (~> 0.6)
android_key_attestation (0.3.0)
appsignal (3.7.0)
appsignal (3.7.5)
rack
ast (2.4.2)
attr_required (1.0.2)
@@ -341,29 +341,29 @@ GEM
activerecord (>= 4.0.0, < 7.2)
awrence (1.2.1)
aws-eventstream (1.3.0)
aws-partitions (1.916.0)
aws-sdk-core (3.192.1)
aws-partitions (1.934.0)
aws-sdk-core (3.196.1)
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.79.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (1.82.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.147.0)
aws-sdk-core (~> 3, >= 3.192.0)
aws-sdk-s3 (1.151.0)
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.75.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2)
axe-core-api (4.9.0)
axe-core-api (4.9.1)
dumb_delegator
virtus
axe-core-rspec (4.9.0)
axe-core-api
axe-core-rspec (4.9.1)
axe-core-api (= 4.9.1)
dumb_delegator
virtus
axiom-types (0.1.1)
@@ -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
@@ -420,7 +420,7 @@ GEM
descendants_tracker (~> 0.0.1)
color_conversion (0.1.1)
colored2 (4.0.0)
commonmarker (1.0.4)
commonmarker (1.1.3)
rb_sys (~> 0.9)
compare-xml (0.66)
nokogiri (~> 1.8)
@@ -436,6 +436,7 @@ GEM
crass (1.0.6)
css_parser (1.17.1)
addressable
csv (3.3.0)
cuprite (0.15)
capybara (~> 3.0)
ferrum (~> 0.14.0)
@@ -457,11 +458,11 @@ GEM
disposable (0.6.3)
declarative (>= 0.0.9, < 1.0.0)
representable (>= 3.1.1, < 4)
doorkeeper (5.6.9)
doorkeeper (5.7.0)
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)
@@ -528,7 +529,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8)
ffi (1.16.3)
ffi (1.17.0)
flamegraph (0.9.5)
fog-aws (3.22.0)
fog-core (~> 2.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)
@@ -570,7 +571,7 @@ GEM
fugit (>= 1.1)
railties (>= 6.0.0)
thor (>= 0.14.1)
google-apis-core (0.14.1)
google-apis-core (0.15.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.1, < 3.a)
@@ -578,8 +579,8 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-gmail_v1 (0.39.0)
google-apis-core (>= 0.14.0, < 2.a)
google-apis-gmail_v1 (0.41.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
googleauth (1.11.0)
@@ -614,17 +615,16 @@ GEM
http-2-next (1.0.3)
http_parser.rb (0.6.0)
httpclient (2.8.3)
httpx (1.2.4)
httpx (1.2.5)
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)
@@ -667,16 +667,16 @@ 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.10)
lefthook (1.6.14)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -693,7 +693,7 @@ GEM
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lookbook (2.2.2)
lookbook (2.3.1)
activemodel
css_parser
htmlbeautifier (~> 1.3)
@@ -719,11 +719,11 @@ GEM
method_source (1.1.0)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2024.0305)
mime-types-data (3.2024.0507)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
minitest (5.22.3)
mini_portile2 (2.8.6)
minitest (5.23.1)
msgpack (1.7.2)
multi_json (1.15.0)
mustermann (3.0.0)
@@ -733,7 +733,7 @@ GEM
mutex_m (0.2.0)
net-http (0.4.1)
uri
net-imap (0.4.10)
net-imap (0.4.11)
date
net-protocol
net-ldap (0.19.0)
@@ -743,8 +743,8 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.1)
nokogiri (1.16.4)
nio4r (2.7.3)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.3)
@@ -767,15 +767,15 @@ GEM
validate_email
validate_url
webfinger (~> 2.0)
openproject-octicons (19.10.0)
openproject-octicons_helper (19.10.0)
openproject-octicons (19.13.0)
openproject-octicons_helper (19.13.0)
actionview
openproject-octicons (= 19.10.0)
openproject-octicons (= 19.13.0)
railties
openproject-primer_view_components (0.29.0)
openproject-primer_view_components (0.32.1)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
openproject-octicons (>= 19.9.0)
openproject-octicons (>= 19.12.0)
view_component (>= 3.1, < 4.0)
openproject-token (4.0.0)
activemodel
@@ -788,12 +788,12 @@ GEM
activerecord (>= 6.1)
request_store (~> 1.4)
parallel (1.24.0)
parallel_tests (4.7.0)
parallel_tests (4.7.1)
parallel
parser (3.3.0.5)
parser (3.3.2.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)
@@ -843,7 +842,7 @@ GEM
puma-plugin-statsd (2.6.0)
puma (>= 5.0, < 7)
raabro (1.4.0)
racc (1.7.3)
racc (1.8.0)
rack (2.2.9)
rack-accept (0.4.5)
rack (>= 0.4)
@@ -867,27 +866,27 @@ GEM
rack (< 3)
rack-test (2.1.0)
rack (>= 1.3)
rack-timeout (0.6.3)
rack-timeout (0.7.0)
rack_session_access (0.2.0)
builder (>= 2.0.0)
rack (>= 1.0.0)
rackup (1.0.0)
rack (< 3)
webrick
rails (7.1.3.2)
actioncable (= 7.1.3.2)
actionmailbox (= 7.1.3.2)
actionmailer (= 7.1.3.2)
actionpack (= 7.1.3.2)
actiontext (= 7.1.3.2)
actionview (= 7.1.3.2)
activejob (= 7.1.3.2)
activemodel (= 7.1.3.2)
activerecord (= 7.1.3.2)
activestorage (= 7.1.3.2)
activesupport (= 7.1.3.2)
rails (7.1.3.3)
actioncable (= 7.1.3.3)
actionmailbox (= 7.1.3.3)
actionmailer (= 7.1.3.3)
actionpack (= 7.1.3.3)
actiontext (= 7.1.3.3)
actionview (= 7.1.3.3)
activejob (= 7.1.3.3)
activemodel (= 7.1.3.3)
activerecord (= 7.1.3.3)
activestorage (= 7.1.3.3)
activesupport (= 7.1.3.3)
bundler (>= 1.15.0)
railties (= 7.1.3.2)
railties (= 7.1.3.3)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -902,9 +901,9 @@ GEM
rails-i18n (7.0.9)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (7.1.3.2)
actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2)
railties (7.1.3.3)
actionpack (= 7.1.3.3)
activesupport (= 7.1.3.3)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -913,32 +912,33 @@ GEM
rainbow (3.1.1)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
rb-inotify (0.11.1)
ffi (~> 1.0)
rb_sys (0.9.94)
rb_sys (0.9.97)
rbtree3 (0.7.1)
rdoc (6.6.3.1)
rdoc (6.7.0)
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.1)
redis (5.2.0)
redis-client (>= 0.22.0)
redis-client (0.22.2)
connection_pool
regexp_parser (2.9.0)
reline (0.5.1)
regexp_parser (2.9.2)
reline (0.5.7)
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)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.2.6)
rexml (3.2.8)
strscan (>= 3.0.9)
rinku (2.0.6)
roar (1.2.0)
representable (~> 3.1)
@@ -953,7 +953,7 @@ GEM
rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.0)
rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.2)
@@ -967,7 +967,7 @@ GEM
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.13.1)
rubocop (1.63.3)
rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -978,8 +978,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)
@@ -991,12 +991,12 @@ GEM
rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.24.1)
rubocop-rails (2.25.0)
activesupport (>= 4.2.0)
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)
@@ -1024,9 +1024,9 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
secure_headers (6.5.0)
selenium-devtools (0.123.0)
selenium-devtools (0.125.0)
selenium-webdriver (~> 4.2)
selenium-webdriver (4.19.0)
selenium-webdriver (4.21.1)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@@ -1040,13 +1040,12 @@ GEM
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simpleidn (0.2.1)
unf (~> 0.1.4)
simpleidn (0.2.3)
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,12 +1058,13 @@ 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)
stringex (2.8.6)
stringio (3.1.0)
strscan (3.1.0)
structured_warnings (0.4.0)
svg-graph (2.2.2)
swd (2.0.3)
@@ -1072,7 +1072,7 @@ GEM
attr_required (>= 0.0.5)
faraday (~> 2.0)
faraday-follow_redirects
sys-filesystem (1.4.4)
sys-filesystem (1.4.5)
ffi (~> 1.1)
table_print (1.5.7)
terminal-table (3.0.2)
@@ -1081,15 +1081,14 @@ GEM
text-hyphen (1.5.0)
thor (1.3.1)
thread_safe (0.3.6)
timecop (0.9.8)
timecop (0.9.9)
timeout (0.4.1)
tpm-key_attestation (0.12.0)
bindata (~> 2.4)
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)
@@ -1101,9 +1100,6 @@ GEM
tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.9.1)
unicode-display_width (2.5.0)
uri (0.13.0)
validate_email (0.1.6)
@@ -1138,7 +1134,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
webmock (3.23.0)
webmock (3.23.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -1154,7 +1150,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.36)
zeitwerk (2.6.13)
zeitwerk (2.6.14)
PLATFORMS
ruby
@@ -1162,7 +1158,7 @@ PLATFORMS
DEPENDENCIES
actionpack-xml_parser (~> 2.0.0)
activemodel-serializers-xml (~> 1.0.1)
activerecord-import (~> 1.6.0)
activerecord-import (~> 1.7.0)
activerecord-nulldb-adapter (~> 1.0.0)
activerecord-session_store (~> 2.1.0)
acts_as_list (~> 1.1.0)
@@ -1178,19 +1174,20 @@ 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)
colored2
commonmarker (~> 1.0.3)
commonmarker (~> 1.1.3)
compare-xml (~> 0.66)
costs!
csv (~> 3.3)
cuprite (~> 0.15.0)
daemons
dalli (~> 3.2.0)
@@ -1199,7 +1196,7 @@ DEPENDENCIES
debug
deckar01-task_list (~> 2.3.1)
disposable (~> 0.6.2)
doorkeeper (~> 5.6.6)
doorkeeper (~> 5.7.0)
dotenv-rails
dry-container
email_validator (~> 2.2.3)
@@ -1234,7 +1231,7 @@ 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!
@@ -1265,10 +1262,10 @@ DEPENDENCIES
openproject-job_status!
openproject-ldap_groups!
openproject-meeting!
openproject-octicons (~> 19.10.0)
openproject-octicons_helper (~> 19.10.0)
openproject-octicons (~> 19.13.0)
openproject-octicons_helper (~> 19.13.0)
openproject-openid_connect!
openproject-primer_view_components (~> 0.29.0)
openproject-primer_view_components (~> 0.32.1)
openproject-recaptcha!
openproject-reporting!
openproject-storages!
@@ -1297,14 +1294,14 @@ DEPENDENCIES
rack-mini-profiler
rack-protection (~> 3.2.0)
rack-test (~> 2.1.0)
rack-timeout (~> 0.6.3)
rack-timeout (~> 0.7.0)
rack_session_access
rails (~> 7.1.3)
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 +1322,7 @@ DEPENDENCIES
sanitize (~> 6.1.0)
secure_headers (~> 6.5.0)
selenium-devtools
selenium-webdriver (~> 4.19.0)
selenium-webdriver (~> 4.20)
semantic (~> 1.6.1)
shoulda-context (~> 2.0)
shoulda-matchers (~> 6.0)
@@ -1343,6 +1340,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 +1355,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
+1
View File
@@ -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"
@@ -0,0 +1,51 @@
<%#-- 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")])
header.with_tab_nav(label: nil) do |tab_nav|
tab_nav.with_tab(selected: @selected == 1, href: admin_settings_attachments_path) do |tab|
tab.with_text { t("settings.general") }
end
tab_nav.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?)
tab_nav.with_tab(selected: @selected == 3, href: admin_quarantined_attachments_path) do |tab|
tab.with_text { t(:"antivirus_scan.quarantined_attachments.title") }
end
end
end
end %>
@@ -1,8 +1,8 @@
# frozen_string_literal: true
# -- copyright
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2023 the OpenProject GmbH
# 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.
@@ -26,17 +26,14 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
#++
module Admin::VirusScanning
# rubocop:disable OpenProject/AddPreviewForViewComponent
class IndexPageHeaderComponent < ApplicationComponent
include ApplicationHelper
def breadcrumb_items
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_attachments_path, text: t("attributes.attachments") },
t("settings.antivirus.title")]
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
@@ -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
%>
@@ -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 %>
@@ -6,6 +6,7 @@
<div class="op-filters-header">
<%= render(Primer::Beta::Button.new(
scheme: :secondary,
disabled:,
data: { 'filters-target': 'filterFormToggle',
'action': 'filters#toggleDisplayFilters',
'test-selector': 'filter-component-toggle' })) do %>
+1
View File
@@ -30,6 +30,7 @@
class FiltersComponent < ApplicationComponent
options :query
options :disabled
options output_format: "params"
renders_many :buttons, lambda { |**system_arguments|
@@ -1,7 +1,7 @@
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { page_title }
header.with_breadcrumbs(breadcrumb_items)
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
header.with_action_button(scheme: :primary,
mobile_icon: :plus,
+1 -1
View File
@@ -43,7 +43,7 @@ module Members
sortable_columns :name, :mail, :status
def apply_sort(model)
apply_member_scopes super(model)
apply_member_scopes super
end
def apply_member_scopes(model)
@@ -128,7 +128,7 @@ module Members
end
def apply_filters(params, query)
super(params, query)
super
filter_shares(query, params[:shared_role_id]) if params.key?(:shared_role_id)
query
@@ -4,36 +4,46 @@
# Hack to give the draggable autcompleter (ng-select) bound to the dialog
# enough height to display all options.
# This is necessary as long as ng-select does not support popovers.
style: "min-height: 430px")) do |d| %>
style: "min-height: 480px")) do |d| %>
<% d.with_header(variant: :large, mb: 3) %>
<%= render(Primer::Alpha::Dialog::Body.new(id: "op-draggable-autocomplete-container")) do %>
<%= render(Primer::Alpha::Dialog::Body.new) do %>
<%= primer_form_with(
url: projects_path,
id: COLUMN_FORM_ID,
method: :get,
data: {
controller: "params-from-query",
'application-target': "dynamic",
'params-from-query-allowed-value': '["filters", "query_id", "page", "per_page", "sortBy"]'
}) do %>
<%= helpers.angular_component_tag 'opce-draggable-autocompleter',
inputs: {
options: helpers.projects_columns_options,
selected: selected_columns,
protected: helpers.protected_projects_columns_options,
name: COLUMN_HTML_NAME,
id: 'columns-select',
inputLabel: I18n.t(:'queries.configure_view.columns.input_label'),
inputPlaceholder: I18n.t(:'queries.configure_view.columns.input_placeholder'),
dragAreaLabel: I18n.t(:'queries.configure_view.columns.drag_area_label'),
appendToComponent: true
}%>
id: QUERY_FORM_ID,
method: :get
) do |form| %>
<% helpers.projects_query_params.except(:columns, :sortBy).each do |name, value| %>
<%= hidden_field_tag name, value %>
<% end %>
<%= render(Primer::Alpha::TabPanels.new(label: "label")) do |tab_panel| %>
<% tab_panel.with_tab(selected: true, id: "tab-selects") do |tab| %>
<% tab.with_text { I18n.t("label_columns") } %>
<% tab.with_panel do %>
<%= helpers.angular_component_tag 'opce-draggable-autocompleter',
inputs: {
options: helpers.projects_columns_options,
selected: selected_columns,
protected: helpers.protected_projects_columns_options,
name: COLUMN_HTML_NAME,
id: 'columns-select',
inputLabel: I18n.t(:'queries.configure_view.columns.input_label'),
inputPlaceholder: I18n.t(:'queries.configure_view.columns.input_placeholder'),
dragAreaLabel: I18n.t(:'queries.configure_view.columns.drag_area_label'),
appendToComponent: true
}%>
<% end %>
<% end %>
<% tab_panel.with_tab(id: "tab-selects") do |tab| %>
<% tab.with_text { I18n.t("label_sort") }%>
<% tab.with_panel do %>
<%= render(Queries::SortByComponent.new(query: query, selectable_columns:)) %>
<% end%>
<% end %>
<% end %>
<% 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::ButtonComponent.new(scheme: :primary, type: :submit, form: COLUMN_FORM_ID)) { I18n.t(:button_apply) } %>
<%= render(Primer::ButtonComponent.new(scheme: :primary, type: :submit, form: QUERY_FORM_ID)) { I18n.t(:button_apply) } %>
<% end %>
<% end %>
@@ -30,11 +30,17 @@
class Projects::ConfigureViewModalComponent < ApplicationComponent
MODAL_ID = "op-project-list-configure-dialog"
COLUMN_FORM_ID = "op-project-list-configure-columns-form"
QUERY_FORM_ID = "op-project-list-configure-query-form"
COLUMN_HTML_NAME = "columns"
options :query
def selectable_columns
@selectable_columns ||= [
{ id: :lft, name: I18n.t(:label_project_hierarchy) }
] + helpers.projects_columns_options
end
def selected_columns
@selected_columns ||= query
.selects
@@ -0,0 +1,6 @@
<p class="information-section">
<%= 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)) %>
</p>
@@ -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?
@@ -2,14 +2,10 @@
id: MODAL_ID)) do |d| %>
<% d.with_header(variant: :large) %>
<% d.with_body do %>
<ul class="op-export-options"
data-controller="params-from-query"
data-application-target="dynamic"
data-params-from-query-all-anchors-value="true"
data-params-from-query-allowed-value='["query_id", "filters", "sortBy", "columns"]'>
<ul class="op-export-options">
<% helpers.supported_export_formats.each do |key| %>
<li class="op-export-options--option">
<%= link_to url_for(action: 'index', format: key),
<%= link_to projects_path(format: key, **helpers.projects_query_params.except(:page, :per_page)),
class: 'op-export-options--option-link' do %>
<%= helpers.op_icon("icon-big icon-export-#{key}") %>
<span class="op-export-options--option-label"><%= t("export.format.#{key}") %></span>
@@ -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)
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
if query_saveable?
header.with_action_text { t('lists.can_be_saved_as') }
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, projects_query_params),
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(projects_query_params)
)
end
header.with_action_menu(menu_arguments: {
@@ -31,6 +29,15 @@
"aria-label": t(:label_more),
data: { "test-selector": "project-more-dropdown-menu" }
}) do |menu|
if can_rename?
menu.with_item(
label: t('button_rename'),
href: rename_projects_query_path(query),
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
if gantt_portfolio_project_ids.any?
menu.with_item(
tag: :a,
@@ -51,20 +58,21 @@
item.with_leading_visual_icon(icon: 'tasklist')
end
if query_saveable?
menu.with_item(
if can_save?
menu_save_item(
menu:,
label: t('button_save'),
href: projects_query_path(query, projects_query_params),
method: :patch
)
end
if may_save_as?
menu_save_item(
menu:,
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
href: new_projects_query_path(projects_query_params)
)
end
menu.with_item(
@@ -82,6 +90,29 @@
end
if query.persisted?
# TODO: Remove section when the sharing modal is implemented (https://community.openproject.org/projects/openproject/work_packages/55163)
if can_publish?
if query.public?
menu.with_item(
label: t(:button_unpublish),
scheme: :danger,
href: unpublish_projects_query_path(query),
content_arguments: { data: { method: :post } }
) do |item|
item.with_leading_visual_icon(icon: 'eye-closed')
end
else
menu.with_item(
label: t(:button_publish),
scheme: :default,
href: publish_projects_query_path(query),
content_arguments: { data: { method: :post } }
) do |item|
item.with_leading_visual_icon(icon: 'eye')
end
end
end
menu.with_item(
label: t(:button_delete),
scheme: :danger,
@@ -91,29 +122,28 @@
end
end
end
else
end
%>
<%= 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,
url: @query.new_record? ? projects_queries_path(projects_query_params) : projects_query_path(@query, projects_query_params),
scope: 'query',
data: {
controller: "params-from-query",
'application-target': "dynamic",
'params-from-query-allowed-value': '["filters", "columns"]'
},
id: 'project-save-form') do |f|
render(Queries::Projects::Create.new(f))
render(
Queries::Projects::Form.new(
f,
cancel_url: projects_path(**projects_query_params, **{ query_id: query.id }.compact)
)
)
end
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 %>
@@ -31,16 +31,15 @@
class Projects::IndexPageHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include Primer::FetchOrFallbackHelper
include Menus::ProjectsHelper
attr_accessor :current_user,
:query,
:state,
:params
STATE_DEFAULT = :show
STATE_EDIT = :edit
STATE_OPTIONS = [STATE_DEFAULT, STATE_EDIT].freeze
STATE_OPTIONS = %i[show edit rename].freeze
delegate :projects_query_params, to: :helpers
def initialize(current_user:, query:, params:, state: :show)
super
@@ -69,8 +68,38 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
query.name || t(:label_project_plural)
end
def query_saveable?
current_user.logged? && query.name.blank?
def may_save_as? = current_user.logged?
def can_save_as? = may_save_as? && query.changed?
def can_save?
return false unless current_user.logged?
return false unless query.persisted?
return false unless query.changed?
if query.public?
current_user.allowed_globally?(:manage_public_project_queries)
else
query.user == current_user
end
end
def can_rename?
return false unless current_user.logged?
return false unless query.persisted?
return false if query.changed?
if query.public?
current_user.allowed_globally?(:manage_public_project_queries)
else
query.user == current_user
end
end
def can_publish?
OpenProject::FeatureDecisions.project_list_sharing_active? &&
current_user.allowed_globally?(:manage_public_project_queries) &&
query.persisted?
end
def show_state?
@@ -87,14 +116,50 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
def current_breadcrumb_element
return page_title if query.name.blank?
current_object = first_level_menu_items.find do |section|
section.children.any?(&:selected)
end
if current_object && current_object.header.present?
I18n.t("menus.breadcrumb.nested_element", section_header: current_object.header, title: query.name).html_safe
if current_section && current_section.header.present?
I18n.t("menus.breadcrumb.nested_element", section_header: current_section.header, title: query.name).html_safe
else
page_title
end
end
def current_section
return @current_section if defined?(@current_section)
projects_menu = Menus::Projects.new(controller_path:, params:, current_user:)
@current_section = projects_menu.first_level_menu_items.find { |section| section.children.any?(&:selected) }
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: }
) 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: }
}
) do |item|
item.with_leading_visual_icon(icon: :"op-save")
end
end
end
+80 -38
View File
@@ -45,9 +45,24 @@ module Projects
end
def favored
if favored_project_ids.include?(project.id)
render(Primer::Beta::Octicon.new(icon: "star-fill", classes: "op-primer--star-icon", "aria-label": I18n.t(:label_favoured)))
end
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)
@@ -146,13 +161,17 @@ module Projects
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}"
@@ -178,7 +197,7 @@ module Projects
def additional_css_class(column)
if column.attribute == :name
"project--hierarchy #{project.archived? ? 'archived' : ''}"
elsif [:status_explanation, :description].include?(column.attribute)
elsif %i[status_explanation description].include?(column.attribute)
"project-long-text-container"
elsif custom_field_column?(column)
cf = column.custom_field
@@ -188,36 +207,29 @@ module Projects
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:) if icon
end
]
end
end
end
@@ -225,12 +237,42 @@ module Projects
@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)
{
@@ -260,7 +302,7 @@ module Projects
scheme: :default,
icon: :check,
label: I18n.t(:label_project_activity),
href: project_activity_index_path(project, event_types: ["project_attributes"]),
href: project_activity_index_path(project, event_types: ["project_attributes"])
}
end
end
@@ -275,7 +317,7 @@ module Projects
data: {
confirm: t("project.archive.are_you_sure", name: project.name),
method: :post
},
}
}
end
end
@@ -298,7 +340,7 @@ module Projects
scheme: :default,
icon: :copy,
label: I18n.t(:button_copy),
href: copy_project_path(project),
href: copy_project_path(project)
}
end
end
@@ -309,7 +351,7 @@ module Projects
scheme: :danger,
icon: :trash,
label: I18n.t(:button_delete),
href: confirm_destroy_project_path(project),
href: confirm_destroy_project_path(project)
}
end
end
@@ -319,7 +361,7 @@ module Projects
end
def custom_field_column?(column)
column.is_a?(Queries::Projects::Selects::CustomField)
column.is_a?(::Queries::Projects::Selects::CustomField)
end
end
end
@@ -0,0 +1,6 @@
.op-project-row-component
&--favorite
opacity: 0
&:hover &--favorite
opacity: 1
@@ -1,6 +0,0 @@
<p class="information-section">
<%= helpers.op_icon('icon-info1') %>
<%= t(:label_projects_storage_information,
count: Project.count,
storage: number_to_human_size(Project.total_projects_size, precision: 2)) %>
</p>
@@ -26,6 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<div class="project-list-page--table">
<div class="generic-table--flex-container">
<div class="generic-table--container <%= container_class %>">
@@ -43,11 +44,7 @@ See COPYRIGHT and LICENSE files for more details.
<% if column.attribute == :hierarchy %>
<th id="project-table--hierarchy-header">
<div class="generic-table--sort-header-outer generic-table--sort-header-outer_no-highlighting">
<div class="generic-table--sort-header"
data-controller="params-from-query"
data-application-target="dynamic"
data-params-from-query-all-anchors-value="true"
data-params-from-query-allowed-value='["query_id", "per_page", "filters", "columns"]'>
<div class="generic-table--sort-header">
<%= content_tag :a,
helpers.op_icon("icon-hierarchy"),
href: href_only_when_not_sort_lft,
@@ -65,7 +62,7 @@ See COPYRIGHT and LICENSE files for more details.
<div class="generic-table--sort-header">
<span>
<% if column.attribute == :favored %>
<%= render(Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, "aria-label": I18n.t(:label_favoured))) %>
<%= render(Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, ml: 2, "aria-label": I18n.t(:label_favorite))) %>
<% else %>
<%= column.caption %>
<% end %>
@@ -84,7 +81,7 @@ See COPYRIGHT and LICENSE files for more details.
<tbody>
<% if rows.empty? %>
<tr class="generic-table--empty-row">
<td colspan="<%= headers.length + 1 %>"><%= empty_row_message %></td>
<td colspan="<%= columns.length + 1 %>"><%= empty_row_message %></td>
</tr>
<% end %>
<%= render_collection rows %>
@@ -101,10 +98,5 @@ See COPYRIGHT and LICENSE files for more details.
</div>
<% if paginated? %>
<div data-controller="params-from-query"
data-application-target="dynamic"
data-params-from-query-all-anchors-value="true"
data-params-from-query-allowed-value='["query_id", "columns"]'>
<%= helpers.pagination_links_full model, { blocked_url_params: [:query_id, :columns] } %>
</div>
<%= helpers.pagination_links_full model, allowed_params: %i[query_id filters columns sortBy] %>
<% end %>
+10 -14
View File
@@ -58,14 +58,14 @@ module Projects
##
# The project sort by is handled differently
def build_sort_header(column, options)
helpers.projects_sort_header_tag(column, options.merge(param: :json))
helpers.projects_sort_header_tag(column, **options, param: :json)
end
# We don't return the project row
# but the [project, level] array from the helper
def rows
@rows ||= begin
projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a } # rubocop:disable Lint/ToEnumArguments
projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a }
instance_exec(model, &projects_enumerator)
end
end
@@ -90,20 +90,16 @@ module Projects
def href_only_when_not_sort_lft
unless sorted_by_lft?
projects_path(sortBy: JSON::dump([["lft", "asc"]]))
projects_path(
sortBy: JSON.dump([%w[lft asc]]),
**helpers.projects_query_params.slice(*helpers.projects_query_param_names_for_sort)
)
end
end
def order_options(select)
{
caption: select.caption,
data:
{
controller: "params-from-query",
"application-target": "dynamic",
"params-from-query-allowed-value": '["query_id"]',
"params-from-query-all-anchors-value": "true"
}
caption: select.caption
}
end
@@ -113,10 +109,10 @@ module Projects
def columns
@columns ||= begin
columns = query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) }
columns = query.selects.reject { |select| select.is_a?(::Queries::Selects::NotExistingSelect) }
index = columns.index { |column| column.attribute == :name }
columns.insert(index, Queries::Projects::Selects::Default.new(:hierarchy)) if index
columns.insert(index, ::Queries::Projects::Selects::Default.new(:hierarchy)) if index
columns
end
@@ -154,7 +150,7 @@ module Projects
end
def favored_project_ids
@favored_projects ||= Favorite.where(user: current_user, favored_type: 'Project').pluck(:favored_id)
@favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id)
end
def sorted_by_lft?
@@ -0,0 +1,12 @@
<%= render(Primer::Beta::Heading.new(tag: :h5)) { I18n.t('queries.configure_view.sort_by.automatic.heading') } %>
<%= render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { I18n.t('queries.configure_view.sort_by.automatic.description', plural: queried_model_name.plural) } %>
<div data-controller="sort-by-config" data-application-target="dynamic">
<%= hidden_field_tag :sortBy, current_orders, data: { "sort-by-config-target" => "sortByField" } %>
<%= render(Primer::OpenProject::FlexLayout.new(data: { "sort-by-config-target" => "inputRowContainer" })) do |layout| %>
<% order_limit.times do |i| %>
<% layout.with_row(mt: 3, data: { "sort-by-config-target" => "inputRow" }) do %>
<%= render(Queries::SortByFieldComponent.new(order: query.orders[i], available_orders:, index: i)) %>
<% end %>
<% end %>
<% end %>
</div>
@@ -0,0 +1,57 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# 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.
#
# 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::SortByComponent < ApplicationComponent
options :query
options :selectable_columns
def current_orders
JSON.dump(query.orders.map { |order| [order.attribute, order.direction] })
end
def order_limit
3
end
def queried_model_name
query.class.model.model_name
end
def available_orders
@available_orders ||= begin
all_order_keys = ::Queries::Register.orders[query.class]&.map(&:key)
# Keys from the order can be symbols, strings or regexes
selectable_columns.select do |column_option|
all_order_keys.any? { |order_key| order_key === column_option[:id] }
end
end
end
end
@@ -0,0 +1,14 @@
<%= render(Primer::OpenProject::FlexLayout.new(test_selector: 'sort-by-field')) do |flex| %>
<% flex.with_column(flex: 1) do %>
<%#- We are just using the classes of the primer component here, because when using the primer component, we cannot detach the input element from the form %>
<%#- The form="none" adds the input to a nonexistant form (as we do not have one with the ID="none" and thus the fields to not get appended to the query string %>
<%= select_tag 'sort_field', select_options, prompt: "-", form: "none", class: "FormControl-select FormControl-medium FormControl--fullWidth", data: { action: "change->sort-by-config#fieldChanged"} %>
<% end %>
<% flex.with_column do %>
<%= render(Primer::Alpha::SegmentedControl.new("aria-label": "Sort order", hide_labels: true, ml: 3)) do |sort_buttons| %>
<%#- The segmented control actions need to be included here as well as they do the visual styling of the currently selected option, just setting our action would remove their action %>
<% sort_buttons.with_item(icon: "sort-asc", label: "sort ascending", selected: order_asc?, data: { direction: 'asc', action: "click:segmented-control#select click->sort-by-config#fieldChanged"}) %>
<% sort_buttons.with_item(icon: "sort-desc", label: "sort descending", selected: order_desc?, data: { direction: 'desc', action: "click:segmented-control#select click->sort-by-config#fieldChanged"}) %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,54 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# 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.
#
# 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::SortByFieldComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
options :order,
:available_orders,
:index
def select_options
options_for_select(
available_orders.map { |order| [order[:name], order[:id]] },
order&.attribute
)
end
def active?
order.present? && available_orders.any? { |o| order.attribute.to_sym == o[:id] }
end
def order_asc?
active? && order&.direction == :asc
end
def order_desc?
active? && order&.direction == :desc
end
end
+2 -2
View File
@@ -33,8 +33,8 @@
class RowComponent < ApplicationComponent
attr_reader :table
def initialize(row:, table:, **options)
super(row, **options)
def initialize(row:, table:, **)
super(row, **)
@table = table
end
@@ -31,5 +31,20 @@ See COPYRIGHT and LICENSE files for more details.
header.with_title { @custom_field.name }
header.with_description { t("settings.project_attributes.edit.description") }
header.with_breadcrumbs(breadcrumbs_items)
header.with_tab_nav(label: nil, test_selector: :project_attribute_detail_header) do |tab_nav|
tab_nav.with_tab(
selected: project_custom_field_edit_selected?,
href: edit_admin_settings_project_custom_field_path(@custom_field)
) do |tab|
tab.with_text { t(:label_details) }
end
tab_nav.with_tab(
selected: project_custom_field_project_mappings_selected?,
href: project_mappings_admin_settings_project_custom_field_path(@custom_field)
) do |tab|
tab.with_text { I18n.t(:label_project_plural) }
end
end
end
%>
@@ -29,18 +29,31 @@
module Settings
module ProjectCustomFields
class EditFormHeaderComponent < ApplicationComponent
def initialize(custom_field:)
super
TAB_NAVS = %i[
project_custom_field_edit
project_custom_field_project_mappings
].freeze
def initialize(custom_field:, selected:)
selected = selected.to_sym
raise "selected must be one of the following: #{TAB_NAVS.join(', ')}" unless TAB_NAVS.include?(selected)
super
@custom_field = custom_field
@selected = selected
end
TAB_NAVS.each do |tab_nav|
define_method(:"#{tab_nav}_selected?") do
@selected == tab_nav
end
end
def breadcrumbs_items
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
@custom_field.name
]
@custom_field.name]
end
end
end
@@ -32,7 +32,7 @@ module Settings
def breadcrumb_items
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
t("settings.project_attributes.new.heading")]
end
end
@@ -27,23 +27,16 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t("project_module_storages") %>
<% if ::Storages::Storage.any? %>
<%= toolbar title: t("storages.page_titles.project_settings.index") do %>
<li class="toolbar-item">
<%= link_to new_project_settings_project_storage_path,
{ class: 'button -primary',
aria: { label: t(:'storages.label_new_storage') },
title: t(:'storages.label_new_storage') } do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= ::Storages::Storage.model_name.human %></span>
<% end %>
</li>
<%= component_wrapper(tag: "tr", class: row_css_class, data: { turbo: true }) do %>
<% columns.each do |column| %>
<td class="<%= column_css_class(column) %>">
<%= column_value(column) %>
</td>
<% end %>
<%= render(::Storages::ProjectStorages::TableComponent.new(rows: @project_storages)) %>
<% else %>
<%= render partial: '/storages/project_settings/toast_no_storage_set_up' %>
<td class="buttons">
<% button_links.each do |link| %>
<%= link %>
<% end %>
</td>
<% end %>
@@ -0,0 +1,62 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 Settings
module ProjectCustomFields
module ProjectCustomFieldMapping
class RowComponent < Projects::RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
include OpTurbo::Streamable
def wrapper_uniq_by
"project-#{project.id}"
end
def more_menu_items
@more_menu_items ||= [more_menu_detach_project].compact
end
private
def more_menu_detach_project
if User.current.admin
{
scheme: :default,
icon: nil,
label: I18n.t("projects.settings.project_custom_fields.actions.deactivate_for_project"),
href: unlink_admin_settings_project_custom_field_path(
id: @table.params[:custom_field].id,
project_custom_field_project_mapping: { project_id: model.first.id }
),
data: { turbo_method: :delete }
}
end
end
end
end
end
end
@@ -0,0 +1,32 @@
<%#-- 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.
++#%>
<%= component_wrapper(tag: "div", data: { turbo: true }) do %>
<%= render_parent %>
<% end %>
@@ -0,0 +1,41 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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 Settings
module ProjectCustomFields
module ProjectCustomFieldMapping
class TableComponent < Projects::TableComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
include OpTurbo::Streamable
def columns
@columns ||= query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) }
end
end
end
end
end
+1 -1
View File
@@ -40,7 +40,7 @@ See COPYRIGHT and LICENSE files for more details.
<tr>
<% headers.each do |name, options| %>
<% if sortable_column?(name) %>
<%= helpers.sort_header_tag name, options %>
<%= helpers.sort_header_tag(name, **options) %>
<% else %>
<th>
<div class="generic-table--sort-header-outer">
+1 -1
View File
@@ -59,7 +59,7 @@ class UserFilterComponent < IndividualPrincipalBaseFilterComponent
protected
def apply_filters(params, query)
super(params, query)
super
filter_status query, status_param(params)
query
@@ -36,7 +36,11 @@ module WorkPackages
FIELD_MAP = {
"estimatedTime" => :estimated_hours,
"remainingTime" => :remaining_hours
"work_package[estimated_hours]" => :estimated_hours,
"remainingTime" => :remaining_hours,
"work_package[remaining_hours]" => :remaining_hours,
"work_package[status_id]" => :status_id,
"statusId" => :status_id
}.freeze
include ApplicationHelper
@@ -44,13 +48,14 @@ module WorkPackages
include OpPrimer::ComponentHelpers
include OpenProject::StaticRouting::UrlHelpers
attr_reader :work_package, :mode, :focused_field
attr_reader :work_package, :mode, :focused_field, :touched_field_map
def initialize(work_package, focused_field: nil)
def initialize(work_package, focused_field: nil, touched_field_map: {})
super()
@work_package = work_package
@focused_field = map_field(focused_field)
@touched_field_map = touched_field_map
end
def submit_path
@@ -2,13 +2,18 @@
<%= primer_form_with(
model: work_package,
url: submit_path,
class: 'progress-form',
id: 'progress-form',
data: { 'application-target': "dynamic",
controller: "work-packages--progress--focus-field" }) do |f| %>
class: "progress-form",
id: "progress-form",
html: { autocomplete: "off" },
data: { "application-target": "dynamic",
"work-packages--progress--preview-progress-target": "form",
controller: "work-packages--progress--focus-field " \
"work-packages--progress--preview-progress " \
"work-packages--progress--touched-field-marker" }
) do |f| %>
<%= flex_layout do |modal_body| %>
<% modal_body.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do |_fields| %>
<%= render(WorkPackages::ProgressForm.new(f, work_package:, mode:, focused_field:)) %>
<%= render(WorkPackages::ProgressForm.new(f, work_package:, mode:, focused_field:, touched_field_map:)) %>
<% end %>
<% modal_body.with_row(mt: 3) do |_tooltip| %>
@@ -32,7 +32,7 @@ module WorkPackages
module Progress
module StatusBased
class ModalBodyComponent < BaseModalComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
def initialize(work_package, focused_field: nil)
def initialize(work_package, focused_field: nil, touched_field_map: {})
super
@mode = :status_based
@@ -4,11 +4,16 @@
url: submit_path,
class: "progress-form",
id: "progress-form",
html: { autocomplete: "off" },
data: { "application-target": "dynamic",
controller: "work-packages--progress--focus-field" }) do |f| %>
"work-packages--progress--preview-progress-target": "form",
controller: "work-packages--progress--focus-field " \
"work-packages--progress--preview-progress " \
"work-packages--progress--touched-field-marker" }
) do |f| %>
<%= flex_layout do |modal_body| %>
<% modal_body.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do |_fields| %>
<%= render(WorkPackages::ProgressForm.new(f, work_package:, mode:, focused_field:)) %>
<%= render(WorkPackages::ProgressForm.new(f, work_package:, mode:, focused_field:, touched_field_map:)) %>
<% end %>
<% if should_display_migration_warning? %>
@@ -33,7 +33,7 @@ module WorkPackages
module WorkBased
# rubocop:disable OpenProject/AddPreviewForViewComponent
class ModalBodyComponent < BaseModalComponent
def initialize(work_package, focused_field: nil)
def initialize(work_package, focused_field: nil, touched_field_map: {})
super
@mode = :work_based
@@ -79,7 +79,11 @@
form_with(url: work_package_shares_bulk_path(@work_package),
method: :delete,
data: { 'work-packages--share--bulk-selection-target': 'bulkForm' }) do
render(Primer::Beta::Button.new(type: :submit, scheme: :invisible)) { I18n.t('work_package.sharing.remove') }
render(Primer::Beta::IconButton.new(icon: "trash",
type: :submit,
scheme: :danger,
"aria-label": I18n.t('work_package.sharing.remove'),
test_selector: 'op-share-wp--bulk-remove'))
end
)
end
@@ -72,7 +72,7 @@ module WorkPackages
end
def permission_name(value)
options.select { |option| option[:value] == value }
options.find { |option| option[:value] == value }[:label]
end
def form_inputs(role_id)
@@ -1,5 +1,3 @@
<%
concat(render(Primer::Beta::Octicon.new(icon: 'person')))
concat(render(Primer::Beta::Text.new(ml: 2)) { I18n.t('work_package.sharing.count', count:) })
concat(render(Primer::Beta::Text.new) { I18n.t('work_package.sharing.count', count:) })
%>
@@ -28,7 +28,11 @@
user_row_grid.with_area(:remove, tag: :div) do
form_with url: work_packages_share_path(share), method: :delete do
render(Primer::Beta::Button.new(type: :submit, scheme: :link)) { I18n.t('work_package.sharing.remove') }
render(Primer::Beta::IconButton.new(icon: "trash",
type: :submit,
scheme: :danger,
"aria-label": I18n.t('work_package.sharing.remove'),
test_selector: 'op-share-wp--remove'))
end
end
end
+1 -1
View File
@@ -36,7 +36,7 @@ module CustomActions
end
def initialize(model, user = nil)
super(model, user)
super
end
attribute :name
+3 -1
View File
@@ -59,8 +59,10 @@ class DeleteContract < ModelContract
user.admin? && user.active?
when Proc
instance_exec(&permission)
when Symbol
model.project && user.allowed_in_project?(permission, model.project)
else
!model.project || user.allowed_in_project?(permission, model.project)
raise ArgumentError, "#{self.class} used without delete_permission. Set a Proc, or project-based permission symbol"
end
end
end
@@ -46,7 +46,7 @@ module ProjectCustomFieldProjectMappings
# enabling a custom field which is required happens in an after_save hook within the custom field model itself
return if model.project_custom_field.nil? || !model.project_custom_field.required?
errors.add :custom_field_id, :invalid
errors.add :custom_field_id, :cannot_delete_mapping
end
def visbile_to_user
+1
View File
@@ -35,6 +35,7 @@ module Projects
attribute :identifier
attribute :description
attribute :public
attribute :settings
attribute :active do
validate_active_present
validate_changing_active
@@ -41,17 +41,36 @@ module Queries::Projects::ProjectQueries
presence: true,
length: { maximum: 255 }
validate :user_is_current_user_and_logged_in
validate :name_select_included
validate :existing_selects
validate :user_is_logged_in
validate :allowed_to_modify_private_query
validate :allowed_to_modify_public_query
protected
def user_is_current_user_and_logged_in
unless user.logged? && user == model.user
def user_is_logged_in
unless user.logged?
errors.add :base, :error_unauthorized
end
end
def allowed_to_modify_private_query
return if model.public?
if model.user != user
errors.add :base, :can_only_be_modified_by_owner
end
end
def allowed_to_modify_public_query
return unless model.public?
unless user.allowed_globally?(:manage_public_project_queries)
errors.add :base, :need_permission_to_modify_public_query
end
end
def name_select_included
if model.selects.none? { |s| s.attribute == :name }
errors.add :selects, :name_not_included
@@ -0,0 +1,33 @@
#-- 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 Queries::Projects::ProjectQueries
class PublishContract < BaseContract
attribute :public
end
end
@@ -0,0 +1,31 @@
# -- copyright
# OpenProject is an open source project management software.
# 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.
#
# 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 Queries::Projects::ProjectQueries
class UpdateContract < BaseContract; end
end
+3 -3
View File
@@ -63,10 +63,10 @@ module Versions
true
else
case s
when 'system'
when "system"
# Only admin users can set a systemwide sharing
user.admin?
when 'hierarchy', 'tree'
when "hierarchy", "tree"
# Only users allowed to manage versions of the root project can
# set sharing to hierarchy or tree
model.project.nil? || user.allowed_in_project?(:manage_versions, model.project.root)
@@ -83,7 +83,7 @@ module Versions
if wiki
wiki.pages
else
WikiPage.where('1=0')
WikiPage.where("1=0")
end
end
@@ -35,7 +35,7 @@ module Admin
before_action :find_attachment, only: %i[destroy]
menu_item :attachment_quarantine
menu_item :attachments
def index; end
+6 -6
View File
@@ -31,7 +31,7 @@ class Admin::BackupsController < ApplicationController
include ActionView::Helpers::TagHelper
include BackupHelper
layout 'admin'
layout "admin"
before_action :check_enabled
before_action :authorize_global
@@ -65,7 +65,7 @@ class Admin::BackupsController < ApplicationController
rescue StandardError => e
token_reset_failed! e
ensure
redirect_to action: 'show'
redirect_to action: "show"
end
def delete_token
@@ -73,7 +73,7 @@ class Admin::BackupsController < ApplicationController
flash[:info] = t("backup.text_token_deleted")
redirect_to action: 'show'
redirect_to action: "show"
end
def default_breadcrumb
@@ -105,16 +105,16 @@ class Admin::BackupsController < ApplicationController
def token_reset_flash_message(token)
[
t('my.access_token.notice_reset_token', type: 'Backup'),
t("my.access_token.notice_reset_token", type: "Backup"),
content_tag(:strong, token.plain_value),
t('my.access_token.token_value_warning')
t("my.access_token.token_value_warning")
]
end
def token_reset_failed!(error)
Rails.logger.error "Failed to reset user ##{current_user.id}'s Backup token: #{error}"
flash[:error] = t('my.access_token.failed_to_reset_token', error: error.message)
flash[:error] = t("my.access_token.failed_to_reset_token", error: error.message)
end
def may_include_attachments?
@@ -39,5 +39,11 @@ module Admin::Settings
settings["apiv3_cors_origins"] = settings["apiv3_cors_origins"].split(/\r?\n/)
end
end
def extra_permitted_filters
# attachment_whitelist is normally permitted as an array parameter.
# Explicitly permit it as a string here.
[:apiv3_cors_origins]
end
end
end
@@ -28,10 +28,12 @@
module Admin::Settings
class AttachmentsSettingsController < ::Admin::SettingsController
menu_item :attachments_settings
menu_item :attachments
def default_breadcrumb
t(:"attributes.attachments")
def default_breadcrumb; end
def show_local_breadcrumb
false
end
def settings_params
@@ -39,5 +41,11 @@ module Admin::Settings
settings["attachment_whitelist"] = settings["attachment_whitelist"].split(/\r?\n/)
end
end
def extra_permitted_filters
# attachment_whitelist is normally permitted as an array parameter.
# Explicitly permit it as a string here.
[:attachment_whitelist]
end
end
end
@@ -30,14 +30,21 @@ module Admin::Settings
class ProjectCustomFieldsController < ::Admin::SettingsController
include CustomFields::SharedActions
include OpTurbo::ComponentStream
include ApplicationComponentStreams
include FlashMessagesOutputSafetyHelper
include Admin::Settings::ProjectCustomFields::ComponentStreams
menu_item :project_custom_fields_settings
# rubocop:disable Rails/LexicallyScopedActionFilter
before_action :set_sections, only: %i[show index edit update move drop]
before_action :find_custom_field, only: %i(show edit update destroy delete_option reorder_alphabetical move drop)
before_action :find_custom_field,
only: %i(show edit project_mappings unlink update destroy delete_option reorder_alphabetical move drop)
before_action :prepare_custom_option_position, only: %i(update create)
before_action :find_custom_option, only: :delete_option
before_action :project_custom_field_mappings_query, only: %i[project_mappings unlink]
before_action :find_unlink_project_custom_field_mapping, only: :unlink
# rubocop:enable Rails/LexicallyScopedActionFilter
def show_local_breadcrumb
false
@@ -61,6 +68,25 @@ module Admin::Settings
def edit; end
def project_mappings; end
def unlink
delete_service = ProjectCustomFieldProjectMappings::DeleteService
.new(user: current_user, model: @project_custom_field_mapping)
.call
delete_service.on_success { render_unlink_response }
delete_service.on_failure do
update_flash_message_via_turbo_stream(
message: join_flash_messages(delete_service.errors.full_messages),
full: true, dismiss_scheme: :hide, scheme: :danger
)
end
respond_to_with_turbo_streams(status: delete_service.success? ? :ok : :unprocessable_entity)
end
def move
call = CustomFields::UpdateService.new(user: current_user, model: @custom_field).call(
move_to: params[:move_to]&.to_sym
@@ -100,10 +126,47 @@ module Admin::Settings
private
def render_unlink_response
update_via_turbo_stream(
component: Settings::ProjectCustomFields::ProjectCustomFieldMapping::TableComponent.new(
query: @project_custom_field_mappings_query,
params: { custom_field: @custom_field }
),
status: :ok
)
end
def project_custom_field_mappings_query
@project_custom_field_mappings_query = Queries::Projects::ProjectQuery.new(
name: "project-custom-field-mappings-#{@custom_field.id}"
) do |query|
query.where(:available_project_attributes, "=", [@custom_field.id])
query.select(:name)
query.order("lft" => "asc")
end
end
def set_sections
@project_custom_field_sections = ProjectCustomFieldSection
.includes(custom_fields: :project_custom_field_project_mappings)
.all
.includes(custom_fields: :project_custom_field_project_mappings)
.all
end
def find_unlink_project_custom_field_mapping
@project = Project.find(permitted_params.project_custom_field_project_mapping[:project_id])
@project_custom_field_mapping = @custom_field.project_custom_field_project_mappings.find_by!(project: @project)
rescue ActiveRecord::RecordNotFound
update_flash_message_via_turbo_stream(
message: t(:notice_file_not_found), full: true, dismiss_scheme: :hide, scheme: :danger
)
replace_via_turbo_stream(
component: Settings::ProjectCustomFields::ProjectCustomFieldMapping::TableComponent.new(
query: project_custom_field_mappings_query,
params: { custom_field: @custom_field }
)
)
respond_with_turbo_streams
end
def find_custom_field
@@ -28,7 +28,7 @@
module Admin::Settings
class VirusScanningSettingsController < ::Admin::SettingsController
menu_item :virus_scanning_settings
menu_item :attachments
before_action :require_ee
before_action :check_clamav, only: %i[update], if: -> { scan_enabled? }
@@ -36,6 +36,7 @@ module Admin::Settings
def show_local_breadcrumb
false
end
def av_form
selected = params.dig(:settings, :antivirus_scan_mode)&.to_sym || :disabled
@@ -87,7 +88,7 @@ module Admin::Settings
def rescan_files
flash[:notice] = t("settings.antivirus.remaining_rescanned_files",
file_count: t(:label_x_files, count: Attachment.status_uploaded.count))
file_count: t(:label_x_files, count: Attachment.status_uploaded.count))
Attachment.status_uploaded.update_all(status: :rescan)
job = Attachments::VirusRescanJob.perform_later
+11 -1
View File
@@ -90,7 +90,17 @@ module Admin
end
def settings_params
permitted_params.settings.to_h
permitted_params.settings(*extra_permitted_filters).to_h
end
# Override to allow additional permitted parameters.
#
# Useful when the format of the setting in the parameters is different from
# the expected format in the setting definition, for instance a setting is
# an array in the definition but is passed as a string to be split in the
# parameters.
def extra_permitted_filters
nil
end
def update_service
@@ -89,7 +89,7 @@ module Accounts::CurrentUser
def current_autologin_user
return unless Setting::Autologin.enabled?
autologin_cookie_name = OpenProject::Configuration['autologin_cookie_name']
autologin_cookie_name = OpenProject::Configuration["autologin_cookie_name"]
autologin_token = cookies[autologin_cookie_name]
return unless autologin_token
@@ -105,7 +105,7 @@ module Accounts::CurrentUser
end
def current_rss_key_user
if params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
if params[:format] == "atom" && params[:key] && accept_key_auth_actions.include?(params[:action])
# RSS key authentication does not start a session
User.find_by_rss_key(params[:key])
end
@@ -183,8 +183,8 @@ module Accounts::CurrentUser
format.any(:xml, :js, :json) do
head :unauthorized,
'X-Reason' => 'login needed',
'WWW-Authenticate' => auth_header
"X-Reason" => "login needed",
"WWW-Authenticate" => auth_header
end
format.all { head :not_acceptable }
+1 -1
View File
@@ -59,7 +59,7 @@ module MemberHelper
def invite_new_users(user_ids, send_notification: true)
user_ids.filter_map do |id|
if id.to_i == 0 && id.present? # we've got an email - invite that user
if id.present? && (id.to_i == 0 || EmailValidator.valid?(id)) # we've got an email - invite that user
# Only users with the create_user permission can add users.
if current_user.allowed_globally?(:create_user) && enterprise_allow_new_users?
# The invitation can pretty much only fail due to the user already
-5
View File
@@ -29,7 +29,6 @@
class FavoritesController < ApplicationController
before_action :find_favored_by_object
before_action :require_login
before_action :check_flag
def favorite
if @favored.visible?(User.current)
@@ -45,10 +44,6 @@ class FavoritesController < ApplicationController
private
def check_flag
render_403 unless OpenProject::FeatureDecisions.favorite_projects_active?
end
def find_favored_by_object
model_name = params[:object_type]
klass = ::OpenProject::Acts::Favorable::Registry.instance(model_name)
+1 -1
View File
@@ -33,7 +33,7 @@ class HomescreenController < ApplicationController
def index
@newest_projects = Project.visible.newest.take(3)
@favorite_projects = Project.visible.favored_by(User.current)
@favorite_projects = Project.visible.active.favored_by(User.current)
@newest_users = User.active.newest.take(3)
@news = News.latest(count: 3)
@announcement = Announcement.active_and_current
+4 -3
View File
@@ -27,13 +27,14 @@
# ++
module Projects
class MenusController < ApplicationController
include Menus::ProjectsHelper
# No authorize as every user (or logged in user)
# is allowed to see the menu.
def show
@sidebar_menu_items = first_level_menu_items
projects_menu = Menus::Projects.new(controller_path: params[:controller_path], params:, current_user:)
@sidebar_menu_items = projects_menu.first_level_menu_items
render layout: nil
end
end
+56 -12
View File
@@ -31,31 +31,59 @@ class Projects::QueriesController < ApplicationController
# No need for a more specific authorization check. That is carried out in the contracts.
before_action :require_login
before_action :find_query, only: :destroy
before_action :load_query_or_deny_access, only: %i[new]
before_action :find_query, only: %i[show rename update destroy publish unpublish]
before_action :build_query_or_deny_access, only: %i[new create]
current_menu_item [:new, :create] do
current_menu_item [:new, :rename, :create, :update] do
:projects
end
def show
redirect_to projects_path(query_id: @query.id)
end
def new
render template: "/projects/index",
layout: "global",
locals: { query: @query, state: :edit }
end
def rename
render template: "/projects/index",
layout: "global",
locals: { query: @query, state: :rename }
end
def create
call = Queries::Projects::ProjectQueries::CreateService
.new(user: current_user)
.new(from: @query, user: current_user)
.call(permitted_query_params)
if call.success?
redirect_to projects_path(query_id: call.result.id)
else
render template: "/projects/index",
layout: "global",
locals: { query: call.result, state: :edit }
end
render_result(call, success_i18n_key: "lists.create.success", error_i18n_key: "lists.create.failure")
end
def update
call = Queries::Projects::ProjectQueries::UpdateService
.new(user: current_user, model: @query)
.call(permitted_query_params)
render_result(call, success_i18n_key: "lists.update.success", error_i18n_key: "lists.update.failure")
end
def publish
call = Queries::Projects::ProjectQueries::PublishService
.new(user: current_user, model: @query)
.call(public: true)
render_result(call, success_i18n_key: "lists.publish.success", error_i18n_key: "lists.publish.failure")
end
def unpublish
call = Queries::Projects::ProjectQueries::PublishService
.new(user: current_user, model: @query)
.call(public: false)
render_result(call, success_i18n_key: "lists.unpublish.success", error_i18n_key: "lists.unpublish.failure")
end
def destroy
@@ -67,7 +95,23 @@ class Projects::QueriesController < ApplicationController
private
def render_result(service_call, success_i18n_key:, error_i18n_key:) # rubocop:disable Metrics/AbcSize
modified_query = service_call.result
if service_call.success?
flash[:notice] = I18n.t(success_i18n_key)
redirect_to modified_query.visible? ? projects_path(query_id: modified_query.id) : projects_path
else
flash[:error] = I18n.t(error_i18n_key, errors: service_call.errors.full_messages.join("\n"))
render template: "/projects/index",
layout: "global",
locals: { query: modified_query, state: :edit }
end
end
def find_query
@query = Queries::Projects::ProjectQuery.find(params[:id])
@query = Queries::Projects::ProjectQuery.visible(current_user).find(params[:id])
end
end
+20 -12
View File
@@ -29,25 +29,33 @@ module Projects
module QueryLoading
private
def load_query(duplicate:)
::Queries::Projects::Factory.find(params[:query_id],
params: permitted_query_params,
user: current_user,
duplicate:)
end
def load_query_or_deny_access
@query = Queries::Projects::Factory.find(params[:query_id],
params: permitted_query_params,
user: current_user)
@query = load_query(duplicate: false)
render_403 unless @query
end
def build_query_or_deny_access
@query = load_query(duplicate: true)
render_403 unless @query
end
def permitted_query_params
query_params = if params[:query]
params
.require(:query)
.permit(:name)
.to_h
else
{}
end
query_params = {}
query_params.merge!(Queries::ParamsParser.parse(params))
if params[:query]
query_params.merge!(params.require(:query).permit(:name))
end
query_params.merge!(::Queries::ParamsParser.parse(params))
query_params.with_indifferent_access
end
+6 -1
View File
@@ -32,7 +32,7 @@ class ProjectsController < ApplicationController
before_action :find_project, except: %i[index new]
before_action :load_query_or_deny_access, only: %i[index]
before_action :authorize, only: %i[copy]
before_action :authorize, only: %i[copy deactivate_work_package_attachments]
before_action :authorize_global, only: %i[new]
before_action :require_admin, only: %i[destroy destroy_info]
@@ -91,6 +91,11 @@ class ProjectsController < ApplicationController
hide_project_in_layout
end
def deactivate_work_package_attachments
@project.deactivate_work_package_attachments = params[:value] != "1"
@project.save
end
private
def has_managed_project_folders?(project)
+1 -1
View File
@@ -101,7 +101,7 @@ class StatusesController < ApplicationController
return unless WorkPackage.use_status_for_done_ratio?
return unless @status.default_done_ratio_previously_changed?
WorkPackages::ApplyStatusesPCompleteJob
WorkPackages::Progress::ApplyStatusesPCompleteJob
.perform_later(cause_type: "status_p_complete_changed",
status_name: @status.name,
status_id: @status.id,
@@ -48,7 +48,7 @@ class WorkPackages::BulkController < ApplicationController
if @call.success?
flash[:notice] = t(:notice_successful_update)
redirect_back_or_default(controller: '/work_packages', action: :index, project_id: @project)
redirect_back_or_default(controller: "/work_packages", action: :index, project_id: @project)
else
flash[:error] = bulk_error_message(@work_packages, @call)
setup_edit
@@ -75,7 +75,7 @@ class WorkPackages::BulkController < ApplicationController
associated: WorkPackage.associated_classes_to_address_before_destruction_of(@work_packages) }
end
format.json do
render json: { error_message: 'Clean up of associated objects required' }, status: 420
render json: { error_message: "Clean up of associated objects required" }, status: 420
end
end
end
@@ -125,6 +125,6 @@ class WorkPackages::BulkController < ApplicationController
def transform_attributes(attributes)
Hash(attributes)
.compact_blank
.transform_values { |v| Array(v).include?('none') ? '' : v }
.transform_values { |v| Array(v).include?("none") ? "" : v }
end
end
@@ -36,17 +36,28 @@ class WorkPackages::ProgressController < ApplicationController
layout false
before_action :set_work_package
before_action :extract_persisted_progress_attributes, only: %i[edit create update]
helper_method :modal_class
def edit
build_up_new_work_package
def new
build_up_brand_new_work_package
render modal_class.new(@work_package, focused_field: params[:field])
render modal_class.new(@work_package,
focused_field: params[:field],
touched_field_map:)
end
def edit
build_up_work_package
render modal_class.new(@work_package,
focused_field: params[:field],
touched_field_map:)
end
def create
service_call = build_up_new_work_package
service_call = build_up_brand_new_work_package
if service_call.errors
.map(&:attribute)
@@ -70,7 +81,7 @@ class WorkPackages::ProgressController < ApplicationController
service_call = WorkPackages::UpdateService
.new(user: current_user,
model: @work_package)
.call(update_work_package_params)
.call(work_package_params)
if service_call.success?
respond_to do |format|
@@ -105,13 +116,19 @@ class WorkPackages::ProgressController < ApplicationController
@work_package = WorkPackage.new
end
def create_work_package_params
params.require(:work_package)
.permit(allowed_params)
.compact_blank
def touched_field_map
params.require(:work_package).permit("estimated_hours_touched",
"remaining_hours_touched",
"status_id_touched").to_h
end
def update_work_package_params
def extract_persisted_progress_attributes
@persisted_progress_attributes = @work_package
.attributes
.slice("estimated_hours", "remaining_hours", "status_id")
end
def work_package_params
params.require(:work_package)
.permit(allowed_params)
end
@@ -124,12 +141,46 @@ class WorkPackages::ProgressController < ApplicationController
end
end
def build_up_new_work_package
def reject_params_that_dont_differ_from_persisted_values
work_package_params.reject do |key, value|
@persisted_progress_attributes[key.to_s].to_f.to_s == value.to_f.to_s
end
end
def filtered_work_package_params
{}.tap do |filtered_params|
filtered_params[:estimated_hours] = work_package_params["estimated_hours"] if estimated_hours_touched?
filtered_params[:remaining_hours] = work_package_params["remaining_hours"] if remaining_hours_touched?
filtered_params[:status_id] = work_package_params["status_id"] if status_id_touched?
end
end
def estimated_hours_touched?
params.require(:work_package)[:estimated_hours_touched] == "true"
end
def remaining_hours_touched?
params.require(:work_package)[:remaining_hours_touched] == "true"
end
def status_id_touched?
params.require(:work_package)[:status_id_touched] == "true"
end
def build_up_work_package
WorkPackages::SetAttributesService
.new(user: current_user,
model: @work_package,
contract_class: WorkPackages::CreateContract)
.call(create_work_package_params)
.call(filtered_work_package_params)
end
def build_up_brand_new_work_package
WorkPackages::SetAttributesService
.new(user: current_user,
model: @work_package,
contract_class: WorkPackages::CreateContract)
.call(work_package_params)
end
def formatted_duration(hours)
@@ -37,6 +37,7 @@ class WorkPackagesController < ApplicationController
before_action :authorize_on_work_package,
:project, only: :show
before_action :find_optional_project,
:check_allowed_export,
:protect_from_unauthorized_export, only: :index
before_action :load_and_validate_query, only: :index, unless: -> { request.format.html? }
+1 -1
View File
@@ -81,7 +81,7 @@ module Projects::CustomFields
form_args = { custom_field:, object: @project, wrapper_id: @wrapper_id }
case custom_field.field_format
when "string"
when "string", "link"
CustomFields::Inputs::String.new(builder, **form_args)
when "text"
CustomFields::Inputs::Text.new(builder, **form_args)
@@ -26,7 +26,7 @@
# See COPYRIGHT and LICENSE files for more details.
# ++
class Queries::Projects::Create < ApplicationForm
class Queries::Projects::Form < ApplicationForm
include OpenProject::StaticRouting::UrlHelpers
form do |query_form|
@@ -51,9 +51,13 @@ class Queries::Projects::Create < ApplicationForm
scheme: :secondary,
label: I18n.t(:button_cancel),
tag: :a,
data: { "params-from-query-target": "anchor" },
href: OpenProject::StaticRouting::StaticUrlHelpers.new.projects_path
href: @cancel_url
)
end
end
def initialize(cancel_url:)
super()
@cancel_url = cancel_url
end
end
+47 -23
View File
@@ -27,14 +27,19 @@
# ++
class WorkPackages::ProgressForm < ApplicationForm
attr_reader :work_package
def initialize(work_package:,
mode: :work_based,
focused_field: :remaining_hours)
focused_field: :remaining_hours,
touched_field_map: {})
super()
@work_package = work_package
@mode = mode
@focused_field = focused_field_by_selection(focused_field) || focused_field_by_error
@focused_field = focused_field_by_selection(focused_field)
@touched_field_map = touched_field_map
ensure_only_one_error_for_remaining_work_exceeding_work
end
form do |query_form|
@@ -58,17 +63,51 @@ class WorkPackages::ProgressForm < ApplicationForm
render_text_field(group, name: :estimated_hours, label: I18n.t(:label_work))
render_readonly_text_field(group, name: :remaining_hours, label: I18n.t(:label_remaining_work))
# Add a hidden field in create forms as the select field is disabled and is otherwise not included in the form payload
group.hidden(name: :status_id) if @work_package.new_record?
group.hidden(name: :status_id_touched,
value: @touched_field_map["status_id_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
"referrer-field": "work_package[status_id]" })
group.hidden(name: :estimated_hours_touched,
value: @touched_field_map["estimated_hours_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
"referrer-field": "work_package[estimated_hours]" })
else
render_text_field(group, name: :estimated_hours, label: I18n.t(:label_work))
render_text_field(group, name: :remaining_hours, label: I18n.t(:label_remaining_work),
disabled: disabled_remaining_work_field?)
render_readonly_text_field(group, name: :done_ratio, label: I18n.t(:label_percent_complete))
group.hidden(name: :estimated_hours_touched,
value: @touched_field_map["estimated_hours_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
"referrer-field": "work_package[estimated_hours]" })
group.hidden(name: :remaining_hours_touched,
value: @touched_field_map["remaining_hours_touched"] || false,
data: { "work-packages--progress--touched-field-marker-target": "touchedFieldInput",
"referrer-field": "work_package[remaining_hours]" })
end
end
end
private
def ensure_only_one_error_for_remaining_work_exceeding_work
if work_package.errors.added?(:remaining_hours, :cant_exceed_work) &&
work_package.errors.added?(:estimated_hours, :cant_be_inferior_to_remaining_work)
error_to_delete =
if @focused_field == :estimated_hours
:remaining_hours
else
:estimated_hours
end
work_package.errors.delete(error_to_delete)
end
end
def focused_field_by_selection(field)
if field == :remaining_hours && @work_package.estimated_hours.nil?
:estimated_hours
@@ -77,24 +116,6 @@ class WorkPackages::ProgressForm < ApplicationForm
end
end
# First field with an error is focused. If it's readonly or disabled, then the
# field before it will be focused
def focused_field_by_error
fields = if @mode == :work_based
%i[estimated_hours remaining_hours done_ratio]
else
%i[status_id estimated_hours remaining_hours]
end
fields.each do |field_name|
break if @focused_field
@focused_field = field_name if @work_package.errors.map(&:attribute).include?(field_name)
end
@focused_field
end
def render_text_field(group,
name:,
label:,
@@ -147,11 +168,14 @@ class WorkPackages::ProgressForm < ApplicationForm
end
def default_field_options(name)
data = { "work-packages--progress--preview-progress-target": "progressInput",
"work-packages--progress--touched-field-marker-target": "progressInput",
action: "input->work-packages--progress--touched-field-marker#markFieldAsTouched" }
if @focused_field == name
{ data: { "work-packages--progress--focus-field-target": "fieldToFocus" } }
else
{}
data[:"work-packages--progress--focus-field-target"] = "fieldToFocus"
end
{ data: }
end
# Remaining work field is enabled when work is set, or when there are errors
+4 -4
View File
@@ -14,13 +14,13 @@ module BrowserHelper
# Older versions behind last ESR FF
return true if browser.firefox? && version < 101
# Chrome versions older than a year
return true if browser.chrome? && version < 109
# Chrome/chromium based Edge based versions older than a year
return true if browser.chromium_based? && version < 109
# Older version of safari
return true if browser.safari? && version < 16
# Older version of EDGE
# Older version of non-chromium based Edge
return true if browser.edge? && version < 109
false
@@ -32,7 +32,7 @@ module BrowserHelper
# or mobile detection
def browser_specific_classes
[].tap do |classes|
classes << "-browser-chrome" if browser.chrome?
classes << "-browser-chrome" if browser.chrome? || browser.chromium_based?
classes << "-browser-firefox" if browser.firefox?
classes << "-browser-safari" if browser.safari?
classes << "-browser-edge" if browser.edge?
+6 -10
View File
@@ -31,8 +31,7 @@ module FlashMessagesHelper
extend ActiveSupport::Concern
included do
# For .safe_join in join_flash_messages
include ActionView::Helpers::OutputSafetyHelper
include FlashMessagesOutputSafetyHelper
end
def render_primer_banner_message?
@@ -45,6 +44,11 @@ module FlashMessagesHelper
render(BannerMessageComponent.new(**flash[:primer_banner].to_hash))
end
# Primer's flash message component wrapped in a component which is empty initially but can be updated via turbo stream
def render_streameable_primer_banner_message
render(FlashMessageComponent.new)
end
# Renders flash messages
def render_flash_messages
return if render_primer_banner_message?
@@ -63,14 +67,6 @@ module FlashMessagesHelper
safe_join messages, "\n"
end
def join_flash_messages(messages)
if messages.respond_to?(:join)
safe_join(messages, "<br />".html_safe)
else
messages
end
end
def render_flash_message(type, message, html_options = {}) # rubocop:disable Metrics/AbcSize
if type.to_s == "notice"
type = "success"
@@ -0,0 +1,45 @@
#-- 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 FlashMessagesOutputSafetyHelper
extend ActiveSupport::Concern
included do
# For .safe_join in join_flash_messages
include ActionView::Helpers::OutputSafetyHelper
end
def join_flash_messages(messages)
if messages.respond_to?(:join)
safe_join(messages, "<br />".html_safe)
else
messages
end
end
end
+13 -4
View File
@@ -43,7 +43,7 @@ module FrontendAssetHelper
def include_frontend_assets
capture do
%w(vendor.js polyfills.js runtime.js main.js).each do |file|
concat javascript_include_tag variable_asset_path(file), skip_pipeline: true
concat nonced_javascript_include_tag variable_asset_path(file), skip_pipeline: true
end
concat stylesheet_link_tag variable_asset_path("styles.css"), media: :all, skip_pipeline: true
@@ -56,10 +56,19 @@ module FrontendAssetHelper
end
end
def nonced_javascript_include_tag(path, **)
javascript_include_tag(path, nonce: content_security_policy_script_nonce, **)
end
private
def frontend_asset_path(unhashed_file_name)
"/assets/frontend/#{::OpenProject::Assets.lookup_asset(unhashed_file_name)}"
def lookup_frontend_asset(unhashed_file_name)
hashed_file_name = ::OpenProject::Assets.lookup_asset(unhashed_file_name)
frontend_asset_path(hashed_file_name)
end
def frontend_asset_path(file_name)
"/assets/frontend/#{file_name}"
end
def variable_asset_path(path)
@@ -72,7 +81,7 @@ module FrontendAssetHelper
else
# we do not need to take care about Rails.application.config.relative_url_root
# because in this case javascript|stylesheet_include_tag will add it automatically.
frontend_asset_path(path)
lookup_frontend_asset(path)
end
end
end
+121
View File
@@ -0,0 +1,121 @@
#-- 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 Menus
class Projects
include Rails.application.routes.url_helpers
attr_reader :controller_path, :params, :current_user
def initialize(controller_path:, params:, current_user:)
# rubocop:disable Rails/HelperInstanceVariable
@controller_path = controller_path
@params = params
@current_user = current_user
# rubocop:enable Rails/HelperInstanceVariable
end
def first_level_menu_items
[
OpenProject::Menu::MenuGroup.new(header: nil,
children: main_static_filters),
OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.public"),
children: public_filters),
OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.my_private"),
children: my_filters),
OpenProject::Menu::MenuGroup.new(header: I18n.t(:"activerecord.attributes.project.status_code"),
children: status_static_filters)
]
end
private
def main_static_filters
static_filters [
::Queries::Projects::Factory::STATIC_ACTIVE,
::Queries::Projects::Factory::STATIC_MY,
::Queries::Projects::Factory::STATIC_FAVORED,
::Queries::Projects::Factory::STATIC_ARCHIVED
]
end
def status_static_filters
static_filters [
::Queries::Projects::Factory::STATIC_ON_TRACK,
::Queries::Projects::Factory::STATIC_OFF_TRACK,
::Queries::Projects::Factory::STATIC_AT_RISK
]
end
def static_filters(ids)
ids.map do |id|
query_menu_item(::Queries::Projects::Factory.static_query(id), id:)
end
end
def public_filters
::Queries::Projects::ProjectQuery
.public_lists
.order(:name)
.map { |query| query_menu_item(query) }
end
def my_filters
::Queries::Projects::ProjectQuery
.private_lists(user: current_user)
.order(:name)
.map { |query| query_menu_item(query) }
end
def query_menu_item(query, id: nil)
OpenProject::Menu::MenuItem.new(title: query.name,
href: projects_path(query_id: id || query.id),
selected: query_item_selected?(id || query.id))
end
def query_item_selected?(id)
case controller_path
when "projects"
case params[:query_id]
when nil
id.to_s == Queries::Projects::Factory::DEFAULT_STATIC
when /\A\d+\z/
id.to_s == params[:query_id]
else
id.to_s == params[:query_id] unless modification_params?
end
when "projects/queries"
id.to_s == params[:id]
end
end
def modification_params?
params.values_at(:filters, :columns, :sortBy).any?
end
end
end
-98
View File
@@ -1,98 +0,0 @@
#-- 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 Menus
module ProjectsHelper
def first_level_menu_items
[
OpenProject::Menu::MenuGroup.new(header: nil,
children: static_filters),
OpenProject::Menu::MenuGroup.new(header: I18n.t(:"projects.lists.my_private"),
children: my_filters),
OpenProject::Menu::MenuGroup.new(header: I18n.t(:"activerecord.attributes.project.status_code"),
children: static_status_filters)
]
end
private
def static_filters
favored_item = query_menu_item(::Queries::Projects::Factory.static_query_favored,
id: ::Queries::Projects::Factory::STATIC_FAVORED)
[
query_menu_item(::Queries::Projects::Factory.static_query_active, selected: no_query_props?),
query_menu_item(::Queries::Projects::Factory.static_query_my,
id: ::Queries::Projects::Factory::STATIC_MY),
OpenProject::FeatureDecisions.favorite_projects_active? ? favored_item : nil,
query_menu_item(::Queries::Projects::Factory.static_query_archived,
id: ::Queries::Projects::Factory::STATIC_ARCHIVED)
].compact
end
def static_status_filters
[
query_menu_item(::Queries::Projects::Factory.static_query_status_on_track,
id: ::Queries::Projects::Factory::STATIC_ON_TRACK),
query_menu_item(::Queries::Projects::Factory.static_query_status_off_track,
id: ::Queries::Projects::Factory::STATIC_OFF_TRACK),
query_menu_item(::Queries::Projects::Factory.static_query_status_at_risk,
id: ::Queries::Projects::Factory::STATIC_AT_RISK)
]
end
def my_filters
::Queries::Projects::ProjectQuery
.where(user: current_user)
.order(:name)
.map do |query|
query_menu_item(query)
end
end
def query_menu_item(query, id: nil, selected: query_item_selected?(id || query.id))
OpenProject::Menu::MenuItem.new(title: query.name,
href: projects_path(query_id: id || query.id),
selected:)
end
def projects_path_with_filters(filters)
return projects_path if filters.empty?
projects_path(filters: filters.to_json, hide_filters_section: true)
end
def query_item_selected?(id)
id.to_s == params[:query_id] && params[:filters].nil?
end
def no_query_props?
params[:query_id].nil? && params[:filters].nil? && params[:sortBy].nil?
end
end
end
+6 -4
View File
@@ -48,9 +48,11 @@ module PaginationHelper
end
def pagination_option_links(paginator, pagination_options)
allowed_params = pagination_options[:allowed_params] || %w[filters sortBy]
option_links = pagination_settings(paginator,
pagination_options[:params]
.merge(safe_query_params(%w{filters sortBy expand})))
.merge(safe_query_params(allowed_params)))
content_tag(:div, option_links, class: "op-pagination--options")
end
@@ -168,7 +170,7 @@ module PaginationHelper
def merge_get_params(url_params)
params = super
params.except(*blocked_url_params)
allowed_params ? params.slice(*allowed_params) : params
end
def page_number(page)
@@ -203,8 +205,8 @@ module PaginationHelper
end
end
def blocked_url_params
@options[:blocked_url_params] || [] # rubocop:disable Rails/HelperInstanceVariable
def allowed_params
@options[:allowed_params] # rubocop:disable Rails/HelperInstanceVariable
end
end
+10 -2
View File
@@ -29,15 +29,17 @@
module ProjectsHelper
include WorkPackagesFilterHelper
PROJECTS_QUERY_PARAM_NAMES = %i[query_id filters columns sortBy per_page page].freeze
# Just like sort_header tag but removes sorting by
# lft from the sort criteria as lft is mutually exclusive with
# the other criteria.
def projects_sort_header_tag(*)
def projects_sort_header_tag(column, **)
former_criteria = @sort_criteria.criteria.dup
@sort_criteria.criteria.reject! { |a, _| a == "lft" }
sort_header_tag(*)
sort_header_tag(column, **, allowed_params: projects_query_param_names_for_sort)
ensure
@sort_criteria.criteria = former_criteria
end
@@ -70,4 +72,10 @@ module ProjectsHelper
projects_columns_options
.select { |c| c[:id] == :name }
end
def projects_query_param_names_for_sort = PROJECTS_QUERY_PARAM_NAMES - %i[sortBy page]
def projects_query_params
safe_query_params(PROJECTS_QUERY_PARAM_NAMES)
end
end
+9
View File
@@ -44,4 +44,13 @@ module RolesHelper
perms.group_by { |p| p.project_module.to_s }
.slice(*enabled_module_names)
end
def permission_header_for_project_module(mod)
if mod.blank?
Project.model_name.human
else
I18n.t("permission_header_for_project_module_#{mod}",
default: [:"project_module_#{mod}", mod.humanize])
end
end
end
+12 -12
View File
@@ -32,11 +32,11 @@ module SearchHelper
return nil unless split_text.length > 1 || text_on_not_found
result = ''
result = ""
split_text.each_with_index do |words, i|
if result.length > 1200
# maximum length of the preview reached
result << '...'
result << "..."
break
end
@@ -83,7 +83,7 @@ module SearchHelper
def notes_anchor(event)
version = event.version.to_i
version > 1 ? "note-#{version - 1}" : ''
version > 1 ? "note-#{version - 1}" : ""
end
def with_notes_anchor(event, tokens)
@@ -100,8 +100,8 @@ module SearchHelper
def current_scope
params[:scope] ||
('subprojects' unless @project.nil? || @project.descendants.active.empty?) ||
('current_project' unless @project.nil?)
("subprojects" unless @project.nil? || @project.descendants.active.empty?) ||
("current_project" unless @project.nil?)
end
def link_to_previous_search_page(pagination_previous_date)
@@ -109,7 +109,7 @@ module SearchHelper
@search_params.merge(previous: 1,
project_id: @project.try(:identifier),
offset: pagination_previous_date.to_r.to_s),
class: 'navigate-left')
class: "navigate-left")
end
def link_to_next_search_page(pagination_next_date)
@@ -117,20 +117,20 @@ module SearchHelper
@search_params.merge(previous: nil,
project_id: @project.try(:identifier),
offset: pagination_next_date.to_r.to_s),
class: 'navigate-right')
class: "navigate-right")
end
private
def attachment_fulltexts(event)
only_if_tsv_supported(event) do
Attachment.where(id: event.attachment_ids).pluck(:fulltext).join(' ')
Attachment.where(id: event.attachment_ids).pluck(:fulltext).join(" ")
end
end
def attachment_filenames(event)
only_if_tsv_supported(event) do
event.attachments&.map(&:filename)&.join(' ')
event.attachments&.map(&:filename)&.join(" ")
end
end
@@ -142,7 +142,7 @@ module SearchHelper
def token_span(tokens, words)
t = (tokens.index(words.downcase) || 0) % 4
content_tag('span', h(words), class: "search-highlight token-#{t}")
content_tag("span", h(words), class: "search-highlight token-#{t}")
end
def abbreviated_text(words)
@@ -154,11 +154,11 @@ module SearchHelper
formatted_words
end
if words[0] == ' '
if words[0] == " "
abbreviated_words = " #{abbreviated_words}"
end
if words[-1] == ' ' && words.length > 1
if words[-1] == " " && words.length > 1
abbreviated_words = "#{abbreviated_words} "
end
+6 -4
View File
@@ -272,7 +272,7 @@ module SortHelper
# - the optional caption explicitly specifies the displayed link text.
# - 2 CSS classes reflect the state of the link: sort and asc or desc
#
def sort_link(column, caption, default_order, html_options = {})
def sort_link(column, caption, default_order, allowed_params: nil, **html_options)
order = order_string(column, inverted: true) || default_order
caption ||= column.to_s.humanize
@@ -283,8 +283,10 @@ module SortHelper
sort_options = { sort_key => sort_param }
allowed_params ||= %w[filters per_page expand columns]
# Don't lose other params.
link_to_content_update(h(caption), safe_query_params(%w{filters per_page expand columns}).merge(sort_options), html_options)
link_to_content_update(h(caption), safe_query_params(allowed_params).merge(sort_options), html_options)
end
# Returns a table header <th> tag with a sort link for the named column
@@ -311,7 +313,7 @@ module SortHelper
# </div>
# </th>
#
def sort_header_tag(column, options = {})
def sort_header_tag(column, allowed_params: nil, **options)
caption = get_caption(column, options)
default_order = options.delete(:default_order) || "asc"
@@ -321,7 +323,7 @@ module SortHelper
options[:title] = sort_header_title(column, caption, options)
within_sort_header_tag_hierarchy(options, sort_class(column)) do
sort_link(column, caption, default_order, param:, lang:, title: options[:title])
sort_link(column, caption, default_order, allowed_params:, param:, lang:, title: options[:title])
end
end
-1
View File
@@ -115,7 +115,6 @@ module UsersHelper
end
def change_user_status_links(user)
build_change_user_status_action(user) do |title, name|
link_to title,
change_status_user_path(user,
@@ -34,6 +34,12 @@ module WorkPackagesControllerHelper
end
end
def check_allowed_export
return unless params[:format] == "pdf" && params[:gantt] == "true"
render_403 unless EnterpriseToken.allows_to?(:gantt_pdf_export)
end
def user_allowed_to_export?
User.current.allowed_in_any_work_package?(:export_work_packages, in_project: @project)
end
+37 -37
View File
@@ -85,7 +85,7 @@ module WorkPackagesHelper
if package.closed? && !options[:no_hidden]
parts[:hidden_link] << content_tag(:span,
I18n.t(:label_closed_work_packages),
class: 'hidden-for-sighted')
class: "hidden-for-sighted")
end
# Suffix part
@@ -109,12 +109,12 @@ module WorkPackagesHelper
# combining
prefix = parts[:prefix].join(' ')
suffix = parts[:suffix].join(' ')
link = parts[:link].join(' ').strip
prefix = parts[:prefix].join(" ")
suffix = parts[:suffix].join(" ")
link = parts[:link].join(" ").strip
hidden_link = parts[:hidden_link].join
title = parts[:title].join(' ')
css_class = parts[:css_class].join(' ')
title = parts[:title].join(" ")
css_class = parts[:css_class].join(" ")
# Determine path or url
work_package_link =
@@ -125,8 +125,8 @@ module WorkPackagesHelper
end
if options[:all_link]
link_text = [prefix, link].reject(&:empty?).join(' - ')
link_text = [link_text, suffix].reject(&:empty?).join(': ')
link_text = [prefix, link].reject(&:empty?).join(" - ")
link_text = [link_text, suffix].reject(&:empty?).join(": ")
link_text = [hidden_link, link_text].reject(&:empty?).join
link_to(link_text.html_safe,
@@ -141,8 +141,8 @@ module WorkPackagesHelper
title:,
class: css_class)
[[prefix, html_link].reject(&:empty?).join(' - '),
suffix].reject(&:empty?).join(': ')
[[prefix, html_link].reject(&:empty?).join(" - "),
suffix].reject(&:empty?).join(": ")
end.html_safe
end
@@ -158,41 +158,41 @@ module WorkPackagesHelper
end
def send_notification_option(checked = false)
content_tag(:label, for: 'send_notification', class: 'form--label-with-check-box') do
(content_tag 'span', class: 'form--check-box-container' do
boxes = hidden_field_tag('send_notification', '0', id: nil)
content_tag(:label, for: "send_notification", class: "form--label-with-check-box") do
(content_tag "span", class: "form--check-box-container" do
boxes = hidden_field_tag("send_notification", "0", id: nil)
boxes += check_box_tag('send_notification',
'1',
boxes += check_box_tag("send_notification",
"1",
checked,
class: 'form--check-box')
class: "form--check-box")
boxes
end) + I18n.t('notifications.send_notifications')
end) + I18n.t("notifications.send_notifications")
end
end
# Returns a string of css classes that apply to the issue
def work_package_css_classes(work_package)
s = 'work_package preview-trigger'.html_safe
s = "work_package preview-trigger".html_safe
s << " status-#{work_package.status.position}" if work_package.status
s << " priority-#{work_package.priority.position}" if work_package.priority
s << ' closed' if work_package.closed?
s << ' overdue' if work_package.overdue?
s << ' child' if work_package.child?
s << ' parent' unless work_package.leaf?
s << ' created-by-me' if User.current.logged? && work_package.author_id == User.current.id
s << ' assigned-to-me' if User.current.logged? && work_package.assigned_to_id == User.current.id
s << " closed" if work_package.closed?
s << " overdue" if work_package.overdue?
s << " child" if work_package.child?
s << " parent" unless work_package.leaf?
s << " created-by-me" if User.current.logged? && work_package.author_id == User.current.id
s << " assigned-to-me" if User.current.logged? && work_package.assigned_to_id == User.current.id
s
end
def work_package_associations_to_address(associated)
ret = ''.html_safe
ret = "".html_safe
ret += content_tag(:p, I18n.t(:text_destroy_with_associated), class: 'bold')
ret += content_tag(:p, I18n.t(:text_destroy_with_associated), class: "bold")
ret += content_tag(:ul) do
associated.inject(''.html_safe) do |list, associated_class|
list += content_tag(:li, associated_class.model_name.human, class: 'decorated')
associated.inject("".html_safe) do |list, associated_class|
list += content_tag(:li, associated_class.model_name.human, class: "decorated")
list
end
@@ -202,8 +202,8 @@ module WorkPackagesHelper
end
def back_url_is_wp_show?
route = Rails.application.routes.recognize_path(params[:back_url] || request.env['HTTP_REFERER'])
route[:controller] == 'work_packages' && route[:action] == 'index' && route[:state]&.match?(/^\d+/)
route = Rails.application.routes.recognize_path(params[:back_url] || request.env["HTTP_REFERER"])
route[:controller] == "work_packages" && route[:action] == "index" && route[:state]&.match?(/^\d+/)
end
def last_work_package_note(work_package)
@@ -228,7 +228,7 @@ module WorkPackagesHelper
def protected_work_packages_columns_options
work_packages_columns_options
.select { |column| column[:id] == 'id' || column[:id] == 'subject' }
.select { |column| column[:id] == "id" || column[:id] == "subject" }
end
private
@@ -239,8 +239,8 @@ module WorkPackagesHelper
if description_lines[lines - 1] && work_package.description.to_s.lines.to_a.size > lines
description_lines[lines - 1].strip!
while !description_lines[lines - 1].end_with?('...')
description_lines[lines - 1] = description_lines[lines - 1] + '.'
while !description_lines[lines - 1].end_with?("...")
description_lines[lines - 1] = description_lines[lines - 1] + "."
end
end
@@ -248,7 +248,7 @@ module WorkPackagesHelper
empty_element_tag
else
::OpenProject::TextFormatting::Renderer.format_text(
description_lines.join(''),
description_lines.join,
object: work_package,
attribute: :description,
no_nesting: true
@@ -267,12 +267,12 @@ module WorkPackagesHelper
h(work_package.assigned_to.name).to_s
end
[responsible, assignee].compact.join('<br>').html_safe
[responsible, assignee].compact.join("<br>").html_safe
end
def link_to_work_package_css_classes(package, options)
classes = ['work_package']
classes << 'closed' if package.closed?
classes = ["work_package"]
classes << "closed" if package.closed?
classes << options[:class].to_s
classes

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