Merge branch 'dev' into document_keycloak_with_openproject

This commit is contained in:
Pavel Balashou
2024-05-28 16:43:01 +02:00
committed by GitHub
2730 changed files with 85340 additions and 44292 deletions
+2
View File
@@ -33,3 +33,5 @@ frontend/node_modules
node_modules
# travis
vendor/bundle
/public/assets
/config/frontend_assets.manifest.json
+2 -1
View File
@@ -3,9 +3,10 @@
48a4f1b6adb1e847a90a61f2ab277f28bcd77608
# Update copyright information for 2023
21a696ef9b170e14ad2daf53364a4c2113822c2f
# Update copyright information for 2023
# Update copyright information for 2024
c795874f7f281297bbd3bad2fdb58b24cb4ce624
# switch to double quotes
f3c99ee5dded81ad55f2b6f3706216d5fa765677
5c72ea0046a6b5230bf456f55a296ed6fd579535
9e4934cd0a468f46d8f0fc0f11ebc2d4216f789c
6678cab48d443b5782fa93b171d62093819ee4fc
@@ -16,7 +16,7 @@ jobs:
steps:
- id: find_latest_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.OPENPROJECT_CI_GH_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \
@@ -79,19 +79,32 @@ jobs:
echo "Successfully merged $RELEASE_BRANCH into $BASE_BRANCH and pushed"
else
# Close all previous PRs with label
for pr_number in $(gh pr list --label create-merge-release-into-dev-pr --json number --jq='.[].number'); do
gh pr close "$pr_number"
pr_numbers=$(gh pr list --label create-merge-release-into-dev-pr --json number --jq='.[].number')
for pr_number in $pr_numbers; do
gh pr close "$pr_number" --delete-branch
done
pr_body=$(
echo 'Created by GitHub action'
for pr_number in $pr_numbers; do
echo "Replaces #$pr_number"
done
)
TEMP_BRANCH="merge-$RELEASE_BRANCH-$(date "+%Y%m%d%H%M%S")"
git branch "$TEMP_BRANCH" "$RELEASE_BRANCH"
git push origin "$TEMP_BRANCH"
gh pr create \
--base "$BASE_BRANCH" \
--head "$RELEASE_BRANCH" \
--head "$TEMP_BRANCH" \
--title "Merge $RELEASE_BRANCH into $BASE_BRANCH" \
--body 'Created by GitHub action' \
--label create-merge-release-into-dev-pr \
--reviewer opf/backend
echo "Created a PR to merge $RELEASE_BRANCH into $BASE_BRANCH"
--body "$pr_body" \
--label create-merge-release-into-dev-pr
echo "Created a PR to merge $RELEASE_BRANCH ($TEMP_BRANCH) into $BASE_BRANCH"
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.OPENPROJECT_CI_GH_TOKEN }}
+9 -7
View File
@@ -82,16 +82,18 @@ jobs:
run: |
script/i18n/fix_crowdin_pt_language_root_key
- name: "Commit translations"
env:
BRANCH: ${{ matrix.branch }}
run: |
git config user.name "OpenProject Actions CI"
git config user.email "operations+ci@openproject.com"
echo "Updating combined translations"
git ls-files -m -o | grep 'crowdin\/.*\.yml$' | xargs git add
git diff --staged --name-only
git diff --staged --exit-code --quiet || (
git commit -m "update locales from crowdin [ci skip]" &&
echo "Run git pull --rebase" &&
git pull --rebase &&
echo "Run git push origin $BRANCH" &&
git push origin $BRANCH
)
if ! git diff --staged --exit-code --quiet; then
git commit -m "update locales from crowdin [ci skip]"
echo "Run git pull --rebase origin $BRANCH"
git pull --rebase origin "$BRANCH"
echo "Run git push origin $BRANCH"
git push origin "$BRANCH"
fi
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2.3'
ruby-version: '3.3.1'
- uses: MeilCli/danger-action@v5
with:
danger_file: 'Dangerfile'
+102 -45
View File
@@ -3,7 +3,7 @@ name: Docker
on:
# build dev daily
schedule:
- cron: '20 2 * * *' # Daily at 02:20
- cron: "20 2 * * *" # Daily at 02:20
push:
tags:
@@ -18,10 +18,10 @@ permissions:
contents: read # to fetch code (actions/checkout)
env:
REGISTRY_IMAGE: openproject/community
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,70 +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=8cpu-linux,family=m7i+m7a,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
- 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 }}
@@ -127,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
@@ -143,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.
@@ -171,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
@@ -189,7 +238,7 @@ jobs:
matrix:
target: [slim, all-in-one]
needs:
- extract_version
- setup
- build
steps:
- name: Download digests
@@ -204,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
@@ -218,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 }}
@@ -235,3 +284,11 @@ jobs:
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
needs: [setup, build, merge]
if: ${{ always() && contains(needs.*.result, 'failure') }}
uses: ./.github/workflows/email-notification.yml
secrets: inherit
with:
subject: "Docker build failed"
body: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+38
View File
@@ -0,0 +1,38 @@
name: email-notification
on:
workflow_call:
inputs:
subject:
type: string
required: true
body:
type: string
required: true
from:
type: string
default: Github CI <github-ci@openproject.com>
to:
type: string
default: operations@openproject.com
secrets:
OPS_MAIL_SMTP_TOKEN:
required: true
jobs:
notify:
name: Notify
runs-on: ubuntu-latest
steps:
- name: Send mail
uses: dawidd6/action-send-mail@v3
with:
subject: ${{ inputs.subject }}
body: ${{ inputs.body }}
from: ${{ inputs.from }}
to: ${{ inputs.to }}
secure: false
server_port: 587
server_address: smtp.postmarkapp.com
username: PM-T-outbound-bw6Xf6uoKnU76g1RcuydgZ
password: ${{secrets.OPS_MAIL_SMTP_TOKEN }}
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
pull-requests: write # to remove labels
statuses: write # to create commit status
if: github.repository == 'opf/openproject' && ( github.event_name == 'schedule' || github.event_name == 'push' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') )
if: github.repository == 'opf/openproject' && ( github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') )
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
+11 -9
View File
@@ -2,11 +2,6 @@ name: rubocop
on:
pull_request:
branches:
- dev
- release/*
paths:
- '**.rb'
jobs:
rubocop:
@@ -14,12 +9,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Fetch all commits for PR branch plus head commit of base branch
run: |
# fetch all commits of the PR branch
git fetch --shallow-exclude "${{ github.base_ref }}" origin "${{ github.ref }}"
# fix for "fatal: error in object: unshallow"
git repack -d
# fetch head commit of base branch
git fetch --deepen 1 origin "${{ github.ref }}"
- uses: ruby/setup-ruby@v1
- uses: opf/action-rubocop@v2
- uses: opf/action-rubocop@master
with:
github_token: ${{ secrets.github_token }}
rubocop_version: gemfile
rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile
rubocop_extensions: rubocop-inflector:gemfile rubocop-performance:gemfile rubocop-rails:gemfile rubocop-rspec:gemfile
reporter: github-pr-check
only_changed: true
+2 -2
View File
@@ -44,9 +44,9 @@ jobs:
uses: runs-on/cache@v4
with:
path: cache/bundle
key: gem-${{ hashFiles('Gemfile.lock') }}
key: gem-bookworm-${{ hashFiles('.ruby-version') }}-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
gem-
gem-bookworm-${{ hashFiles('.ruby-version') }}-
- name: Cache NPM
uses: runs-on/cache@v4
with:
+4 -1
View File
@@ -1,6 +1,7 @@
require:
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
- ./lib_static/rubocop/cop/open_project/add_preview_for_view_component.rb
- ./lib_static/rubocop/cop/open_project/no_do_end_block_with_rspec_capybara_matcher_in_expect.rb
- ./lib_static/rubocop/cop/open_project/use_service_result_factory_methods.rb
@@ -21,7 +22,7 @@ inherit_mode:
- Exclude
AllCops:
TargetRubyVersion: 3.2
TargetRubyVersion: 3.3
# Enable any new cops in new versions by default
NewCops: enable
Exclude:
@@ -257,8 +258,10 @@ RSpec/ContextWording:
- as
- even
- for
- given
- having
- if
- in
- 'on'
- to
- unless
+1 -1
View File
@@ -1 +1 @@
3.2.3
3.3.1
+20 -17
View File
@@ -36,7 +36,7 @@ ruby File.read(".ruby-version").strip
gem "actionpack-xml_parser", "~> 2.0.0"
gem "activemodel-serializers-xml", "~> 1.0.1"
gem "activerecord-import", "~> 1.6.0"
gem "activerecord-import", "~> 1.7.0"
gem "activerecord-session_store", "~> 2.1.0"
gem "ox"
gem "rails", "~> 7.1.3"
@@ -46,11 +46,11 @@ gem "ffi", "~> 1.15"
gem "rdoc", ">= 2.4.2"
gem "doorkeeper", "~> 5.6.6"
gem "doorkeeper", "~> 5.7.0"
# Maintain our own omniauth due to relative URL root issues
# see upstream PR: https://github.com/omniauth/omniauth/pull/903
gem "omniauth", git: "https://github.com/opf/omniauth", ref: "fe862f986b2e846e291784d2caa3d90a658c67f0"
gem "request_store", "~> 1.6.0"
gem "request_store", "~> 1.7.0"
gem "warden", "~> 1.2"
gem "warden-basic_auth", "~> 0.2.1"
@@ -83,7 +83,7 @@ gem "htmldiff"
gem "stringex", "~> 2.8.5"
# CommonMark markdown parser with GFM extension
gem "commonmarker", "~> 1.0.3"
gem "commonmarker", "~> 1.1.3"
# HTML pipeline for transformations on text formatter output
# such as sanitization or additional features
@@ -115,6 +115,8 @@ gem "ruby-duration", "~> 3.2.0"
# released.
gem "mail", "= 2.8.1"
gem "csv", "~> 3.3"
# provide compatible filesystem information for available storage
gem "sys-filesystem", "~> 1.4.0", require: false
@@ -138,7 +140,7 @@ gem "rack-attack", "~> 6.7.0"
gem "secure_headers", "~> 6.5.0"
# Browser detection for incompatibility checks
gem "browser", "~> 5.3.0"
gem "browser", "~> 6.0.0"
# Providing health checks
gem "okcomputer", "~> 1.18.1"
@@ -157,20 +159,21 @@ gem "airbrake", "~> 13.0.0", require: false
gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "8f14736a88ad0064d2a97be108fe7061ffbcee91"
gem "prawn", "~> 2.4"
gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved.
# prawn implicitly depends on matrix gem no longer in ruby core with 3.1
gem "matrix", "~> 0.4.2"
gem "meta-tags", "~> 2.20.0"
gem "meta-tags", "~> 2.21.0"
gem "paper_trail", "~> 15.1.0"
gem "clamav-client", github: "honestica/clamav-client", ref: "29e78ae94307cb34e79ddd29c5da79752239d8b7"
gem "op-clamav-client", "~> 3.4", require: "clamav"
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
@@ -233,7 +236,7 @@ group :test do
# Test prof provides factories from code
# and other niceties
gem "test-prof", "~> 1.3.0"
gem "turbo_tests", github: "crohr/turbo_tests", ref: "fix/runtime-info"
gem "turbo_tests", github: "opf/turbo_tests", ref: "with-patches"
gem "rack_session_access"
gem "rspec", "~> 3.13.0"
@@ -264,7 +267,7 @@ group :test do
gem "capybara-screenshot", "~> 1.0.17"
gem "cuprite", "~> 0.15.0"
gem "selenium-devtools"
gem "selenium-webdriver", "~> 4.18.0"
gem "selenium-webdriver", "~> 4.20"
gem "fuubar", "~> 2.5.0"
gem "timecop", "~> 0.9.0"
@@ -292,7 +295,7 @@ end
group :development do
gem "listen", "~> 3.9.0" # Use for event-based reloaders
gem 'letter_opener_web'
gem "letter_opener_web"
gem "spring"
gem "spring-commands-rspec"
@@ -381,6 +384,6 @@ gemfiles.each do |file|
send(:eval_gemfile, file) if File.readable?(file)
end
gem "openproject-octicons", "~>19.8.0"
gem "openproject-octicons_helper", "~>19.8.0"
gem "openproject-primer_view_components", "~>0.23.0"
gem "openproject-octicons", "~>19.12.0"
gem "openproject-octicons_helper", "~>19.12.0"
gem "openproject-primer_view_components", "~>0.31.0"
+198 -198
View File
@@ -1,28 +1,11 @@
GIT
remote: https://github.com/citizensadvice/capybara_accessible_selectors
revision: 755b3d9b43695aeaae7edb09a24da0e3cda97ee4
revision: 347bbe06cb420416855e80bb4e3a3016b2d5872c
branch: main
specs:
capybara_accessible_selectors (0.11.0)
capybara (~> 3.36)
GIT
remote: https://github.com/crohr/turbo_tests.git
revision: b700cdb344e1ed8e100fd1f56525ec12fa792b45
ref: fix/runtime-info
specs:
turbo_tests (2.0.0)
bundler (>= 2.1)
parallel_tests (>= 3.3.0, < 5)
rspec (>= 3.10)
GIT
remote: https://github.com/honestica/clamav-client.git
revision: 29e78ae94307cb34e79ddd29c5da79752239d8b7
ref: 29e78ae94307cb34e79ddd29c5da79752239d8b7
specs:
clamav-client (3.4.2)
GIT
remote: https://github.com/opf/md-to-pdf
revision: 8f14736a88ad0064d2a97be108fe7061ffbcee91
@@ -66,6 +49,15 @@ GIT
omniauth-openid_connect-providers (0.2.0)
omniauth-openid-connect (>= 0.2.1)
GIT
remote: https://github.com/opf/turbo_tests.git
revision: c1c4707f536a5642a168650d273d714dfb62d842
ref: with-patches
specs:
turbo_tests (2.2.0)
parallel_tests (>= 3.3.0, < 5)
rspec (>= 3.10)
PATH
remote: modules/auth_plugins
specs:
@@ -82,7 +74,7 @@ PATH
remote: modules/avatars
specs:
openproject-avatars (1.0.0)
fastimage (~> 2.2.0)
fastimage (~> 2.3.0)
gravatar_image_tag (~> 1.2.0)
PATH
@@ -214,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)
@@ -233,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)
@@ -273,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)
@@ -310,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)
@@ -339,7 +331,7 @@ GEM
airbrake-ruby (6.2.2)
rbtree3 (~> 0.6)
android_key_attestation (0.3.0)
appsignal (3.6.4)
appsignal (3.7.5)
rack
ast (2.4.2)
attr_required (1.0.2)
@@ -349,29 +341,29 @@ GEM
activerecord (>= 4.0.0, < 7.2)
awrence (1.2.1)
aws-eventstream (1.3.0)
aws-partitions (1.901.0)
aws-sdk-core (3.191.5)
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.78.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.146.0)
aws-sdk-core (~> 3, >= 3.191.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.8.2)
axe-core-api (4.9.1)
dumb_delegator
virtus
axe-core-rspec (4.8.2)
axe-core-api
axe-core-rspec (4.9.1)
axe-core-api (= 4.9.1)
dumb_delegator
virtus
axiom-types (0.1.1)
@@ -387,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)
@@ -408,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
@@ -428,7 +420,8 @@ 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)
concurrent-ruby (1.2.3)
@@ -441,8 +434,9 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.16.0)
css_parser (1.17.1)
addressable
csv (3.3.0)
cuprite (0.15)
capybara (~> 3.0)
ferrum (~> 0.14.0)
@@ -452,7 +446,7 @@ GEM
date_validator (0.12.0)
activemodel (>= 3)
activesupport (>= 3)
debug (1.9.1)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
deckar01-task_list (2.3.4)
@@ -464,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)
@@ -513,7 +507,7 @@ GEM
erblint-github (1.0.1)
erubi (1.12.0)
escape_utils (1.3.0)
et-orbi (1.2.9)
et-orbi (1.2.11)
tzinfo
eventmachine (1.2.7)
eventmachine_httpserver (0.2.1)
@@ -529,7 +523,7 @@ GEM
faraday (>= 1, < 3)
faraday-net_http (3.1.0)
net-http
fastimage (2.2.7)
fastimage (2.3.1)
ferrum (0.14)
addressable (~> 2.5)
concurrent-ruby (~> 1.1)
@@ -556,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)
@@ -577,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)
@@ -585,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)
@@ -621,17 +615,16 @@ GEM
http-2-next (1.0.3)
http_parser.rb (0.6.0)
httpclient (2.8.3)
httpx (1.2.3)
httpx (1.2.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
@@ -645,12 +638,12 @@ 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)
json (2.7.1)
json (2.7.2)
json-jwt (1.16.6)
activesupport (>= 4.2)
aes_key_wrap
@@ -658,7 +651,7 @@ GEM
bindata
faraday (~> 2.0)
faraday-follow_redirects
json-schema (4.2.0)
json-schema (4.3.0)
addressable (>= 2.8)
json_schemer (2.2.1)
base64
@@ -674,16 +667,17 @@ 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.7)
letter_opener (1.0.0)
launchy (>= 2.0.4)
letter_opener_web (1.4.1)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
lefthook (1.6.13)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
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)
rb-inotify (~> 0.9, >= 0.9.10)
@@ -699,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)
@@ -720,15 +714,16 @@ GEM
markly (0.10.0)
matrix (0.4.2)
messagebird-rest (1.4.2)
meta-tags (2.20.0)
meta-tags (2.21.0)
actionpack (>= 6.0.0, < 7.2)
method_source (1.0.0)
method_source (1.1.0)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2024.0305)
mime-types-data (3.2024.0507)
mini_magick (4.12.0)
mini_mime (1.1.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)
@@ -738,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)
@@ -746,10 +741,11 @@ GEM
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.4.0.1)
net-smtp (0.5.0)
net-protocol
nio4r (2.7.0)
nokogiri (1.16.3)
nio4r (2.7.3)
nokogiri (1.16.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.3)
bigdecimal (>= 3.0)
@@ -757,6 +753,7 @@ GEM
omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.9)
op-clamav-client (3.4.2)
open4 (1.3.4)
openid_connect (2.2.1)
activemodel
@@ -770,15 +767,15 @@ GEM
validate_email
validate_url
webfinger (~> 2.0)
openproject-octicons (19.8.0)
openproject-octicons_helper (19.8.0)
openproject-octicons (19.12.0)
openproject-octicons_helper (19.12.0)
actionview
openproject-octicons (= 19.8.0)
openproject-octicons (= 19.12.0)
railties
openproject-primer_view_components (0.23.0)
openproject-primer_view_components (0.31.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
openproject-octicons (>= 19.8.0)
openproject-octicons (>= 19.12.0)
view_component (>= 3.1, < 4.0)
openproject-token (4.0.0)
activemodel
@@ -791,12 +788,12 @@ GEM
activerecord (>= 6.1)
request_store (~> 1.4)
parallel (1.24.0)
parallel_tests (4.6.0)
parallel_tests (4.7.1)
parallel
parser (3.3.0.5)
parser (3.3.1.0)
ast (~> 2.4.1)
racc
pdf-core (0.10.0)
pdf-core (0.9.0)
pdf-inspector (1.3.0)
pdf-reader (>= 1.0, < 3.0.a)
pdf-reader (2.12.0)
@@ -810,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)
@@ -832,7 +828,7 @@ GEM
pry (>= 0.12.0)
psych (5.1.2)
stringio
public_suffix (5.0.4)
public_suffix (5.0.5)
puffing-billy (4.0.0)
addressable (~> 2.5)
em-http-request (~> 1.1, >= 1.1.0)
@@ -846,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)
@@ -870,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)
@@ -905,42 +901,44 @@ 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)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.1.0)
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.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.0)
redis (5.2.0)
redis-client (>= 0.22.0)
redis-client (0.22.2)
connection_pool
regexp_parser (2.9.0)
reline (0.4.3)
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)
@@ -955,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)
@@ -969,7 +967,7 @@ GEM
rspec-retry (0.6.2)
rspec-core (> 3.3)
rspec-support (3.13.1)
rubocop (1.62.1)
rubocop (1.64.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@@ -980,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)
@@ -990,23 +988,26 @@ GEM
activesupport
rubocop
rubocop-rspec
rubocop-performance (1.20.2)
rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0)
rubocop-rails (2.24.1)
rubocop-ast (>= 1.31.1, < 2.0)
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.27.1)
rubocop-rspec (2.29.2)
rubocop (~> 1.40)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-duration (3.2.3)
activesupport (>= 3.0.0)
i18n
iso8601
ruby-ole (1.2.12.2)
ruby-ole (1.2.13.1)
ruby-prof (1.7.0)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
@@ -1023,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.18.1)
selenium-webdriver (4.21.1)
base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@@ -1039,30 +1040,31 @@ 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.1.3)
spring (4.2.1)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
spring (>= 1.0)
sprockets (3.7.2)
sprockets (3.7.3)
base64
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
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)
@@ -1070,12 +1072,12 @@ 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)
unicode-display_width (>= 1.1.1, < 3)
test-prof (1.3.2)
test-prof (1.3.3)
text-hyphen (1.5.0)
thor (1.3.1)
thread_safe (0.3.6)
@@ -1086,8 +1088,7 @@ GEM
openssl (> 2.0)
openssl-signature_algorithm (~> 1.0)
trailblazer-option (0.1.2)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
ttfunk (1.7.0)
turbo-rails (2.0.5)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
@@ -1099,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)
@@ -1111,7 +1109,7 @@ GEM
activemodel (>= 3.0.0)
public_suffix
vcr (6.2.0)
view_component (3.11.0)
view_component (3.12.1)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
@@ -1136,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)
@@ -1152,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
@@ -1160,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)
@@ -1176,20 +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)
clamav-client!
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)
@@ -1198,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)
@@ -1233,11 +1231,11 @@ DEPENDENCIES
letter_opener_web
listen (~> 3.9.0)
lograge (~> 0.14.0)
lookbook (~> 2.2.1)
lookbook (~> 2.3.0)
mail (= 2.8.1)
matrix (~> 0.4.2)
md_to_pdf!
meta-tags (~> 2.20.0)
meta-tags (~> 2.21.0)
mini_magick (~> 4.12.0)
multi_json (~> 1.15.0)
my_page!
@@ -1249,6 +1247,7 @@ DEPENDENCIES
omniauth-openid-connect!
omniauth-openid_connect-providers!
omniauth-saml (~> 1.10.1)
op-clamav-client (~> 3.4)
openproject-auth_plugins!
openproject-auth_saml!
openproject-avatars!
@@ -1263,10 +1262,10 @@ DEPENDENCIES
openproject-job_status!
openproject-ldap_groups!
openproject-meeting!
openproject-octicons (~> 19.8.0)
openproject-octicons_helper (~> 19.8.0)
openproject-octicons (~> 19.12.0)
openproject-octicons_helper (~> 19.12.0)
openproject-openid_connect!
openproject-primer_view_components (~> 0.23.0)
openproject-primer_view_components (~> 0.31.0)
openproject-recaptcha!
openproject-reporting!
openproject-storages!
@@ -1295,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)
@@ -1323,7 +1322,7 @@ DEPENDENCIES
sanitize (~> 6.1.0)
secure_headers (~> 6.5.0)
selenium-devtools
selenium-webdriver (~> 4.18.0)
selenium-webdriver (~> 4.20)
semantic (~> 1.6.1)
shoulda-context (~> 2.0)
shoulda-matchers (~> 6.0)
@@ -1341,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)
@@ -1355,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
+2
View File
@@ -1,5 +1,7 @@
@import "work_packages/share/modal_body_component"
@import "work_packages/share/invite_user_form_component"
@import "work_packages/progress/modal_body_component"
@import "open_project/common/attribute_component"
@import "filters_component"
@import "projects/settings/project_custom_field_sections/index_component"
@import "projects/row_component"
@@ -29,14 +29,22 @@ See COPYRIGHT and LICENSE files for more details.
<li class="op-activity-list--item">
<div class="op-activity-list--item-title">
<%= link_to @event.event_title.squish, @event.event_path %>
<% if @event.event_path %>
<%= link_to @event.event_title.squish, @event.event_path %>
<% else %>
<%= @event.event_title %>
<% end %>
<%= project_suffix %>
</div>
<%=
render(Activities::ItemSubtitleComponent.new(user: display_user? && @event.event_author,
datetime: @event.event_datetime,
is_creation: @event.journal.initial?,
journable_type: @event.journal.journable_type))
render(Activities::ItemSubtitleComponent.new(
user: display_user? && @event.event_author,
datetime: @event.event_datetime,
is_creation: initial?,
is_deletion: deletion?,
is_work_package: work_package?,
journable_type: @event.journal.journable_type)
)
%>
<% if comment.present? -%>
<ul class="op-activity-list--item-details">
@@ -60,5 +68,11 @@ See COPYRIGHT and LICENSE files for more details.
<li class="op-activity-list--item-detail"><%= link_to "Details", @event.event_url %></li>
<% end -%>
</ul>
<% elsif noop? -%>
<ul class="op-activity-list--item-details">
<li class="op-activity-list--item-detail">
<em><%= I18n.t(:"journals.changes_retracted") %></em>
</li>
</ul>
<% end -%>
</li>
+37 -5
View File
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
#++
class Activities::ItemComponent < ViewComponent::Base
class Activities::ItemComponent < ViewComponent::Base # rubocop:disable OpenProject/AddPreviewForViewComponent
with_collection_parameter :event
strip_trailing_whitespace
@@ -44,7 +44,7 @@ class Activities::ItemComponent < ViewComponent::Base
return if activity?(Project)
return if activity_is_from_current_project?
kind = activity_is_from_subproject? ? 'subproject' : 'project'
kind = activity_is_from_subproject? ? "subproject" : "project"
suffix = I18n.t("events.title.#{kind}", name: link_to(@event.project.name, @event.project))
"(#{suffix})".html_safe # rubocop:disable Rails/OutputSafety
end
@@ -54,13 +54,23 @@ class Activities::ItemComponent < ViewComponent::Base
end
def display_details?
return false if @event.journal.initial? && @event.journal.journable_type != 'TimeEntry'
journal = @event.journal
return false if (initial? && journal.journable_type != "TimeEntry") || deletion?
rendered_details.present?
end
def noop?
!initial? && !deletion? && @event.journal.noop?
end
def rendered_details
filter_details.filter_map { |detail| @event.journal.render_detail(detail, activity_page: @activity_page) }
if data[:details]
render_event_details
else
# this doesn't do anything for details of type "agenda_items_10_title"
filter_journal_details.filter_map { |detail| @event.journal.render_detail(detail, activity_page: @activity_page) }
end
end
def comment
@@ -81,6 +91,22 @@ class Activities::ItemComponent < ViewComponent::Base
@event.event_url
end
def initial?
@event.journal.initial? || data[:initial]
end
def deletion?
data[:deleted]
end
def work_package?
data[:work_package]
end
def data
@event.data || {}
end
private
def activity?(type)
@@ -95,7 +121,7 @@ class Activities::ItemComponent < ViewComponent::Base
@current_project && (@event.project != @current_project)
end
def filter_details
def filter_journal_details
details = @event.journal.details
details.delete(:user_id) if details[:logged_by_id] == details[:user_id]
@@ -110,4 +136,10 @@ class Activities::ItemComponent < ViewComponent::Base
def delete_detail(details, field)
details.delete(field) if details[field] && details[field].first.nil?
end
def render_event_details
data[:details].filter_map do |detail|
@event.journal.render_detail(detail, activity_page: @activity_page)
end
end
end
@@ -29,11 +29,13 @@
#++
class Activities::ItemSubtitleComponent < ViewComponent::Base
def initialize(user:, datetime:, is_creation:, journable_type:)
def initialize(user:, datetime:, is_creation:, is_deletion:, is_work_package:, journable_type:)
super()
@user = user
@datetime = datetime
@is_creation = is_creation
@is_deletion = is_deletion
@is_work_package = is_work_package
@journable_type = journable_type
end
@@ -41,9 +43,9 @@ class Activities::ItemSubtitleComponent < ViewComponent::Base
return unless @user
[
helpers.avatar(@user, size: 'mini'),
helpers.content_tag('span', helpers.link_to_user(@user), class: %w[spot-caption spot-caption_bold])
].join(' ')
helpers.avatar(@user, size: "mini"),
helpers.content_tag("span", helpers.link_to_user(@user), class: %w[spot-caption spot-caption_bold])
].join(" ")
end
def datetime_html
@@ -51,15 +53,27 @@ class Activities::ItemSubtitleComponent < ViewComponent::Base
end
def time_entry?
@journable_type == 'TimeEntry'
@journable_type == "TimeEntry"
end
def i18n_key
i18n_key = 'activity.item.'.dup
i18n_key << (@is_creation ? 'created_' : 'updated_')
i18n_key << 'by_' if @user
i18n_key << 'on'
i18n_key << '_time_entry' if time_entry?
i18n_key = +"activity.item."
i18n_key << (@is_deletion ? deletion_selector : creation_selector)
i18n_key << "by_" if @user
i18n_key << "on"
i18n_key << "_time_entry" if time_entry?
i18n_key
end
def deletion_selector
@is_work_package ? "removed_" : "deleted_"
end
def creation_selector
if @is_creation
@is_work_package ? "added_" : "created_"
else
"updated_"
end
end
end
@@ -0,0 +1,53 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2024 the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% helpers.html_title t(:label_administration), @title %>
<%= render(Primer::OpenProject::PageHeader.new(border_bottom: 0)) do |header| %>
<% header.with_title { t(:"attributes.attachments") } %>
<% header.with_breadcrumbs([{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_storages_path, text: t("project_module_storages") },
t(:"attributes.attachments")]) %>
<% end %>
<%= render(Primer::Alpha::TabNav.new(label: "label")) do |component|
component.with_tab(selected: @selected == 1, href: admin_settings_attachments_path) do |tab|
tab.with_text { t("settings.general") }
end
component.with_tab(selected: @selected == 2, href: admin_settings_virus_scanning_path) do |tab|
tab.with_icon(icon: :"op-enterprise-addons") unless EnterpriseToken.allows_to?("virus_scanning")
tab.with_text { t(:"settings.antivirus.title") }
end
if User.current.admin? && (EnterpriseToken.allows_to?(:virus_scanning) || Attachment.status_quarantined.any?)
component.with_tab(selected: @selected == 3, href: admin_quarantined_attachments_path) do |tab|
tab.with_text { t(:"antivirus_scan.quarantined_attachments.title") }
end
end
end
%>
@@ -0,0 +1,39 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Admin
class AttachmentsSettingsHeaderComponent < ApplicationComponent
def initialize(title:, selected:)
raise 'selected must 1, 2 or 3' if [1, 2, 3].exclude?(selected)
@title = title
@selected = selected
end
end
end
@@ -0,0 +1,47 @@
<%#-- 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.
++#%>
<%= render(Settings::ProjectCustomFields::EditFormHeaderComponent.new(custom_field: @custom_field)) %>
<%=
render(Primer::Alpha::TabNav.new(label: nil, test_selector: :project_attribute_detail_header)) do |component|
component.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
component.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
%>
@@ -0,0 +1,57 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Admin
module Settings
module ProjectCustomFields
class ProjectAttributeDetailHeaderComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
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
end
end
end
end
@@ -43,7 +43,7 @@ module OpTurbo
included do
def render_as_turbo_stream(view_context:, action: :update)
case action
when :update
when :update, :dialog
@inner_html_only = true
template = render_in(view_context)
when :replace
@@ -56,7 +56,7 @@ module OpTurbo
raise ArgumentError, "Unsupported action #{action}"
end
unless wrapped?
if action != :dialog && !wrapped?
raise MissingComponentWrapper,
"Wrap your component in a `component_wrapper` block in order to use turbo-stream methods"
end
+15 -3
View File
@@ -1,15 +1,21 @@
<div data-controller="filters"
data-application-target="dynamic"
data-filters-output-format-value="<%= output_format %>"
data-filters-display-filters-value="<%= show_filters_section? %>">
<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 %>
<%= t(:label_filter) %>
<%= render(Primer::Beta::Counter.new(count: filters_count, round: true, hide_if_zero: false, scheme: :default)) %>
<%= render(Primer::Beta::Counter.new(count: filters_count,
round: true,
hide_if_zero: false,
scheme: :default,
test_selector: "filters-button-counter" )) %>
<% end %>
<div class="op-filters-header-actions">
<% buttons.each do |button| %>
@@ -31,8 +37,9 @@
data-action="filters#toggleDisplayFilters"></a>
<legend><%= t(:label_filter_plural) %></legend>
<ul class="advanced-filters--filters">
<% each_filter do |filter, filter_active| %>
<% each_filter do |filter, filter_active, additional_options| %>
<% filter_boolean = filter.is_a?(Queries::Filters::Shared::BooleanFilter) %>
<% autocomplete_filter = additional_options.key?(:autocomplete_options) %>
<li class="advanced-filters--filter <%= filter_active ? '' : 'hidden' %>"
filter-name="<%= filter.name %>"
@@ -58,7 +65,12 @@
} %>
<% end %>
<% value_visibility = operators_without_values.include?(selected_operator) ? 'hidden' : '' %>
<% if filter_boolean %>
<% if autocomplete_filter %>
<%= render partial: 'filters/autocomplete',
locals: { value_visibility: value_visibility,
filter: filter,
autocomplete_options: additional_options[:autocomplete_options] } %>
<% elsif filter_boolean %>
<%= render partial: 'filters/boolean',
locals: { value_visibility: value_visibility,
filter: filter } %>
+32 -8
View File
@@ -30,6 +30,8 @@
class FiltersComponent < ApplicationComponent
options :query
options :disabled
options output_format: "params"
renders_many :buttons, lambda { |**system_arguments|
system_arguments[:ml] ||= 2
@@ -43,15 +45,11 @@ class FiltersComponent < ApplicationComponent
# Returns filters, active and inactive.
# In case a filter is active, the active one will be preferred over the inactive one.
def each_filter
allowed_filters.map do |filter|
allowed_filters.each do |filter|
active_filter = query.find_active_filter(filter.name)
filter_active = active_filter.present?
additional_attributes = additional_filter_attributes(filter)
if filter_active
yield active_filter, filter_active
else
yield filter, filter_active
end
yield active_filter.presence || filter, active_filter.present?, additional_attributes
end
end
@@ -61,6 +59,32 @@ class FiltersComponent < ApplicationComponent
end
def filters_count
query.filters.count
@filters_count ||= query.filters.count
end
protected
# With this method we can pass additional options for each type of filter into the frontend. This is especially
# useful when we want to pass options for the autocompleter components.
#
# When the method is overwritten in a subclass, the subclass should call super(filter) to get the default attributes.
#
# @param filter [QueryFilter] the filter for which we want to pass additional attributes
# @return [Hash] the additional attributes for the filter, that will be yielded in the each_filter method
def additional_filter_attributes(filter)
case filter
when Queries::Filters::Shared::ProjectFilter
{
autocomplete_options: {
component: "opce-project-autocompleter",
resource: "projects",
filters: [
{ name: "active", operator: "=", values: ["t"] }
]
}
}
else
{}
end
end
end
@@ -0,0 +1,97 @@
<%#-- 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.
++#%>
<%= render(Primer::Alpha::Dialog.new(id:, title: scoped_t(:title), size: :large)) do |dialog| %>
<% dialog.with_header(variant: :large) %>
<% case %>
<% when can_delete_roles? && may_delete_shares? && shared_work_packages? %>
<% dialog.with_body do %>
<% if !can_delete? %>
<%= paragraph { scoped_t(:can_remove_direct_but_not_shared_roles) } %>
<%= paragraph { scoped_t(:also_work_packages_shared_with_user_html, shared_work_packages_link:, count: shared_work_packages_count) } %>
<%= paragraph do %>
<%= scoped_t(:remove_project_membership_or_work_package_shares_too) %>
<%= scoped_t(:will_not_affect_inherited_shares) if inherited_shared_work_packages_count? %>
<% end %>
<% elsif principal.is_a?(Group) %>
<%= paragraph do %>
<%= scoped_t(:will_remove_the_groups_role) %>
<%= scoped_t(:however_work_packages_shared_with_group_html, shared_work_packages_link:, count: shared_work_packages_count) %>
<% end %>
<%= paragraph do %>
<%= scoped_t(:remove_work_packages_shared_with_group_too) %>
<% end %>
<% else %>
<%= paragraph do %>
<%= scoped_t(:will_remove_the_users_role) %>
<%= scoped_t(:however_work_packages_shared_with_user_html, shared_work_packages_link:, count: shared_work_packages_count) %>
<% end %>
<%= paragraph do %>
<%= scoped_t(:remove_work_packages_shared_with_user_too) %>
<%= scoped_t(:will_not_affect_inherited_shares) if inherited_shared_work_packages_count? %>
<% end %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= cancel_button %>
<%= button(scheme: :danger, tag: :a, href: delete_url(project: true), data: { method: :delete }) { t(:button_remove_member) } %>
<%= button(scheme: :danger, tag: :a, href: delete_url(project: true, work_package_shares_role_id: "all"), data: { method: :delete }) { t(:button_remove_member_and_shares) } %>
<% end %>
<% when can_delete_roles? %>
<% dialog.with_body do %>
<% if !can_delete? %>
<%= paragraph { scoped_t(:can_remove_direct_but_not_shared_roles) } %>
<% elsif principal.is_a?(Group) %>
<%= paragraph { scoped_t(:will_remove_all_group_access_priveleges) } %>
<% else %>
<%= paragraph { scoped_t(:will_remove_all_user_access_priveleges) } %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= cancel_button %>
<%= button(scheme: :danger, tag: :a, href: delete_url(project: true), data: { method: :delete }) { t(:button_remove) } %>
<% end %>
<% else %>
<% dialog.with_body do %>
<%= paragraph { scoped_t(:cannot_delete_inherited_membership) } %>
<% if may_manage_user? %>
<%= paragraph { scoped_t(:cannot_delete_inherited_membership_note_admin_html, administration_settings_link:) } %>
<% else %>
<%= paragraph { scoped_t(:cannot_delete_inherited_membership_note_non_admin) } %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= cancel_button %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,73 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Members
class DeleteMemberDialogComponent < ::ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
options :row
delegate :shared_work_packages_count,
:shared_work_packages_link,
:administration_settings_link,
:can_delete?,
:can_delete_roles?,
:may_delete_shares?,
:shared_work_packages?,
:may_manage_user?,
to: :row
delegate :principal,
:inherited_shared_work_packages_count?,
to: :model
def paragraph(&) = render(Primer::Beta::Text.new(tag: "p"), &)
def button(**, &) = render(Primer::Beta::Button.new(**), &)
def cancel_button = button(data: { close_dialog_id: id }) { t(:button_cancel) }
def scoped_t(key, **)
t(key, scope: "members.delete_member_dialog", **)
end
def id
"principal-#{principal.id}-delete-member-dialog"
end
def delete_url(project: nil, work_package_shares_role_id: nil)
url_for(
controller: "/members",
action: "destroy_by_principal",
project_id: row.project,
principal_id: row.principal,
project:,
work_package_shares_role_id:
)
end
end
end
@@ -0,0 +1,107 @@
<%#-- 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.
++#%>
<%= render(Primer::Alpha::Dialog.new(id:, title: scoped_t(:title), size: :large)) do |dialog| %>
<% dialog.with_header(variant: :large) %>
<% case %>
<% when direct_shared_work_packages_count? && other_shared_work_packages_count? %>
<% dialog.with_body do %>
<% if inherited_shared_work_packages_count? %>
<%= paragraph do %>
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
<%= scoped_t(:shared_with_permission_html, shared_work_packages_link:, count: shared_work_packages_count, shared_role_name:) %>
<% end %>
<%= paragraph do %>
<%= scoped_t(:revoke_all_or_with_role, shared_role_name:) %>
<%= scoped_t(:will_not_affect_inherited_shares) %>
<% end %>
<% elsif principal.is_a?(Group) %>
<%= paragraph do %>
<%= scoped_t(:shared_with_this_group_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
<%= scoped_t(:shared_with_permission_html, shared_work_packages_link:, count: shared_work_packages_count, shared_role_name:) %>
<% end %>
<%= paragraph { scoped_t(:revoke_all_or_with_role, shared_role_name:) } %>
<% else %>
<%= paragraph do %>
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
<%= scoped_t(:shared_with_permission_html, shared_work_packages_link:, count: shared_work_packages_count, shared_role_name:) %>
<% end %>
<%= paragraph { scoped_t(:revoke_all_or_with_role, shared_role_name:) } %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= cancel_button %>
<%= button(scheme: :danger, tag: :a, href: delete_url(work_package_shares_role_id: "all"), data: { method: :delete }) { t(:button_revoke_all) } %>
<%= button(scheme: :danger, tag: :a, href: delete_url(work_package_shares_role_id: shared_role_id), data: { method: :delete }) { t(:button_revoke_only, shared_role_name:) } %>
<% end %>
<% when direct_shared_work_packages_count? %>
<% dialog.with_body do %>
<% if inherited_shared_work_packages_count? %>
<%= paragraph do %>
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
<%= scoped_t(:will_revoke_directly_granted_access) %>
<% end %>
<% elsif principal.is_a?(Group) %>
<%= paragraph do %>
<%= scoped_t(:shared_with_this_group_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
<%= scoped_t(:will_revoke_access_to_all) %>
<% end %>
<% else %>
<%= paragraph do %>
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
<%= scoped_t(:will_revoke_access_to_all) %>
<% end %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= cancel_button %>
<%= button(scheme: :danger, tag: :a, href: delete_url(work_package_shares_role_id: "all"), data: { method: :delete }) { t(:button_revoke_access) } %>
<% end %>
<% else %>
<% dialog.with_body do %>
<% if other_shared_work_packages_count? %>
<%= paragraph { scoped_t(:cannot_remove_inherited_with_role, shared_role_name:) } %>
<% else %>
<%= paragraph { scoped_t(:cannot_remove_inherited, shared_role_name:) } %>
<% end %>
<% if may_manage_user? %>
<%= paragraph { scoped_t(:cannot_remove_inherited_note_admin_html, administration_settings_link:) } %>
<% else %>
<%= paragraph { scoped_t(:cannot_remove_inherited_note_non_admin) } %>
<% end %>
<% end %>
<% dialog.with_footer do %>
<%= cancel_button %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,77 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Members
class DeleteWorkPackageSharesDialogComponent < ::ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
options :row
delegate :table,
:shared_work_packages_count,
:all_shared_work_packages_count,
:shared_work_packages_link,
:all_shared_work_packages_link,
:administration_settings_link,
:may_manage_user?,
to: :row
delegate :shared_role_id,
:shared_role_name,
to: :table
delegate :principal,
:other_shared_work_packages_count?,
:direct_shared_work_packages_count?,
:inherited_shared_work_packages_count?,
to: :model
def paragraph(&) = render(Primer::Beta::Text.new(tag: "p"), &)
def button(**, &) = render(Primer::Beta::Button.new(**), &)
def cancel_button = button(data: { close_dialog_id: id }) { t(:button_cancel) }
def scoped_t(key, **)
t(key, scope: "members.delete_work_package_shares_dialog", **)
end
def id
"principal-#{principal.id}-delete-work-package-shares-dialog"
end
def delete_url(work_package_shares_role_id: nil)
url_for(
controller: "/members",
action: "destroy_by_principal",
project_id: row.project,
principal_id: row.principal,
work_package_shares_role_id:
)
end
end
end
@@ -1,39 +1,27 @@
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t(:label_member_plural) }
header.with_title { page_title }
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
header.with_actions do
flex_layout do |actions|
if User.current.allowed_in_project?(:manage_members, @project)
actions.with_column(mr: BUTTON_MARGIN_RIGHT) do
render(
Primer::Beta::Button.new(
scheme: :primary,
size: :medium,
aria: { label: I18n.t(:button_add_member) },
title: I18n.t(:button_add_member),
id: 'add-member-button',
data: add_button_data_attributes
)
) do |button|
button.with_leading_visual_icon(icon: :plus)
t('activerecord.models.member')
end
end
end
actions.with_column do
render(
Primer::Beta::IconButton.new(
icon: 'filter',
id: 'filter-member-button',
aria: { label: I18n.t(:description_filter) },
class: 'toggle-member-filter-link',
data: filter_button_data_attributes
)
)
end
end
header.with_action_button(scheme: :primary,
mobile_icon: :plus,
mobile_label: t('activerecord.models.member'),
size: :medium,
aria: { label: I18n.t(:button_add_member) },
title: I18n.t(:button_add_member),
id: "add-member-button",
data: add_button_data_attributes) do |button|
button.with_leading_visual_icon(icon: :plus)
t('activerecord.models.member')
end
header.with_action_icon_button(mobile_icon: "filter",
scheme: :default,
icon: "filter",
label: I18n.t(:description_filter),
id: "filter-member-button",
aria: { label: I18n.t(:description_filter) },
class: "toggle-member-filter-link",
data: filter_button_data_attributes)
end
%>
@@ -31,8 +31,7 @@
class Members::IndexPageHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include ApplicationHelper
BUTTON_MARGIN_RIGHT = 2
include Menus::MembersHelper
def initialize(project: nil)
super
@@ -57,4 +56,55 @@ class Members::IndexPageHeaderComponent < ApplicationComponent
action: "members-form#toggleMemberFilter"
}
end
def breadcrumb_items
[{ href: project_overview_path(@project.id), text: @project.name },
{ href: project_members_path(@project), text: t(:label_member_plural) },
current_breadcrumb_element]
end
def page_title
# Rework this, when the Members page actually works with queries
@query ||= current_query
query_name = @query[:query_name]
if @query && query_name
query_name
else
t(:label_member_plural)
end
end
def current_breadcrumb_element
# Rework this, when the Members page actually works with queries
@query ||= current_query
query_name = @query[:query_name]
menu_header = @query[:menu_header]
if @query && query_name
if menu_header.present?
I18n.t("menus.breadcrumb.nested_element", section_header: menu_header, title: query_name).html_safe
else
query_name
end
else
t(:label_member_plural)
end
end
def current_query
query_name = nil
menu_header = nil
first_level_menu_items.find do |section|
section.children.find do |menu_query|
if !!menu_query.selected
query_name = menu_query.title
menu_header = section.header
end
end
end
{ query_name:, menu_header: }
end
end
@@ -27,12 +27,14 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= form_with model: member,
<%= form_with model: member.new_record? ? [member.project, member] : member,
builder: TabularFormBuilder,
html: form_html_options do |f| %>
<%= hidden_field_tag :page, params[:page].to_i %>
<%= hidden_field_tag :per_page, params[:per_page].to_i %>
<%= hidden_field_tag f.field_name(:user_ids, multiple: true), member.principal.id if member.new_record? %>
<p>
<% roles.each do |role| %>
<label class="form--label-with-check-box">
@@ -40,6 +42,7 @@ See COPYRIGHT and LICENSE files for more details.
role.id,
member.roles.include?(role),
role.name,
disabled: role_disabled?(role),
no_label: true
%>
<%= role %>
@@ -48,13 +51,13 @@ See COPYRIGHT and LICENSE files for more details.
</p>
<p>
<%= f.submit t(:button_change), class: "button -primary -small" %>
<%= f.submit t(member.new_record? ? :button_add : :button_change), class: "button -primary -small" %>
<%= link_to t(:button_cancel),
'#',
"#",
data: {
action: 'members-form#toggleMembershipEdit',
'members-form-toggling-class-param': row.toggle_item_class_name
action: "members-form#toggleMembershipEdit",
"members-form-toggling-class-param": row.toggle_item_class_name
},
class: 'button -small toggle-membership-button' %>
class: "button -small toggle-membership-button" %>
</p>
<% end %>
+133 -61
View File
@@ -29,7 +29,7 @@
#++
module Members
class RowComponent < ::RowComponent
class RowComponent < ::RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
property :principal
delegate :project, to: :table
@@ -75,17 +75,52 @@ module Members
end
def shared
count = member.shared_work_packages_count
if count > 0
link_to I18n.t(:label_x_work_packages, count:),
helpers.project_work_packages_shared_with_path(principal, member.project),
target: "_blank", rel: "noopener"
return unless may_view_shared_work_packages?
return if member.shared_work_package_ids.empty?
shared_work_packages_link
end
def shared_work_packages_count = member.shared_work_package_ids.length
def shared_work_packages_link
link_to I18n.t(:label_x_work_packages, count: shared_work_packages_count),
shared_work_packages_url,
target: "_blank",
rel: "noopener"
end
def shared_work_packages_url
if member.other_shared_work_packages_count.zero?
all_shared_work_packages_url
else
helpers.project_work_packages_with_ids_path(member.shared_work_package_ids, member.project)
end
end
delegate :all_shared_work_packages_count, to: :member
def all_shared_work_packages_link
link_to I18n.t(:label_x_work_packages, count: all_shared_work_packages_count),
all_shared_work_packages_url,
target: "_blank",
rel: "noopener"
end
def all_shared_work_packages_url
helpers.project_work_packages_shared_with_path(principal, member.project)
end
def administration_settings_link
link_to "administration settings",
edit_user_path(model.principal, tab: :groups),
target: "_blank",
rel: "noopener"
end
def roles_label
project_roles = member.roles.select { |role| role.is_a?(ProjectRole) }.uniq.sort
label = h project_roles.collect(&:name).join(', ')
project_roles = member.roles.grep(ProjectRole).uniq.sort
label = h project_roles.collect(&:name).join(", ")
if principal&.admin?
label << tag(:br) if project_roles.any?
@@ -97,7 +132,7 @@ module Members
def role_form
render Members::RoleFormComponent.new(
member,
member.project_role? ? member : Member.new(project:, principal:),
row: self,
params: controller.params,
roles: table.available_roles
@@ -114,42 +149,105 @@ module Members
helpers.translate_user_status(model.principal.status)
end
def may_update?
table.authorize_update
end
def shared_work_packages? = member.shared_work_package_ids.present?
def may_delete?
table.authorize_update
end
def may_update? = table.authorize_update
def may_delete? = table.authorize_delete
def may_view_shared_work_packages? = table.authorize_work_package_shares_view
def may_delete_shares? = table.authorize_work_package_shares_delete
def may_manage_user? = table.authorize_manage_user
def can_update? = may_update? && !table.hide_roles?
def can_delete? = may_delete? && member.project_role? && member.deletable?
def can_delete_roles? = may_delete? && member.project_role? && member.some_roles_deletable?
def can_view_shared_work_packages? = may_view_shared_work_packages? && shared_work_packages?
def button_links
if !model.project_role?
[share_warning]
elsif may_update? && may_delete?
[edit_link, delete_link].compact
elsif may_delete?
[delete_link].compact
return [] if actions.empty?
if actions.one?
actions.first => {label:, **button_options}
[render(Primer::Beta::IconButton.new(**button_options, size: :small, "aria-label": label))]
else
[]
[
render(Primer::Alpha::ActionMenu.new) do |menu|
menu.with_show_button(scheme: :invisible, size: :small, icon: :"kebab-horizontal", "aria-label": t(:button_actions),
tooltip_direction: :w)
actions.each do |action_options|
action_options => {scheme:, label:, icon:, **button_options}
menu.with_item(scheme:, label:, content_arguments: button_options) do |item|
item.with_leading_visual_icon(icon:)
end
end
end
]
end
end
def share_warning
content_tag(:span,
title: I18n.t('members.no_modify_on_shared')) do
helpers.op_icon('icon icon-info1')
def actions
@actions ||= [].tap do |actions|
actions << edit_action_options if can_update?
actions << view_work_package_shares_action_options if can_view_shared_work_packages?
actions << delete_action_options if may_delete? && member.project_role?
actions << delete_work_package_shares_action_options if may_delete_shares? && shared_work_packages?
end
end
def edit_link
link_to(
helpers.op_icon('icon icon-edit'),
'#',
class: "toggle-membership-button #{toggle_item_class_name}",
'data-action': 'members-form#toggleMembershipEdit',
'data-members-form-toggling-class-param': toggle_item_class_name,
title: t(:button_edit)
)
def edit_action_options
{
scheme: :default,
icon: :pencil,
label: I18n.t(:button_manage_roles),
data: {
action: "members-form#toggleMembershipEdit",
members_form_toggling_class_param: toggle_item_class_name
}
}
end
def delete_action_options
dialog = Members::DeleteMemberDialogComponent.new(member, row: self)
content_for :content_body do
render(dialog)
end
{
scheme: :danger,
icon: "op-person-remove",
label: I18n.t(:button_remove_member),
data: {
show_dialog_id: dialog.id
}
}
end
def view_work_package_shares_action_options
{
scheme: :default,
tag: :a,
icon: "op-view-list",
label: I18n.t(:button_view_shared_work_packages),
href: shared_work_packages_url
}
end
def delete_work_package_shares_action_options
dialog = Members::DeleteWorkPackageSharesDialogComponent.new(member, row: self)
content_for :content_body do
render(dialog)
end
{
scheme: :danger,
icon: :trash,
label: I18n.t(:button_revoke_work_package_shares),
data: {
show_dialog_id: dialog.id
}
}
end
def roles_css_id
@@ -160,32 +258,6 @@ module Members
"member-#{member.id}--edit-toggle-item"
end
def delete_link
if model.deletable?
link_to(
helpers.op_icon('icon icon-delete'),
{ controller: '/members', action: 'destroy', id: model, page: params[:page] },
method: :delete,
data: { confirm: delete_link_confirmation, disable_with: I18n.t(:label_loading) },
title: delete_title
)
end
end
def delete_title
if model.disposable?
I18n.t(:title_remove_and_delete_user)
else
I18n.t(:button_remove)
end
end
def delete_link_confirmation
if !User.current.admin? && model.include?(User.current)
t(:text_own_membership_delete_confirmation)
end
end
def column_css_class(column)
if column == :mail
"email"
+31 -14
View File
@@ -29,8 +29,16 @@
#++
module Members
class TableComponent < ::TableComponent
options :authorize_update, :available_roles, :is_filtered, :project
class TableComponent < ::TableComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
options :authorize_update,
:authorize_delete,
:authorize_work_package_shares_view,
:authorize_work_package_shares_delete,
:authorize_manage_user,
:available_roles,
:is_filtered,
:shared_role_name,
:project
columns :name, :mail, :roles, :groups, :shared, :status
sortable_columns :name, :mail, :status
@@ -40,7 +48,7 @@ module Members
def apply_member_scopes(model)
model
.with_shared_work_packages_count(only_role_id:)
.with_shared_work_packages_info(only_role_id: shared_role_id)
# This additional select is necessary for removing "duplicate" memberships in the members table
# In reality, we want to show distinct principals in the members page, but are filtering on the members
# table which now has multiplpe entries per user if they are the recipient of multiple shares,
@@ -52,24 +60,29 @@ module Members
Member
.where(
id: model
.reselect('DISTINCT ON (members.user_id) members.id')
.reorder('members.user_id, members.entity_type NULLS FIRST')
.reselect("DISTINCT ON (members.user_id) members.id")
.reorder("members.user_id, members.entity_type NULLS FIRST")
)
end
def only_role_id
case params[:shared_role_id]
when 'all', nil
nil
else
params[:shared_role_id]
end
def shared_role_id
params[:shared_role_id] if params[:shared_role_id].present? && params[:shared_role_id] != "all"
end
def initial_sort
%i[name asc]
end
def hide_roles? = params[:shared_role_id].present?
def columns
if hide_roles?
super - [:roles]
else
super
end
end
def headers
columns.map do |name|
[name.to_s, header_options(name)]
@@ -80,7 +93,7 @@ module Members
caption =
case name
when :shared
I18n.t('members.columns.shared')
I18n.t("members.columns.shared")
else
User.human_attribute_name(name)
end
@@ -88,6 +101,10 @@ module Members
{ caption: }
end
def container_class
"generic-table--container_visible-overflow"
end
##
# Adjusts the order so that users are joined to support
# sorting by their attributes
@@ -103,7 +120,7 @@ module Members
if is_filtered
I18n.t :notice_no_principals_found
else
I18n.t :'members.index.no_results_title_text'
I18n.t :"members.index.no_results_title_text"
end
end
end
@@ -44,11 +44,15 @@ module OpPrimer
render(OpPrimer::ComponentCollectionComponent.new(**), &)
end
def border_box_container(**system_args, &)
Primer::Beta::BorderBox.new(position: :relative, **system_args, &)
end
def border_box_row(wrapper_arguments, &)
if container
container.with_row(**wrapper_arguments, &)
else
container = Primer::Beta::BorderBox.new
container = border_box_container
row = container.registered_slots[:rows][:renderable_function]
.bind_call(container, **wrapper_arguments)
@@ -26,6 +26,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<turbo-frame id="<%=turbo_frame_id%>">
<%= content %>
</turbo-frame>
<% if options[:lazy] == true %>
<turbo-frame id="<%=turbo_frame_id%>" src="<%= options[:lazy_source] %>" loading="lazy">
<%= content %>
</turbo-frame>
<% else %>
<turbo-frame id="<%=turbo_frame_id%>">
<%= content %>
</turbo-frame>
<% end %>
@@ -1,24 +1,46 @@
<div
data-controller="attribute"
data-application-target="dynamic"
data-attribute-background-reference-id-value="<%= background_reference_id %>"
class="op-long-text-attribute">
<%= render(
Primer::Beta::Text.new(tag: :div,
classes: ['op-long-text-attribute--text', PARAGRAPH_CSS_CLASS],
color: text_color,
style: "max-height: #{max_height};",
data: {
'attribute-target': "descriptionText"
})) { short_text } %>
})) { short_text }
%>
<%= render(
Primer::Beta::Text.new(tag: :div,
display: display_expand_button_value,
classes: 'op-long-text-attribute--text-hider',
data: { 'attribute-target': 'textHider' }))
%>
<%= render(
Primer::Alpha::HiddenTextExpander.new(inline: false,
"aria-label": I18n.t('label_attribute_expand_text', attribute: name),
display: display_expand_button_value,
data: {
'attribute-target': 'expandButton',
'test-selector': 'expand-button'
},
button_arguments: { 'data-show-dialog-id': id },
classes: 'op-long-text-attribute--text-expander'
))
%>
<%= render(
Primer::Alpha::Dialog.new(id: id,
data: {
'test-selector': 'attribute-dialog'
},
title: name,
size: :large)) do |component|
component.with_show_button(scheme: :link,
display: display_expand_button_value,
ml: 1,
data: { 'attribute-target': 'expandButton' }) { I18n.t('js.label_expand') }
component.with_body(mt: 2) { full_text }
component.with_header(variant: :large)
end
@@ -32,16 +32,22 @@ module OpenProject
class AttributeComponent < Primer::Component
attr_reader :id,
:name,
:description
:description,
:lines,
:background_reference_id,
:formatted
PARAGRAPH_CSS_CLASS = "op-uc-p".freeze
def initialize(id, name, description, **args)
def initialize(id, name, description, lines: 1, background_reference_id: "content", formatted: false, **args)
super
@id = id
@name = name
@description = description
@system_arguments = args
@lines = lines
@background_reference_id = background_reference_id
@formatted = formatted
end
def short_text
@@ -53,7 +59,7 @@ module OpenProject
end
def full_text
@full_text ||= helpers.format_text(description)
@full_text ||= formatted ? description : helpers.format_text(description)
end
def display_expand_button_value
@@ -64,8 +70,20 @@ module OpenProject
:muted if multi_type?
end
def max_height
"#{lines * 1.6}em"
end
private
def first_paragraph_content
return unless first_paragraph_ast
first_paragraph_ast
.inner_html
.html_safe # rubocop:disable Rails/OutputSafety
end
def first_paragraph
@first_paragraph ||= if body_children.any?
body_children
@@ -77,6 +95,13 @@ module OpenProject
end
end
def first_paragraph_ast
@first_paragraph_ast ||= text_ast
.xpath("html/body")
.children
.first
end
def text_ast
@text_ast ||= Nokogiri::HTML(full_text)
end
@@ -88,7 +113,10 @@ module OpenProject
end
def multi_type?
first_paragraph.include?("figure") || first_paragraph.include?("macro")
@multi_type ||= (description.present? && first_paragraph_ast.nil?) ||
%w[opce-macro-embedded-table figure macro].include?(first_paragraph_ast.name) ||
first_paragraph_ast.css("figure, macro, .op-uc-toc--list, .opce-macro-embedded-table")&.any? ||
(body_children.any? && first_paragraph.blank?)
end
end
end
@@ -1,7 +1,17 @@
.op-long-text-attribute
display: flex
align-items: center
position: relative
&--text
@include text-shortener
overflow: hidden
margin: 0
flex-grow: 1
&--text-hider
position: absolute
bottom: 0
right: 0
height: 1.5em
width: 2em
&--text-expander
position: absolute
bottom: 1px
right: 0
float: right
@@ -42,9 +42,11 @@ module OpenProject
end
def call
render(Primer::Beta::Text.new) do
localized_parts.join(separator)
end
render(Primer::Beta::Text.new) { text }
end
def text
localized_parts.join(separator)
end
private
@@ -4,36 +4,47 @@
# 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) do %>
<%= primer_form_with(
url: projects_path,
id: COLUMN_FORM_ID,
id: QUERY_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
}%>
'params-from-query-allowed-value': '["filters", "query_id", "page", "per_page"]'
}) do |form| %>
<%= render(Primer::Alpha::TabPanels.new(label: "label")) do |tab_panel| %>
<% tab_panel.with_tab(selected: true, id: "tab-selects") do |tab| %>
<% tab.with_text { I18n.t("label_columns") } %>
<% tab.with_panel do %>
<%= helpers.angular_component_tag 'opce-draggable-autocompleter',
inputs: {
options: helpers.projects_columns_options,
selected: selected_columns,
protected: helpers.protected_projects_columns_options,
name: COLUMN_HTML_NAME,
id: 'columns-select',
inputLabel: I18n.t(:'queries.configure_view.columns.input_label'),
inputPlaceholder: I18n.t(:'queries.configure_view.columns.input_placeholder'),
dragAreaLabel: I18n.t(:'queries.configure_view.columns.drag_area_label'),
appendToComponent: true
}%>
<% end %>
<% end %>
<% tab_panel.with_tab(id: "tab-selects") do |tab| %>
<% tab.with_text { I18n.t("label_sort") }%>
<% tab.with_panel do %>
<%= render(Queries::SortByComponent.new(query: query, selectable_columns:)) %>
<% end%>
<% end %>
<% end %>
<% end %>
<% end %>
<%= render(Primer::Alpha::Dialog::Footer.new) do %>
<%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(Primer::ButtonComponent.new(scheme: :primary, type: :submit, form: COLUMN_FORM_ID)) { I18n.t(:button_apply) } %>
<%= render(Primer::ButtonComponent.new(scheme: :primary, type: :submit, form: QUERY_FORM_ID)) { I18n.t(:button_apply) } %>
<% end %>
<% end %>
@@ -30,11 +30,17 @@
class Projects::ConfigureViewModalComponent < ApplicationComponent
MODAL_ID = "op-project-list-configure-dialog"
COLUMN_FORM_ID = "op-project-list-configure-columns-form"
QUERY_FORM_ID = "op-project-list-configure-query-form"
COLUMN_HTML_NAME = "columns"
options :query
def selectable_columns
@selectable_columns ||= [
{ id: :lft, name: I18n.t(:label_project_hierarchy) }
] + helpers.projects_columns_options
end
def selected_columns
@selected_columns ||= query
.selects
@@ -0,0 +1,6 @@
<p class="information-section">
<%= helpers.op_icon('icon-info1') %>
<%= t(:label_projects_disk_usage_information,
count: Project.count,
used_disk_space: number_to_human_size(Project.total_projects_size, precision: 2)) %>
</p>
@@ -28,7 +28,7 @@
# See COPYRIGHT and LICENSE files for more details.
# ++
class Projects::StorageInformationComponent < ApplicationComponent
class Projects::DiskUsageInformationComponent < ApplicationComponent
options :current_user
def render?
@@ -1,37 +1,42 @@
<%= render(Primer::OpenProject::PageHeader.new) do |header| %>
<% if show_state? %>
<% header.with_title(data: { 'test-selector': 'project-query-name'}) { page_title } %>
<% 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, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
<% header.with_actions do %>
<% if query_saveable? %>
<%= render(
Primer::Beta::Text.new(tag: :span,
mr: BUTTON_MARGIN_RIGHT,
color: :muted)) do
t('lists.can_be_saved_as')
if can_save?
header_save_action(
header:,
message: t("lists.can_be_saved"),
label: t("button_save"),
href: projects_query_path(query),
method: :patch
)
elsif can_save_as?
header_save_action(
header:,
message: t("lists.can_be_saved_as"),
label: t("button_save_as"),
href: new_projects_query_path
)
end
header.with_action_menu(menu_arguments: {
anchor_align: :end
},
button_arguments: {
icon: "op-kebab-vertical",
"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
%>
<%= render(
Primer::Beta::Button.new(scheme: :invisible,
size: :medium,
mr: BUTTON_MARGIN_RIGHT,
tag: :a,
href: new_projects_query_path,
data: {
controller: "params-from-query",
'application-target': "dynamic",
'params-from-query-allowed-value': '["filters", "columns"]'
},
classes: 'Button--invisibleOP')) do |button|
button.with_leading_visual_icon(icon: :'op-save')
t('button_save_as')
end %>
<% end %>
<%= render(Primer::Alpha::ActionMenu.new) do |menu|
menu.with_show_button(icon: 'op-kebab-vertical', 'aria-label': t(:label_more), data: { 'test-selector': 'project-more-dropdown-menu' })
end
if gantt_portfolio_project_ids.any?
menu.with_item(
@@ -53,20 +58,12 @@
item.with_leading_visual_icon(icon: 'tasklist')
end
if query_saveable?
menu.with_item(
label: t('button_save_as'),
href: new_projects_query_path,
content_arguments: {
data: {
controller: "params-from-query",
'application-target': "dynamic",
'params-from-query-allowed-value': '["filters", "columns"]'
}
}
) do |item|
item.with_leading_visual_icon(icon: :'op-save')
end
if can_save?
menu_save_item(menu:, label: t('button_save'), href: projects_query_path(query), method: :patch)
end
if may_save_as?
menu_save_item(menu:, label: t('button_save_as'), href: new_projects_query_path)
end
menu.with_item(
@@ -84,6 +81,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,
@@ -93,28 +113,28 @@
end
end
end
%>
<% end %>
<% else %>
<% header.with_title(mt: 2, mb: 2, data: { 'test-selector': 'project-query-name'}) do
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_path(@query),
scope: 'query',
data: {
controller: "params-from-query",
'application-target': "dynamic",
'params-from-query-allowed-value': '["filters", "columns"]'
'params-from-query-allowed-value': '["filters", "columns", "query_id", "sortBy"]'
},
id: 'project-save-form') do |f|
render(Queries::Projects::Create.new(f))
render(Queries::Projects::Form.new(f, query:))
end
end %>
<% end %>
<% end %>
<% if show_state? %>
<%= render(Projects::ConfigureViewModalComponent.new(query:)) %>
<%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %>
<%= render(Projects::ExportListModalComponent.new(query:)) %>
end
header.with_breadcrumbs(breadcrumb_items)
end
%>
<% end %>
@@ -37,11 +37,7 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
:state,
:params
BUTTON_MARGIN_RIGHT = 2
STATE_DEFAULT = :show
STATE_EDIT = :edit
STATE_OPTIONS = [STATE_DEFAULT, STATE_EDIT].freeze
STATE_OPTIONS = %i[show edit rename].freeze
def initialize(current_user:, query:, params:, state: :show)
super
@@ -70,11 +66,108 @@ 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?
state == :show
end
def breadcrumb_items
[
{ href: projects_path, text: t(:label_project_plural) },
current_breadcrumb_element
]
end
def current_breadcrumb_element
return page_title if query.name.blank?
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:,
controller: "params-from-query",
"application-target": "dynamic",
"params-from-query-allowed-value": '["filters", "columns", "sortBy", "query_id"]'
}.compact
) do
render(
Primer::Beta::Octicon.new(
icon: "op-save",
align_self: :center,
"aria-label": label,
mr: 1
)
) + content_tag(:span, label)
end
end
def menu_save_item(menu:, label:, href:, method: nil)
menu.with_item(
label:,
href:,
content_arguments: {
data: {
method:,
controller: "params-from-query",
"application-target": "dynamic",
"params-from-query-allowed-value": '["filters", "columns", "sortBy", "query_id"]'
}.compact
}
) do |item|
item.with_leading_visual_icon(icon: :"op-save")
end
end
end
@@ -47,7 +47,8 @@ class Projects::ProjectsFiltersComponent < FiltersComponent
Queries::Projects::Filters::CreatedAtFilter,
Queries::Projects::Filters::LatestActivityAtFilter,
Queries::Projects::Filters::NameAndIdentifierFilter,
Queries::Projects::Filters::TypeFilter
Queries::Projects::Filters::TypeFilter,
Queries::Projects::Filters::FavoredFilter
]
allowlist << Queries::Filters::Shared::CustomFields::Base if EnterpriseToken.allows_to?(:custom_fields_in_projects_list)
+3 -14
View File
@@ -37,19 +37,8 @@ See COPYRIGHT and LICENSE files for more details.
</td>
<% end %>
<td class="buttons">
<% if more_menu_items.any? %>
<ul class="project-actions">
<li aria-haspopup="true" title="<%= I18n.t(:label_open_menu) %>" class="drop-down">
<a class="icon icon-show-more-horizontal context-menu--icon" title="<%= t(:label_open_menu) %>" href></a>
<ul style="display:none;" class="menu-drop-down-container">
<% more_menu_items.each do |item| %>
<li>
<%= link_to(*item) %>
</li>
<% end %>
</ul>
</li>
</ul>
<% button_links.each do |link| %>
<%= link %>
<% end %>
</td>
</tr>
</tr>
+140 -37
View File
@@ -29,6 +29,8 @@
#++
module Projects
class RowComponent < ::RowComponent
delegate :favored_project_ids, to: :table
def project
model.first
end
@@ -42,6 +44,27 @@ module Projects
""
end
def favored
render(Primer::Beta::IconButton.new(
icon: currently_favored? ? "star-fill" : "star",
scheme: :invisible,
mobile_icon: currently_favored? ? "star-fill" : "star",
size: :medium,
tag: :a,
tooltip_direction: :e,
href: helpers.build_favorite_path(project, format: :html),
data: { method: currently_favored? ? :delete : :post },
classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite",
label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
test_selector: "project-list-favorite-button"
))
end
def currently_favored?
@currently_favored ||= favored_project_ids.include?(project.id)
end
def column_value(column)
if custom_field_column?(column)
custom_field_column(column)
@@ -57,7 +80,12 @@ module Projects
custom_value = project.formatted_custom_value_for(cf)
if cf.field_format == "text" && custom_value.present?
render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-cf-#{cf.id}", cf.name, custom_value.html_safe) # rubocop:disable Rails/OutputSafety
render OpenProject::Common::AttributeComponent.new(
"dialog-#{project.id}-cf-#{cf.id}",
cf.name,
custom_value,
formatted: true
)
elsif custom_value.is_a?(Array)
safe_join(Array(custom_value).compact_blank, ", ")
else
@@ -133,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}"
@@ -165,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
@@ -174,82 +206,153 @@ module Projects
end
end
def button_links
if more_menu_items.empty?
[]
else
[action_menu]
end
end
def action_menu
render(Primer::Alpha::ActionMenu.new(test_selector: "project-list-row--action-menu")) do |menu|
menu.with_show_button(scheme: :invisible,
size: :small,
icon: :"kebab-horizontal",
"aria-label": t(:label_open_menu),
tooltip_direction: :w)
more_menu_items.each do |action_options|
action_options => { scheme:, label:, icon:, **button_options }
menu.with_item(scheme:,
label:,
test_selector: "project-list-row--action-menu-item",
content_arguments: button_options) do |item|
item.with_leading_visual_icon(icon:)
end
end
end
end
def more_menu_items
@more_menu_items ||= [more_menu_subproject_item,
more_menu_settings_item,
more_menu_activity_item,
more_menu_favorite_item,
more_menu_unfavorite_item,
more_menu_archive_item,
more_menu_unarchive_item,
more_menu_copy_item,
more_menu_delete_item].compact
end
def more_menu_favorite_item
return if currently_favored?
{
scheme: :default,
icon: "star",
href: helpers.build_favorite_path(project, format: :html),
data: { method: :post },
label: I18n.t(:button_favorite),
aria: { label: I18n.t(:button_favorite) }
}
end
def more_menu_unfavorite_item
return unless currently_favored?
{
scheme: :default,
icon: "star-fill",
size: :medium,
href: helpers.build_favorite_path(project, format: :html),
data: { method: :delete },
classes: "op-primer--star-icon",
label: I18n.t(:button_unfavorite),
aria: { label: I18n.t(:button_unfavorite) }
}
end
def more_menu_subproject_item
if User.current.allowed_in_project?(:add_subprojects, project)
[t(:label_subproject_new),
new_project_path(parent_id: project.id),
{ class: "icon-context icon-add",
title: t(:label_subproject_new) }]
{
scheme: :default,
icon: :plus,
label: I18n.t(:label_subproject_new),
href: new_project_path(parent_id: project.id)
}
end
end
def more_menu_settings_item
if User.current.allowed_in_project?({ controller: "/projects/settings/general", action: "show", project_id: project.id },
project)
[t(:label_project_settings),
project_settings_general_path(project),
{ class: "icon-context icon-settings",
title: t(:label_project_settings) }]
{
scheme: :default,
icon: :gear,
label: I18n.t(:label_project_settings),
href: project_settings_general_path(project)
}
end
end
def more_menu_activity_item
if User.current.allowed_in_project?(:view_project_activity, project)
[
t(:label_project_activity),
project_activity_index_path(project, event_types: ["project_attributes"]),
{ class: "icon-context icon-checkmark",
title: t(:label_project_activity) }
]
{
scheme: :default,
icon: :check,
label: I18n.t(:label_project_activity),
href: project_activity_index_path(project, event_types: ["project_attributes"])
}
end
end
def more_menu_archive_item
if User.current.allowed_in_project?(:archive_project, project) && project.active?
[t(:button_archive),
project_archive_path(project, status: params[:status]),
{ data: { confirm: t("project.archive.are_you_sure", name: project.name) },
method: :post,
class: "icon-context icon-locked",
title: t(:button_archive) }]
{
scheme: :default,
icon: :lock,
label: I18n.t(:button_archive),
href: project_archive_path(project, status: params[:status]),
data: {
confirm: t("project.archive.are_you_sure", name: project.name),
method: :post
}
}
end
end
def more_menu_unarchive_item
if User.current.admin? && project.archived? && (project.parent.nil? || project.parent.active?)
[t(:button_unarchive),
project_archive_path(project, status: params[:status]),
{ method: :delete,
class: "icon-context icon-unlocked",
title: t(:button_unarchive) }]
{
scheme: :default,
icon: :unlock,
label: I18n.t(:button_unarchive),
href: project_archive_path(project, status: params[:status]),
data: { method: :delete }
}
end
end
def more_menu_copy_item
if User.current.allowed_in_project?(:copy_projects, project) && !project.archived?
[t(:button_copy),
copy_project_path(project),
{ class: "icon-context icon-copy",
title: t(:button_copy) }]
{
scheme: :default,
icon: :copy,
label: I18n.t(:button_copy),
href: copy_project_path(project)
}
end
end
def more_menu_delete_item
if User.current.admin
[t(:button_delete),
confirm_destroy_project_path(project),
{ class: "icon-context icon-delete",
title: t(:button_delete) }]
{
scheme: :danger,
icon: :trash,
label: I18n.t(:button_delete),
href: confirm_destroy_project_path(project)
}
end
end
@@ -258,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,15 +1,15 @@
<%=
component_wrapper do
flex_layout(align_items: :center, justify_content: :space_between, classes: 'op-project-custom-field', data: {
qa_selector: "project-custom-field-#{@project_custom_field.id}"
test_selector: "project-custom-field-#{@project_custom_field.id}"
}) do |custom_field_container|
custom_field_container.with_column(flex_layout: true) do |title_container|
title_container.with_column(pt: 1, mr: 2) do
render(Primer::Beta::Text.new) do
render(Primer::Beta::Text.new) do
@project_custom_field.name
end
end
title_container.with_column(pt: 1, mr: 2, data: { qa_selector: "custom-field-type" } ) do
title_container.with_column(pt: 1, mr: 2, data: { test_selector: "custom-field-type" } ) do
render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do
@project_custom_field.field_format.capitalize
end
@@ -37,7 +37,7 @@
),
csrf_token: form_authenticity_token,
data: { 'turbo-method': :put, 'turbo-stream': true,
qa_selector: "toggle-project-custom-field-mapping-#{@project_custom_field.id}" },
test_selector: "toggle-project-custom-field-mapping-#{@project_custom_field.id}" },
checked: active_in_project?,
enabled: !@project_custom_field.required?, # required fields cannot be disabled
size: :small,
@@ -0,0 +1,8 @@
<%= render Primer::OpenProject::PageHeader.new do |header| %>
<%= header.with_title(variant: :default) { t('projects.settings.project_custom_fields.header.title') } %>
<%= header.with_description { t('projects.settings.project_custom_fields.header.description',
overview_url: project_path(@project),
admin_settings_url: admin_settings_project_custom_fields_path
).html_safe } %>
<%= header.with_breadcrumbs(breadcrumb_items) %>
<% end %>
@@ -2,7 +2,7 @@
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2023 the OpenProject GmbH
# Copyright (C) 2010-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.
@@ -27,32 +27,21 @@
#
# See COPYRIGHT and LICENSE files for more details.
# ++
#
module Meetings
class AddButtonComponent < ::AddButtonComponent
def render?
if current_project
User.current.allowed_in_project?(:create_meetings, current_project)
else
User.current.allowed_in_any_project?(:create_meetings)
end
module Projects::Settings::ProjectCustomFieldSections
# rubocop:disable OpenProject/AddPreviewForViewComponent
class IndexPageHeaderComponent < ApplicationComponent
include ApplicationHelper
def initialize(project: nil)
super
@project = project
end
def dynamic_path
polymorphic_path([:new, current_project, :meeting])
end
def id
"add-meeting-button"
end
def accessibility_label_text
I18n.t(:label_meeting_new)
end
def label_text
I18n.t(:label_meeting)
def breadcrumb_items
[{ href: project_overview_path(@project.id), text: @project.name },
{ href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") },
t("settings.project_attributes.heading")]
end
end
end
@@ -1,7 +1,7 @@
<%=
component_wrapper do
render(Primer::Beta::BorderBox.new(mt: 3, classes: 'op-project-custom-field-section', data: {
qa_selector: "project-custom-field-section-#{@project_custom_field_section.id}"
render(border_box_container(mt: 3, classes: 'op-project-custom-field-section', data: {
test_selector: "project-custom-field-section-#{@project_custom_field_section.id}"
})) do |component|
component.with_header(font_weight: :bold, py: 2) do
flex_layout(justify_content: :space_between, align_items: :center) do |section_header_container|
@@ -26,7 +26,7 @@
font_weight: :bold,
color: :subtle,
'aria-label': t('projects.settings.project_custom_fields.actions.label_enable_all'),
data: { 'turbo-method': :put, 'turbo-stream': true, qa_selector: "enable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
data: { 'turbo-method': :put, 'turbo-stream': true, test_selector: "enable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
)) do |button|
button.with_leading_visual_icon(icon: 'check-circle', color: :subtle)
t('projects.settings.project_custom_fields.actions.label_enable_all')
@@ -45,7 +45,7 @@
font_weight: :bold,
color: :subtle,
'aria-label': t('projects.settings.project_custom_fields.actions.label_disable_all'),
data: { 'turbo-method': :put, 'turbo-stream': true, qa_selector: "disable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
data: { 'turbo-method': :put, 'turbo-stream': true, test_selector: "disable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
)) do |button|
button.with_leading_visual_icon(icon: 'x-circle', color: :subtle)
t('projects.settings.project_custom_fields.actions.label_disable_all')
@@ -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>
@@ -27,78 +27,85 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<div class="generic-table--flex-container">
<div class="generic-table--container">
<div class="generic-table--results-container">
<table class="generic-table" <%= table_id ? "id=\"#{table_id}\"".html_safe : '' %>>
<colgroup>
<% columns.each do |column| %>
<col <%= "opHighlightCol" unless column.attribute == :hierarchy %> >
<% end %>
<col opHighlightCol>
</colgroup>
<thead class="-sticky">
<tr>
<% columns.each do |column| %>
<% 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"]'>
<%= content_tag :a,
helpers.op_icon("icon-hierarchy"),
href: href_only_when_not_sort_lft,
class: "spot-link #{deactivate_class_on_lft_sort}",
title: t(:label_sort_by, value: %("#{t(:label_project_hierarchy)}")) %>
</div>
</div>
</th>
<% elsif sortable_column?(column) %>
<%= build_sort_header column.attribute,
order_options(column) %>
<% else %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= column.caption %>
</span>
</div>
</div>
</th>
<div class="project-list-page--table">
<div class="generic-table--flex-container">
<div class="generic-table--container <%= container_class %>">
<div class="generic-table--results-container">
<table class="generic-table" <%= table_id ? "id=\"#{table_id}\"".html_safe : '' %>>
<colgroup>
<% columns.each do |column| %>
<col <%= "opHighlightCol" unless column.attribute == :hierarchy %> >
<% end %>
<% end %>
<th class="-right">
<div class="generic-table--header-outer">
</div>
</th>
</tr>
</thead>
<tbody>
<% if rows.empty? %>
<tr class="generic-table--empty-row">
<td colspan="<%= headers.length + 1 %>"><%= empty_row_message %></td>
<col>
</colgroup>
<thead class="-sticky">
<tr>
<% columns.each do |column| %>
<% 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"]'>
<%= content_tag :a,
helpers.op_icon("icon-hierarchy"),
href: href_only_when_not_sort_lft,
class: "spot-link #{deactivate_class_on_lft_sort}",
title: t(:label_sort_by, value: %("#{t(:label_project_hierarchy)}")) %>
</div>
</div>
</th>
<% elsif sortable_column?(column) %>
<%= build_sort_header column.attribute,
order_options(column) %>
<% else %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<% if column.attribute == :favored %>
<%= render(Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, ml: 2, "aria-label": I18n.t(:label_favorite))) %>
<% else %>
<%= column.caption %>
<% end %>
</span>
</div>
</div>
</th>
<% end %>
<% end %>
<th>
<div class="generic-table--empty-header">
</div>
</th>
</tr>
</thead>
<tbody>
<% if rows.empty? %>
<tr class="generic-table--empty-row">
<td colspan="<%= columns.length + 1 %>"><%= empty_row_message %></td>
</tr>
<% end %>
<%= render_collection rows %>
</tbody>
</table>
<% if inline_create_link && show_inline_create %>
<div class="wp-inline-create-button">
<%= inline_create_link %>
</div>
<% end %>
<%= render_collection rows %>
</tbody>
</table>
<% if inline_create_link && show_inline_create %>
<div class="wp-inline-create-button">
<%= inline_create_link %>
</div>
<% end %>
</div>
</div>
</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>
<% end %>
</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>
<% end %>
+11 -3
View File
@@ -51,6 +51,10 @@ module Projects
"project-table"
end
def container_class
"generic-table--container_visible-overflow"
end
##
# The project sort by is handled differently
def build_sort_header(column, options)
@@ -61,7 +65,7 @@ module Projects
# 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
@@ -109,10 +113,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
@@ -149,6 +153,10 @@ module Projects
end
end
def favored_project_ids
@favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id)
end
def sorted_by_lft?
query.orders.first&.attribute == :lft
end
@@ -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
@@ -1,5 +1,5 @@
<%=
component_wrapper(class: "op-project-custom-field-container", data: { qa_selector: "project-custom-field-container-#{@project_custom_field.id}" }) do
component_wrapper(class: "op-project-custom-field-container", data: { test_selector: "project-custom-field-container-#{@project_custom_field.id}" }) do
flex_layout(justify_content: :space_between, align_items: :center) do |main_container|
main_container.with_column(flex_layout: true, align_items: :center) do |content_container|
content_container.with_column(mr: 2) do
@@ -36,7 +36,7 @@
end
end
main_container.with_column do
render(Primer::Alpha::ActionMenu.new(data: { qa_selector: "project-custom-field-action-menu" })) do |menu|
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "project-custom-field-action-menu" })) do |menu|
menu.with_show_button(icon: "kebab-horizontal", 'aria-label': t("settings.project_attributes.label_project_custom_field_actions"), scheme: :invisible)
edit_action_item(menu)
move_actions(menu)
@@ -45,7 +45,7 @@ module Settings
def edit_action_item(menu)
menu.with_item(label: t("label_edit"),
href: edit_admin_settings_project_custom_field_path(@project_custom_field),
data: { turbo: "false", qa_selector: "project-custom-field-edit" }) do |item|
data: { turbo: "false", test_selector: "project-custom-field-edit" }) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
@@ -68,7 +68,7 @@ module Settings
menu.with_item(label: label_text,
href: move_admin_settings_project_custom_field_path(@project_custom_field, move_to:),
form_arguments: {
method: :put, data: { 'turbo-stream': true, qa_selector: "project-custom-field-move-#{move_to}" }
method: :put, data: { "turbo-stream": true, test_selector: "project-custom-field-move-#{move_to}" }
}) do |item|
item.with_leading_visual_icon(icon:)
end
@@ -79,8 +79,8 @@ module Settings
scheme: :danger,
href: admin_settings_project_custom_field_path(@project_custom_field),
form_arguments: {
method: :delete, data: { confirm: t("text_are_you_sure"), 'turbo-stream': true,
qa_selector: "project-custom-field-delete" }
method: :delete, data: { confirm: t("text_are_you_sure_with_project_custom_fields"),
"turbo-stream": true, test_selector: "project-custom-field-delete" }
}) do |item|
item.with_leading_visual_icon(icon: :trash)
end
@@ -2,7 +2,7 @@
component_wrapper do
primer_form_with(**form_config) do |f|
component_collection do |collection|
collection.with_component(Primer::Alpha::Dialog::Body.new) do
collection.with_component(Primer::BaseComponent.new(tag: :div)) do
flex_layout(my: 3) do |modal_body|
modal_body.with_row do
render(ProjectCustomFieldSections::NameForm.new(f))
@@ -1,6 +1,6 @@
<%=
component_wrapper(class: "op-project-custom-field-section-container", data: { qa_selector: "project-custom-field-section-container-#{@project_custom_field_section.id}" }) do
render(Primer::Beta::BorderBox.new(mt: 3, data: drag_and_drop_target_config)) do |component|
component_wrapper(class: "op-project-custom-field-section-container", data: { test_selector: "project-custom-field-section-container-#{@project_custom_field_section.id}" }) do
render(border_box_container(mt: 3, data: drag_and_drop_target_config)) do |component|
component.with_header(font_weight: :bold) do
flex_layout(justify_content: :space_between, align_items: :center) do |section_header_container|
section_header_container.with_column(flex_layout: true, align_items: :center) do |content_container|
@@ -15,7 +15,7 @@
end
section_header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container|
actions_container.with_column do
render(Primer::Alpha::ActionMenu.new(data: { qa_selector: "project-custom-field-section-action-menu" })) do |menu|
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "project-custom-field-section-action-menu" })) do |menu|
menu.with_show_button(icon: "kebab-horizontal", 'aria-label': t("settings.project_attributes.label_section_actions"), scheme: :invisible)
edit_action_item(menu)
move_actions(menu)
@@ -50,7 +50,7 @@
type: "ProjectCustomField", custom_field_section_id: @project_custom_field_section.id
),
scheme: :secondary,
data: { turbo: "false", qa_selector: "new-project-custom-field-button" }
data: { turbo: "false", test_selector: "new-project-custom-field-button" }
)) do |button|
button.with_leading_visual_icon(icon: :plus)
t('settings.project_attributes.label_new_attribute')
@@ -49,18 +49,18 @@ module Settings
def drag_and_drop_target_config
{
'is-drag-and-drop-target': true,
'target-container-accessor': '.Box > ul', # the accessor of the container that contains the drag and drop items
'target-id': @project_custom_field_section.id, # the id of the target
'target-allowed-drag-type': 'custom-field' # the type of dragged items which are allowed to be dropped in this target
"is-drag-and-drop-target": true,
"target-container-accessor": ".Box > ul", # the accessor of the container that contains the drag and drop items
"target-id": @project_custom_field_section.id, # the id of the target
"target-allowed-drag-type": "custom-field" # the type of dragged items which are allowed to be dropped in this target
}
end
def draggable_item_config(project_custom_field)
{
'draggable-id': project_custom_field.id,
'draggable-type': 'custom-field',
'drop-url': drop_admin_settings_project_custom_field_path(project_custom_field)
"draggable-id": project_custom_field.id,
"draggable-type": "custom-field",
"drop-url": drop_admin_settings_project_custom_field_path(project_custom_field)
}
end
@@ -82,7 +82,8 @@ module Settings
menu.with_item(label: label_text,
href: move_admin_settings_project_custom_field_section_path(@project_custom_field_section, move_to:),
form_arguments: {
method: :put, data: { 'turbo-stream': true, qa_selector: "project-custom-field-section-move-#{move_to}" }
method: :put, data: { "turbo-stream": true,
test_selector: "project-custom-field-section-move-#{move_to}" }
}) do |item|
item.with_leading_visual_icon(icon:)
end
@@ -99,8 +100,8 @@ module Settings
menu.with_item(label: t("settings.project_attributes.label_edit_section"),
tag: :button,
content_arguments: {
'data-show-dialog-id': "project-custom-field-section-dialog#{@project_custom_field_section.id}",
'data-qa-selector': "project-custom-field-section-edit"
"data-show-dialog-id": "project-custom-field-section-dialog#{@project_custom_field_section.id}",
"data-test-selector": "project-custom-field-section-edit"
},
value: "") do |item|
item.with_leading_visual_icon(icon: :pencil)
@@ -112,8 +113,8 @@ module Settings
scheme: :danger,
href: admin_settings_project_custom_field_section_path(@project_custom_field_section),
form_arguments: {
method: :delete, data: { confirm: t("text_are_you_sure"), 'turbo-stream': true,
qa_selector: "project-custom-field-section-delete" }
method: :delete, data: { confirm: t("text_are_you_sure"), "turbo-stream": true,
test_selector: "project-custom-field-section-delete" }
}) do |item|
item.with_leading_visual_icon(icon: :trash)
end
@@ -28,11 +28,8 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title(variant: :medium) { @custom_field.name }
header.with_description { t('settings.project_attributes.edit.description') }
header.with_parent_link(href: admin_settings_project_custom_fields_path, 'aria-label': I18n.t("button_back")) do
t('settings.project_attributes.heading')
end
header.with_back_button(href: admin_settings_project_custom_fields_path, 'aria-label': t('button_back'))
header.with_title { @custom_field.name }
header.with_description { t("settings.project_attributes.edit.description") }
header.with_breadcrumbs(breadcrumbs_items)
end
%>
%>
@@ -34,6 +34,14 @@ module Settings
@custom_field = custom_field
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
]
end
end
end
end
@@ -1,34 +1,30 @@
<% button_block = lambda do |button|
button.with_leading_visual_icon(icon: :plus)
t('settings.project_attributes.label_new_section')
end %>
<%=
component_wrapper do
component_wrapper do
render Primer::OpenProject::PageHeader.new do |header|
header.with_title(variant: :default) { t('settings.project_attributes.heading') }
header.with_description { t('settings.project_attributes.heading_description') }
header.with_actions do
flex_layout(justify_content: :space_between, align_items: :center) do |action_buttons_container|
action_buttons_container.with_column(mr: 2) do
render(Primer::Beta::Button.new(
tag: :a,
href: new_admin_settings_project_custom_field_path(type: "ProjectCustomField"),
scheme: :primary,
data: { turbo: "false", qa_selector: "new-project-custom-field-button" }
)) do |button|
button.with_leading_visual_icon(icon: :plus)
t('settings.project_attributes.label_new_attribute')
end
end
action_buttons_container.with_column do
render(Primer::Alpha::Dialog.new(
id: "project-custom-field-section-dialog", title: t('settings.project_attributes.label_new_section'),
size: :medium_portrait
)) do |dialog|
dialog.with_show_button('aria-label': t('settings.project_attributes.label_new_section'), scheme: :default) do |button|
button.with_leading_visual_icon(icon: :plus)
t('settings.project_attributes.label_new_section')
end
render(Settings::ProjectCustomFieldSections::DialogBodyFormComponent.new())
end
end
header.with_title(variant: :default) { t("settings.project_attributes.heading") }
header.with_description { t("settings.project_attributes.heading_description") }
header.with_breadcrumbs(breadcrumbs_items)
header.with_action_button(tag: :a,
href: new_admin_settings_project_custom_field_path(type: "ProjectCustomField"),
scheme: :primary,
data: { turbo: "false", test_selector: "new-project-custom-field-button" },
mobile_icon: :plus,
mobile_label: t("settings.project_attributes.label_new_attribute")) do |button|
button.with_leading_visual_icon(icon: :plus)
t("settings.project_attributes.label_new_attribute")
end
header.with_action_dialog(mobile_icon: :plus,
mobile_label: t('settings.project_attributes.label_new_section'),
dialog_arguments: {id: "project-custom-field-section-dialog", title: t('settings.project_attributes.label_new_section', size: :medium_portrait), size: :medium_portrait},
button_arguments: {'aria-label': t('settings.project_attributes.label_new_section'), button_block: button_block}) do |dialog|
dialog.with_body do
render(Settings::ProjectCustomFieldSections::DialogBodyFormComponent.new)
end
end
end
@@ -32,6 +32,12 @@ module Settings
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
def breadcrumbs_items
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
t("settings.project_attributes.heading")]
end
end
end
end
@@ -29,11 +29,8 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title(variant: :medium) { t('settings.project_attributes.new.heading') }
header.with_title { t('settings.project_attributes.new.heading') }
header.with_description { t('settings.project_attributes.new.description') }
header.with_parent_link(href: admin_settings_project_custom_fields_path, 'aria-label': I18n.t("button_back")) do
t('settings.project_attributes.heading')
end
header.with_back_button(href: admin_settings_project_custom_fields_path, 'aria-label': t('button_back'))
header.with_breadcrumbs(breadcrumb_items)
end
%>
@@ -29,6 +29,12 @@
module Settings
module ProjectCustomFields
class NewFormHeaderComponent < ApplicationComponent
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") },
t("settings.project_attributes.new.heading")]
end
end
end
end
@@ -27,23 +27,16 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t("project_module_storages") %>
<% if ::Storages::Storage.any? %>
<%= toolbar title: t("storages.page_titles.project_settings.index") do %>
<li class="toolbar-item">
<%= link_to new_project_settings_project_storage_path,
{ class: 'button -primary',
aria: { label: t(:'storages.label_new_storage') },
title: t(:'storages.label_new_storage') } do %>
<%= op_icon('button--icon icon-add') %>
<span class="button--text"><%= ::Storages::Storage.model_name.human %></span>
<% end %>
</li>
<%= component_wrapper(tag: "tr", class: row_css_class, data: { turbo: true }) do %>
<% columns.each do |column| %>
<td class="<%= column_css_class(column) %>">
<%= column_value(column) %>
</td>
<% end %>
<%= render(::Storages::ProjectStorages::TableComponent.new(rows: @project_storages)) %>
<% else %>
<%= render partial: '/storages/project_settings/toast_no_storage_set_up' %>
<td class="buttons">
<% button_links.each do |link| %>
<%= link %>
<% end %>
</td>
<% end %>
@@ -0,0 +1,62 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Settings
module ProjectCustomFields
module ProjectCustomFieldMapping
class RowComponent < Projects::RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
include OpTurbo::Streamable
def wrapper_uniq_by
"project-#{project.id}"
end
def more_menu_items
@more_menu_items ||= [more_menu_detach_project].compact
end
private
def more_menu_detach_project
if User.current.admin
{
scheme: :danger,
icon: :trash,
label: I18n.t(:button_delete),
href: unlink_admin_settings_project_custom_field_path(
id: @table.params[:custom_field].id,
project_custom_field_project_mapping: { project_id: model.first.id }
),
data: { turbo_method: :delete }
}
end
end
end
end
end
end
@@ -0,0 +1,32 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) 2012-2024 the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<%= component_wrapper(tag: "div", data: { turbo: true }) do %>
<%= render_parent %>
<% end %>
@@ -0,0 +1,37 @@
#-- 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
end
end
end
end
+2 -5
View File
@@ -59,15 +59,12 @@ module Statuses
[
[:name, { caption: Status.human_attribute_name(:name) }],
[:color, { caption: Status.human_attribute_name(:color) }],
[:done_ratio, { caption: WorkPackage.human_attribute_name(:done_ratio) }],
[:default?, { caption: Status.human_attribute_name(:is_default) }],
[:closed?, { caption: Status.human_attribute_name(:is_closed) }],
[:readonly?, { caption: Status.human_attribute_name(:is_readonly) }],
[:sort, { caption: I18n.t(:label_sort) }]
].tap do |default|
if WorkPackage.use_status_for_done_ratio?
default.insert 2, [:done_ratio, { caption: WorkPackage.human_attribute_name(:done_ratio) }]
end
end
]
end
end
end
+1 -1
View File
@@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<div class="generic-table--container" data-test-selector="<%= test_selector %>">
<div class="generic-table--container <%= container_class %>" data-test-selector="<%= test_selector %>">
<div class="generic-table--results-container">
<table class="generic-table">
<colgroup>
+4
View File
@@ -157,6 +157,10 @@ class TableComponent < ApplicationComponent
self.class.row_class
end
def container_class
nil
end
def columns
self.class.columns
end
@@ -0,0 +1,93 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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 WorkPackages
module Progress
# rubocop:disable OpenProject/AddPreviewForViewComponent
class BaseModalComponent < ApplicationComponent
# rubocop:enable OpenProject/AddPreviewForViewComponent
FIELD_MAP = {
"estimatedTime" => :estimated_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
include Turbo::FramesHelper
include OpPrimer::ComponentHelpers
include OpenProject::StaticRouting::UrlHelpers
attr_reader :work_package, :mode, :focused_field, :touched_field_map
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
if work_package.new_record?
url_for(controller: "work_packages/progress",
action: "create")
else
url_for(controller: "work_packages/progress",
action: "update",
work_package_id: work_package.id)
end
end
def learn_more_href
OpenProject::Static::Links.links[:progress_tracking_docs][:href]
end
private
def map_field(field)
# Scenarios when a field is not provided occur after a
# form submission since the last focused element
# was the submit button. In this case, don't focus on
# an element by default.
return nil if field.nil?
field = FIELD_MAP[field.to_s]
return field if field.present?
raise ArgumentError, "The selected field is not one of #{FIELD_MAP.keys.join(', ')}."
end
end
end
end
@@ -0,0 +1,6 @@
.progress-form
.input--readonly
background-color: unset !important
border: none !important
box-shadow: none !important
@@ -0,0 +1,35 @@
<%= turbo_frame_tag "work_package_progress_modal" do %>
<%= primer_form_with(
model: work_package,
url: submit_path,
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:, touched_field_map:)) %>
<% end %>
<% modal_body.with_row(mt: 3) do |_tooltip| %>
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { t("work_package.progress.label_note") } %>
<%= render(Primer::Beta::Text.new) { t("work_package.progress.modal.status_based_help_text") } %>
<%= render(Primer::Beta::Link.new(href: learn_more_href)) { t(:label_learn_more) } %>
<% end %>
<% modal_body.with_row(mt: 3) do |_actions_row| %>
<%= flex_layout(justify_content: :flex_end) do |action_buttons| %>
<%= action_buttons.with_column do %>
<%= render(Primer::Beta::Button.new(scheme: :primary,
type: :submit)) { t(:button_save) } %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,43 @@
# 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.
# ++
module WorkPackages
module Progress
module StatusBased
class ModalBodyComponent < BaseModalComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
def initialize(work_package, focused_field: nil, touched_field_map: {})
super
@mode = :status_based
end
end
end
end
end
@@ -0,0 +1,41 @@
<%= turbo_frame_tag "work_package_progress_modal" do %>
<%= primer_form_with(
model: work_package,
url: submit_path,
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:, touched_field_map:)) %>
<% end %>
<% if should_display_migration_warning? %>
<% modal_body.with_row(mt: 3) do |_migration_warning| %>
<%= render(Primer::Alpha::Banner.new) { t("work_package.progress.modal.migration_warning_text") } %>
<% end %>
<% end %>
<% modal_body.with_row(mt: 3) do |_tooltip| %>
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { t("work_package.progress.label_note") } %>
<%= render(Primer::Beta::Text.new) { t("work_package.progress.modal.work_based_help_text") } %>
<%= render(Primer::Beta::Link.new(href: learn_more_href)) { t(:label_learn_more) } %>
<% end %>
<% modal_body.with_row(mt: 3) do |_actions_row| %>
<%= flex_layout(justify_content: :flex_end) do |action_buttons| %>
<%= action_buttons.with_column do %>
<%= render(Primer::Beta::Button.new(scheme: :primary,
type: :submit)) { t(:button_save) } %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,50 @@
# 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.
# ++
module WorkPackages
module Progress
module WorkBased
# rubocop:disable OpenProject/AddPreviewForViewComponent
class ModalBodyComponent < BaseModalComponent
def initialize(work_package, focused_field: nil, touched_field_map: {})
super
@mode = :work_based
end
def should_display_migration_warning?
work_package.done_ratio.present? && work_package.estimated_hours.nil? && work_package.remaining_hours.nil?
end
end
# rubocop:enable OpenProject/AddPreviewForViewComponent
end
end
end
@@ -9,7 +9,7 @@
data: { 'test-selector': 'op-share-wp-active-list',
controller: 'work-packages--share--bulk-selection',
application_target: 'dynamic' }) do
render(Primer::Beta::BorderBox.new(list_id: insert_target_modifier_id)) do |border_box|
render(border_box_container(list_id: insert_target_modifier_id)) do |border_box|
border_box.with_header(color: :muted, data: { 'test-selector': 'op-share-wp-header' }) do
grid_layout('op-share-wp-modal-body--header', tag: :div, align_items: :center) do |header_grid|
header_grid.with_area(:counter, tag: :div) do
@@ -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)
+3 -1
View File
@@ -59,8 +59,10 @@ class DeleteContract < ModelContract
user.admin? && user.active?
when Proc
instance_exec(&permission)
when Symbol
model.project && user.allowed_in_project?(permission, model.project)
else
!model.project || user.allowed_in_project?(permission, model.project)
raise ArgumentError, "#{self.class} used without delete_permission. Set a Proc, or project-based permission symbol"
end
end
end
+1 -1
View File
@@ -35,7 +35,7 @@ module Members
private
def member_is_deletable
errors.add(:base, :not_deletable) unless model.deletable?
errors.add(:base, :not_deletable) unless model.some_roles_deletable?
end
end
end
@@ -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 ProjectCustomFieldProjectMappings
class DeleteContract < ::DeleteContract
delete_permission :select_project_custom_fields
end
end
+1
View File
@@ -35,6 +35,7 @@ module Projects
attribute :identifier
attribute :description
attribute :public
attribute :settings
attribute :active do
validate_active_present
validate_changing_active
@@ -41,17 +41,36 @@ module Queries::Projects::ProjectQueries
presence: true,
length: { maximum: 255 }
validate :user_is_current_user_and_logged_in
validate :name_select_included
validate :existing_selects
validate :user_is_logged_in
validate :allowed_to_modify_private_query
validate :allowed_to_modify_public_query
protected
def user_is_current_user_and_logged_in
unless user.logged? && user == model.user
def user_is_logged_in
unless user.logged?
errors.add :base, :error_unauthorized
end
end
def allowed_to_modify_private_query
return if model.public?
if model.user != user
errors.add :base, :can_only_be_modified_by_owner
end
end
def allowed_to_modify_public_query
return unless model.public?
unless user.allowed_globally?(:manage_public_project_queries)
errors.add :base, :need_permission_to_modify_public_query
end
end
def name_select_included
if model.selects.none? { |s| s.attribute == :name }
errors.add :selects, :name_not_included
@@ -0,0 +1,33 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Queries::Projects::ProjectQueries
class PublishContract < BaseContract
attribute :public
end
end
@@ -0,0 +1,31 @@
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Queries::Projects::ProjectQueries
class UpdateContract < BaseContract; end
end
+1 -1
View File
@@ -29,7 +29,7 @@
module Relations
class BaseContract < ::ModelContract
attribute :relation_type
attribute :delay
attribute :lag
attribute :description
attribute :from
attribute :to
@@ -35,7 +35,7 @@ module WorkPackageMembers
private
def member_is_deletable
errors.add(:base, :not_deletable) unless model.deletable?
errors.add(:base, :not_deletable) unless model.some_roles_deletable?
end
end
end
+48 -9
View File
@@ -53,17 +53,20 @@ module WorkPackages
attribute :project_id
attribute :done_ratio,
writable: ->(*) do
Setting.work_package_done_ratio == 'field'
end
writable: false
attribute :derived_done_ratio,
writable: false
attribute :estimated_hours
attribute :estimated_hours do
validate_work_is_set_when_remaining_work_is_set
end
attribute :derived_estimated_hours,
writable: false
attribute :remaining_hours
attribute :remaining_hours do
validate_remaining_work_is_lower_than_work
validate_remaining_work_is_set_when_work_is_set
end
attribute :derived_remaining_hours,
writable: false
@@ -74,7 +77,7 @@ module WorkPackages
next unless model.project
validate_people_visible :assigned_to,
'assigned_to_id',
"assigned_to_id",
assignable_assignees
end
@@ -82,7 +85,7 @@ module WorkPackages
next unless model.project
validate_people_visible :responsible,
'responsible_id',
"responsible_id",
assignable_responsibles
end
@@ -223,7 +226,7 @@ module WorkPackages
def validate_after_soonest_start(date_attribute)
if !model.schedule_manually? && before_soonest_start?(date_attribute)
message = I18n.t('activerecord.errors.models.work_package.attributes.start_date.violates_relationships',
message = I18n.t("activerecord.errors.models.work_package.attributes.start_date.violates_relationships",
soonest_start: model.soonest_start)
errors.add date_attribute, message, error_symbol: :violates_relationships
@@ -320,6 +323,42 @@ module WorkPackages
end
end
def validate_remaining_work_is_lower_than_work
if work_set? && remaining_work_set? && remaining_work_exceeds_work?
if model.changed.include?("estimated_hours")
errors.add(:estimated_hours, :cant_be_inferior_to_remaining_work)
end
if model.changed.include?("remaining_hours")
errors.add(:remaining_hours, :cant_exceed_work)
end
end
end
def validate_remaining_work_is_set_when_work_is_set
if work_set? && !remaining_work_set?
errors.add(:remaining_hours, :must_be_set_when_work_is_set)
end
end
def validate_work_is_set_when_remaining_work_is_set
if remaining_work_set? && !work_set?
errors.add(:estimated_hours, :must_be_set_when_remaining_work_is_set)
end
end
def work_set?
model.estimated_hours.present?
end
def remaining_work_set?
model.remaining_hours.present?
end
def remaining_work_exceeds_work?
model.remaining_hours > model.estimated_hours
end
def validate_no_reopen_on_closed_version
if model.version_id && model.reopened? && model.version.closed?
errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version)
@@ -333,7 +372,7 @@ module WorkPackages
unless principal_visible?(id, list)
errors.add attribute,
I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
I18n.t("api_v3.errors.validation.invalid_user_assigned_to_work_package",
property: I18n.t("attributes.#{attribute}"))
end
end
@@ -0,0 +1,40 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require "work_packages/create_contract"
module WorkPackages
class CopyContract < CreateContract
# As % Complete can be set while Work and Remaining work are not, copying is
# a scenario where this field must be writable
attribute :done_ratio,
writable: true
end
end
@@ -26,14 +26,14 @@
# See COPYRIGHT and LICENSE files for more details.
#++
require "work_packages/create_contract"
require "work_packages/copy_contract"
# Can be used to copy all of a project's work packages. As the
# work packages can be old, some of the validations that would
# apply to newly created work packages need not apply there, e.g
# on copying, it is ok for work packages to have closed versions
module WorkPackages
class CopyProjectContract < CreateContract
class CopyProjectContract < CopyContract
include WorkPackages::SkipAuthorizationChecks
# let the contract be used in error messages
@@ -35,7 +35,7 @@ module Admin
before_action :find_attachment, only: %i[destroy]
menu_item :attachment_quarantine
menu_item :attachments
def index; end
@@ -51,12 +51,10 @@ module Admin
redirect_to action: :index
end
def default_breadcrumb
t("antivirus_scan.quarantined_attachments.title")
end
def default_breadcrumb; end
def show_local_breadcrumb
true
false
end
private

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