mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge branch dev into feature/36233-openproject-dark-mode-mvp
This commit is contained in:
@@ -33,3 +33,5 @@ frontend/node_modules
|
||||
node_modules
|
||||
# travis
|
||||
vendor/bundle
|
||||
/public/assets
|
||||
/config/frontend_assets.manifest.json
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
21a696ef9b170e14ad2daf53364a4c2113822c2f
|
||||
# Update copyright information for 2024
|
||||
c795874f7f281297bbd3bad2fdb58b24cb4ce624
|
||||
# switch to double quotes
|
||||
# rubocop autocorrections
|
||||
f3c99ee5dded81ad55f2b6f3706216d5fa765677
|
||||
5c72ea0046a6b5230bf456f55a296ed6fd579535
|
||||
9e4934cd0a468f46d8f0fc0f11ebc2d4216f789c
|
||||
6678cab48d443b5782fa93b171d62093819ee4fc
|
||||
fa5d03eae00bc8931f99598a74ffd76e0cbca3da
|
||||
|
||||
@@ -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
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
3.2.3
|
||||
3.3.1
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
+9
-12
@@ -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 %>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+10
-17
@@ -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 %>
|
||||
+62
@@ -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
|
||||
+32
@@ -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 %>
|
||||
+41
@@ -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
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@ module CustomActions
|
||||
end
|
||||
|
||||
def initialize(model, user = nil)
|
||||
super(model, user)
|
||||
super
|
||||
end
|
||||
|
||||
attribute :name
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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? }
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user