Merge remote-tracking branch 'origin/dev' into feature/73089-restyled-work-package-card-in-backlogs-and-sprints-view

This commit is contained in:
Henriette Darge
2026-05-19 09:37:17 +02:00
1089 changed files with 32006 additions and 12013 deletions
+5 -3
View File
@@ -25,10 +25,12 @@ jobs:
RUBY_GC_HEAP_INIT_SLOTS: 100000
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- name: Setup Brakeman
run: |
@@ -44,6 +46,6 @@ jobs:
--output output.sarif.json
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v4
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
with:
sarif_file: output.sarif.json
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
uses: contributor-assistant/github-action@v2.6.1
uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
env:
# https://github.com/contributor-assistant/github-action?tab=readme-ov-file#environmental-variables
# Built-in GitHub token to make the API calls for interacting with GitHub. Does not need to be specified the secrets store.
+5 -3
View File
@@ -31,10 +31,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
with:
config-file: ./.github/codeql/config.yml
languages: ${{ matrix.language }}
@@ -42,6 +44,6 @@ jobs:
queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
with:
category: "/language:${{matrix.language}}"
@@ -59,7 +59,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ env.RELEASE_BRANCH }}
@@ -44,7 +44,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ env.BASE_BRANCH }}
+3 -3
View File
@@ -42,11 +42,11 @@ jobs:
- dev
- "${{ needs.setup.outputs.latest_release_branch }}"
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ matrix.branch }}
fetch-depth: 1
- uses: ruby/setup-ruby@v1
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
bundler-cache: true
- name: "Set crowdin branch name"
@@ -66,7 +66,7 @@ jobs:
bundle exec script/i18n/generate_seeders_i18n_source_file
fi
- name: "Crowdin: upload sources and download translations"
uses: crowdin/github-action@v2
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2
with:
# Upload current source files
upload_sources: true
+6 -2
View File
@@ -14,11 +14,15 @@ jobs:
if: github.repository_owner == 'opf'
runs-on: [ubuntu-latest]
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- uses: ruby/setup-ruby@v1
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
ruby-version: .ruby-version
bundler-cache: true
+4 -2
View File
@@ -34,9 +34,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4.9.0
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5
# Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options.
with:
comment-summary-in-pr: on-failure
+8 -1
View File
@@ -10,18 +10,23 @@ on:
description: "The tag to release. Note that this happens by default on the tag push. Only run this action when something went wrong!"
required: false
permissions: {}
jobs:
compute-inputs:
if: github.repository == 'opf/openproject'
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
tag: ${{ steps.compute.outputs.tag }}
branch: ${{ steps.compute.outputs.branch }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.ref }}
persist-credentials: false
- name: Compute tags and branch
id: compute
env:
@@ -47,6 +52,8 @@ jobs:
build:
needs: [compute-inputs]
permissions:
contents: read
uses: ./.github/workflows/docker.yml
with:
branch: ${{ needs.compute-inputs.outputs.branch }}
+8 -1
View File
@@ -6,9 +6,13 @@ on:
- cron: "20 2 * * *" # Daily at 02:20
workflow_dispatch: # Allow manual trigger
permissions: {}
jobs:
build-dev:
if: github.repository == 'opf/openproject'
permissions:
contents: read
uses: ./.github/workflows/docker.yml
with:
branch: dev
@@ -20,9 +24,12 @@ jobs:
OPS_MAIL_SMTP_TOKEN: ${{ secrets.OPS_MAIL_SMTP_TOKEN }}
build-release-candidate:
if: github.repository == 'opf/openproject'
permissions:
contents: read
# References to release/X.Y and X.Y-rc are being
# updated from the devkit (UpdateWorkflows step) whenever a new release branch is created
uses: opf/openproject/.github/workflows/docker.yml@release/17.4
# Ignore unpinned uses as we reference the latest release branch and change it
uses: opf/openproject/.github/workflows/docker.yml@release/17.4 # zizmor: ignore[unpinned-uses]
with:
branch: release/17.4
tag: 17.4-rc
+56 -36
View File
@@ -62,9 +62,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.branch }}
persist-credentials: false
- name: Set version outputs and convert tags
id: extract_version
env:
@@ -85,23 +86,27 @@ jobs:
fi
- name: Verify outputs
run: |
if [ -z "${{ steps.extract_version.outputs.version }}" ]; then
if [ -z "${STEPS_EXTRACT_VERSION_OUTPUTS_VERSION}" ]; then
echo "Error: version output is empty"
exit 1
fi
if [ -z "${{ steps.extract_version.outputs.docker_tags }}" ]; then
if [ -z "${STEPS_EXTRACT_VERSION_OUTPUTS_DOCKER_TAGS}" ]; then
echo "Error: docker_tags output is empty"
exit 1
fi
if [ -z "${{ steps.extract_version.outputs.registry_image }}" ]; then
if [ -z "${STEPS_EXTRACT_VERSION_OUTPUTS_REGISTRY_IMAGE}" ]; then
echo "Error: registry_image output is empty"
exit 1
fi
echo "✓ version: ${{ steps.extract_version.outputs.version }}"
echo "✓ docker_tags: ${{ steps.extract_version.outputs.docker_tags }}"
echo "✓ registry_image: ${{ steps.extract_version.outputs.registry_image }}"
echo "✓ version: ${STEPS_EXTRACT_VERSION_OUTPUTS_VERSION}"
echo "✓ docker_tags: ${STEPS_EXTRACT_VERSION_OUTPUTS_DOCKER_TAGS}"
echo "✓ registry_image: ${STEPS_EXTRACT_VERSION_OUTPUTS_REGISTRY_IMAGE}"
env:
STEPS_EXTRACT_VERSION_OUTPUTS_VERSION: ${{ steps.extract_version.outputs.version }}
STEPS_EXTRACT_VERSION_OUTPUTS_DOCKER_TAGS: ${{ steps.extract_version.outputs.docker_tags }}
STEPS_EXTRACT_VERSION_OUTPUTS_REGISTRY_IMAGE: ${{ steps.extract_version.outputs.registry_image }}
- name: Cache NPM
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: |
frontend/node_modules
@@ -109,17 +114,18 @@ jobs:
key: nodejs-x64-${{ hashFiles('**/package-lock.json') }}
restore-keys: nodejs-x64-
- name: Cache angular
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: frontend/.angular
key: angular-${{ github.ref }}
restore-keys: angular-
- uses: ruby/setup-ruby@v1
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
bundler-cache: true
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22.15
package-manager-cache: false
cache: npm
cache-dependency-path: |
package-lock.json
@@ -129,7 +135,7 @@ jobs:
./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@v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
path: public/
name: public-assets-${{ inputs.tag }}-${{ github.sha }}
@@ -146,7 +152,7 @@ jobs:
needs:
- setup
runs-on:
labels: "runs-on=${{ github.run_id }}/ssh=false/${{ matrix.runner }}/volume=100g"
labels: "runs-on=${{ github.run_id }}/ssh=false/${{ matrix.runner }}/volume=200g"
strategy:
matrix:
include:
@@ -177,14 +183,15 @@ jobs:
runner: runner=4cpu-linux-arm64
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.branch }}
persist-credentials: false
- name: Prepare docker files
run: |
cp ./docker/prod/Dockerfile ./Dockerfile
- name: Download precompiled public assets
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: public-assets-${{ inputs.tag }}-${{ github.sha }}
path: public/
@@ -198,23 +205,25 @@ jobs:
echo '!/public/assets' >> .dockerignore
echo '!/config/frontend_assets.manifest.json' >> .dockerignore
- name: Add build information
env:
INPUTS_BRANCH: ${{ inputs.branch }}
run: |
echo "${{ inputs.branch }}" > PRODUCT_VERSION
echo "https://github.com/opf/openproject/commits/${{ inputs.branch }}" > PRODUCT_URL
echo "$INPUTS_BRANCH" > PRODUCT_VERSION
echo "https://github.com/opf/openproject/commits/$INPUTS_BRANCH" > PRODUCT_URL
date -u +"%Y-%m-%dT%H:%M:%SZ" > RELEASE_DATE
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
context: git
labels: |
@@ -229,7 +238,7 @@ jobs:
${{ needs.setup.outputs.registry_image }}
- name: Restore vendor/bundle
id: restore-vendor-bundle
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
vendor/bundle
@@ -239,7 +248,7 @@ jobs:
sed -i 's/vendor\/bundle//g' .dockerignore
- name: Build image
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
platforms: ${{ matrix.platform }}
@@ -254,8 +263,10 @@ jobs:
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/trixie,manifests_prefix=cache/${{ github.repository }}/trixie,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/trixie,manifests_prefix=cache/${{ github.repository }}/trixie,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
- name: Extract vendor/bundle from container
env:
STEPS_BUILD_OUTPUTS_IMAGEID: ${{ steps.build.outputs.imageid }}
run: |
docker create --name bundle ${{ steps.build.outputs.imageid }}
docker create --name bundle $STEPS_BUILD_OUTPUTS_IMAGEID
if [ -d vendor/bundle ]; then
mv vendor/bundle vendor/bundle.bak
fi
@@ -263,7 +274,7 @@ jobs:
docker rm bundle
- name: Save vendor/bundle
id: save-vendor-bundle
uses: actions/cache/save@v5
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
vendor/bundle
@@ -276,14 +287,16 @@ jobs:
mv vendor/bundle.bak vendor/bundle
fi
- name: Validate image
env:
STEPS_BUILD_OUTPUTS_IMAGEID: ${{ steps.build.outputs.imageid }}
run: |
./script/ci/docker_validate_image.sh \
--image "${{ steps.build.outputs.imageid }}" \
--image "$STEPS_BUILD_OUTPUTS_IMAGEID" \
--target "${{ matrix.target }}" \
--platform "${{ matrix.platform }}"
- name: Push image
id: push
uses: docker/build-push-action@v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
platforms: ${{ matrix.platform }}
@@ -295,12 +308,14 @@ jobs:
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/trixie,manifests_prefix=cache/${{ github.repository }}/trixie,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/trixie,manifests_prefix=cache/${{ github.repository }}/trixie,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
- name: Export digest
env:
STEPS_PUSH_OUTPUTS_DIGEST: ${{ steps.push.outputs.digest }}
run: |
mkdir -p /tmp/digests
digest="${{ steps.push.outputs.digest }}"
digest="$STEPS_PUSH_OUTPUTS_DIGEST"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: digests-${{ inputs.tag }}-${{ matrix.target }}--${{ matrix.digest }}
path: /tmp/digests/*
@@ -316,13 +331,13 @@ jobs:
- build
steps:
- name: Merge digests
uses: actions/upload-artifact/merge@v7
uses: actions/upload-artifact/merge@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
pattern: "digests-${{ inputs.tag }}-${{ matrix.target }}--*"
overwrite: true
name: "merged-digests-${{ inputs.tag }}-${{ matrix.target }}-${{ github.run_number }}-${{ github.run_attempt }}"
- name: Download digests
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: "merged-digests-${{ inputs.tag }}-${{ matrix.target }}-${{ github.run_number }}-${{ github.run_attempt }}"
path: /tmp/digests
@@ -333,10 +348,10 @@ jobs:
if [ "$suffix" = "-all-in-one" ]; then suffix="" ; fi
echo "suffix=$suffix" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
images: ${{ needs.setup.outputs.registry_image }}
labels: |
@@ -351,7 +366,7 @@ jobs:
tags: |
${{ needs.setup.outputs.docker_tags }}
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -359,10 +374,15 @@ jobs:
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ needs.setup.outputs.registry_image }}@sha256:%s ' *)
$(printf '${NEEDS_SETUP_OUTPUTS_REGISTRY_IMAGE}@sha256:%s ' *)
env:
NEEDS_SETUP_OUTPUTS_REGISTRY_IMAGE: ${{ needs.setup.outputs.registry_image }}
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ needs.setup.outputs.registry_image }}:${{ steps.meta.outputs.version }}
docker buildx imagetools inspect ${NEEDS_SETUP_OUTPUTS_REGISTRY_IMAGE}:${STEPS_META_OUTPUTS_VERSION}
env:
NEEDS_SETUP_OUTPUTS_REGISTRY_IMAGE: ${{ needs.setup.outputs.registry_image }}
STEPS_META_OUTPUTS_VERSION: ${{ steps.meta.outputs.version }}
notify-failure:
needs: [setup, build, merge]
if: ${{ always() && contains(needs.*.result, 'failure') }}
+11 -5
View File
@@ -17,8 +17,10 @@ jobs:
name: Check internal links in documentation
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
bundler-cache: true
- run: bundle exec ./script/docs/check_links
@@ -26,14 +28,18 @@ jobs:
name: Check README.md case in documentation
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- run: script/docs/check_readme_case
docs-readme-yaml-header-syntax-check:
name: Check README.md YAML header syntax in documentation
runs-on: [ubuntu-latest]
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
bundler-cache: true
- run: script/docs/check_readme_yaml_header_syntax
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send mail
uses: dawidd6/action-send-mail@v16
uses: dawidd6/action-send-mail@d38f3f7cd391cdebfe0d38efc3998b935e951c4f # v16
with:
subject: ${{ inputs.subject }}
body: ${{ inputs.body }}
+8 -3
View File
@@ -13,16 +13,21 @@ jobs:
eslint:
name: eslint
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22.15'
package-manager-cache: false
cache: npm
cache-dependency-path: frontend/package-lock.json
- uses: reviewdog/action-eslint@v1
- uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1
with:
reporter: github-pr-check
workdir: 'frontend/'
+19 -7
View File
@@ -52,12 +52,16 @@ on:
jobs:
build:
if: github.repository == 'opf/openproject'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ inputs.branch }} # this is empty on push, so it just checks out the event SHA in that case
persist-credentials: false
- name: Include Git SHA in package.json version
id: short-sha
run: |
@@ -85,31 +89,37 @@ jobs:
BRANCH=$(echo "${INPUT_BRANCH:-$FALLBACK_BRANCH}" | tr / -)
SPECIFIC_TAG=$BRANCH-$SHORT_SHA
LATEST_TAG=$BRANCH-latest
MINOR_TAG=
MAJOR_TAG=
if [ -n "$INPUT_TAG" ]; then
SPECIFIC_TAG=$(echo "$INPUT_TAG" | tr -d v)
LATEST_TAG=latest
MINOR_TAG=$(echo $SPECIFIC_TAG | cut -d. -f1-2)
MAJOR_TAG=$(echo $SPECIFIC_TAG | cut -d. -f1)
fi
echo 'tags<<EOF' >> $GITHUB_OUTPUT
echo $REGISTRY:$SPECIFIC_TAG >> $GITHUB_OUTPUT
echo $REGISTRY:$LATEST_TAG >> $GITHUB_OUTPUT
[ -n "$MINOR_TAG" ] && echo $REGISTRY:$MINOR_TAG >> $GITHUB_OUTPUT
[ -n "$MAJOR_TAG" ] && echo $REGISTRY:$MAJOR_TAG >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build and push
id: build
uses: docker/build-push-action@v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: extensions/op-blocknote-hocuspocus
push: true
@@ -121,11 +131,13 @@ jobs:
- name: Summarize build
run: |
echo "Built and pushed the following tags:" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.tags.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
echo "${STEPS_TAGS_OUTPUTS_TAGS}" >> $GITHUB_STEP_SUMMARY
env:
STEPS_TAGS_OUTPUTS_TAGS: ${{ steps.tags.outputs.tags }}
- name: Generate GHA token
id: generate-gha-token
uses: actions/create-github-app-token@v3
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3
with:
app-id: ${{ vars.DEPLOY_APP_ID }}
private-key: ${{ secrets.DEPLOY_APP_PRIVATE_KEY }}
+6 -4
View File
@@ -18,19 +18,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22.19'
package-manager-cache: false
cache: npm
cache-dependency-path: extensions/op-blocknote-hocuspocus/package-lock.json
- name: Install Dependencies
id: npm-i
run: npm i
id: npm-ci
run: npm ci
- name: run tests
id: npm-test
+4 -2
View File
@@ -28,10 +28,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Setup Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- name: Setup i18n-tasks
run: |
+9 -4
View File
@@ -17,12 +17,17 @@ jobs:
name: APIv3 specification (OpenAPI 3.0)
if: github.repository_owner == 'opf'
runs-on: [ubuntu-latest]
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
bundler-cache: true
- uses: actions/setup-node@v6
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
bundler-cache: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '20'
package-manager-cache: false
- run: ./script/api/validate_spec
+8 -5
View File
@@ -15,6 +15,8 @@ jobs:
if: github.repository == 'opf/openproject'
name: ${{ matrix.target }}
runs-on: ubuntu-latest
permissions:
contents: read
services:
postgres:
image: postgres:16
@@ -35,22 +37,23 @@ jobs:
- el:9
- sles:15
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
persist-credentials: false
- name: Setup
id: setup
run: |
VERSION=$(ruby -r ./lib/open_project/version.rb -e "puts OpenProject::VERSION")
echo "version=$VERSION" >> $GITHUB_OUTPUT
if [[ "${{ github.ref_type }}" == "tag" ]]; then
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
MAJOR=$(ruby -r ./lib/open_project/version.rb -e "puts OpenProject::VERSION::MAJOR")
echo "channel=stable/${MAJOR}" >> $GITHUB_OUTPUT
else
echo "channel=${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "channel=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
fi
- name: Package
uses: pkgr/action/package@main
uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main
id: package
with:
target: ${{ matrix.target }}
@@ -60,7 +63,7 @@ jobs:
env: |
DATABASE_URL=postgres://app:p4ssw0rd@127.0.0.1:5432/app
- name: Publish
uses: pkgr/action/publish@main
uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main
with:
target: ${{ matrix.target }}
token: ${{ secrets.PACKAGER_PUBLISH_TOKEN }}
+4 -2
View File
@@ -19,7 +19,9 @@ jobs:
runs-on: ubuntu-slim
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Generate .env.pullpreview file
run: |
echo "OPENPROJECT_SEED_ADMIN_USER_PASSWORD_RESET=false" >> .env.pullpreview
@@ -41,7 +43,7 @@ jobs:
cp ./docker/pullpreview/docker-compose.yml ./docker-compose.pullpreview.yml
cp ./docker/pullpreview/Caddyfile ./Caddyfile
cp ./docker/prod/Dockerfile ./Dockerfile
- uses: pullpreview/action@v6
- uses: pullpreview/action@0dc5a741255c0afdd5a9e23e9264d6376652ec8e # v6
with:
# allows to ssh to the instance using our GitHub ssh key
admins: "crohr,HDinger,machisuji,oliverguenther,ulferts,wielinde,cbliard,@collaborators/push"
+9 -4
View File
@@ -9,13 +9,18 @@ jobs:
rubocop:
name: rubocop
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- name: Run Rubocop
uses: reviewdog/action-rubocop@v2
uses: reviewdog/action-rubocop@b6d5e953a5fc0bf3ab65254e77730ea2174d6d6d # v2
with:
github_token: ${{ secrets.github_token }}
rubocop_version: gemfile
@@ -32,7 +37,7 @@ jobs:
- name: Install erb_lint
run: gem install -N erb_lint erblint-github
- name: Run erb-lint
uses: tk0miya/action-erblint@v1
uses: tk0miya/action-erblint@44c5fe3552356fe8bff23f30d534aa4258aa3f7b # v1
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-check
+10 -5
View File
@@ -41,15 +41,18 @@ jobs:
fi
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.use_input_or_find_latest_release.outputs.ref }}
persist-credentials: false
- name: Print ref to summary
run: |
SHA=$(git rev-parse HEAD)
SHORT_SHA=$(git rev-parse --short HEAD)
echo "Testing seeding on **${{ steps.use_input_or_find_latest_release.outputs.ref }}** ([$SHORT_SHA](https://github.com/opf/openproject/commit/$SHA))" >> "$GITHUB_STEP_SUMMARY"
echo "Testing seeding on **${STEPS_USE_INPUT_OR_FIND_LATEST_RELEASE_OUTPUTS_REF}** ([$SHORT_SHA](https://github.com/opf/openproject/commit/$SHA))" >> "$GITHUB_STEP_SUMMARY"
env:
STEPS_USE_INPUT_OR_FIND_LATEST_RELEASE_OUTPUTS_REF: ${{ steps.use_input_or_find_latest_release.outputs.ref }}
- name: List available locales
id: list
@@ -86,15 +89,16 @@ jobs:
RAILS_ENV: development
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.prepare.outputs.ref }}
persist-credentials: false
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libpq-dev
- name: Setup Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
with:
bundler-cache: true
@@ -109,7 +113,8 @@ jobs:
env:
SILENCE_SQL_LOGS: 1
OPENPROJECT_EDITION: ${{ matrix.edition }}
run: ruby script/i18n/test_seed_all_locales ${{ matrix.locale }}
MATRIX_LOCALE: ${{ matrix.locale }}
run: ruby script/i18n/test_seed_all_locales ${MATRIX_LOCALE}
notify-failure:
needs: [prepare, seed]
+3 -2
View File
@@ -10,10 +10,11 @@ jobs:
# Only run in the private fork, never in the public repo
if: github.repository == 'opf/openproject-internal-fork'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ secrets.OPENPROJECTCI_GH_FORK_TOKEN }}
+9 -9
View File
@@ -26,19 +26,19 @@ jobs:
name: Units + Features
if: github.repository_owner == 'opf'
runs-on:
labels: "runs-on=${{ github.run_id }}/image=ubuntu24-full-x64/family=m7+c7+r7+i7+r8/ram=128+256/cpu=32"
labels: "runs-on=${{ github.run_id }}/image=ubuntu24-full-x64/family=m8azn+m8+m7+i7+r8/ram=128+256/cpu=24+32/volume=50gb"
timeout-minutes: 40
env:
DOCKER_BUILDKIT: 1
CI_RETRY_COUNT: 3
steps:
- uses: runs-on/action@v2
- uses: runs-on/action@742bf56072eb4845a0f94b3394673e4903c90ff0 # v2.1.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
metrics: cpu,memory,disk,io,network
- uses: actions/checkout@v6
persist-credentials: false
- name: Cache DOCKER
id: cache_docker
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: cache/docker
# Note: no restore keys since whenever the files below change, we want to rebuild the full image from scratch
@@ -47,28 +47,28 @@ jobs:
if: steps.cache_docker.outputs.cache-hit == 'true'
run: docker load -i cache/docker/image.tar
- name: Cache GEM
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: cache/bundle
key: gem-trixie-${{ hashFiles('.ruby-version') }}-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
gem-trixie-${{ hashFiles('.ruby-version') }}-
- name: Cache NPM
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: cache/node
key: node-${{ hashFiles('package.json', 'frontend/package-lock.json') }}
restore-keys: |
node-
- name: Cache ANGULAR
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: cache/angular
key: angular-${{ hashFiles('package.json', 'frontend/package-lock.json') }}
restore-keys: |
angular-
- name: Cache TEST RUNTIME
uses: runs-on/cache@v5
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
with:
path: cache/runtime-logs
key: runtime-logs-${{ github.head_ref || github.ref }}-${{ github.sha }}
+22 -14
View File
@@ -26,29 +26,37 @@ defaults:
working-directory: ./frontend
jobs:
units:
name: Units
unit:
name: Units (${{ matrix.browser }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22.15'
cache: npm
cache-dependency-path: frontend/package-lock.json
package-manager-cache: false
- name: Install Dependencies
id: npm-i
run: npm i
run: npm ci
- name: Register plugins
id: npm-run-ci-plugins-register-frontend
run: npm run ci:plugins:register_frontend
- name: Test (Angular)
id: npm-test
run: npm test
- name: Install Playwright dependencies
timeout-minutes: 5
run: npx playwright install-deps ${{ matrix.browser }}
- name: Install Playwright Browser
timeout-minutes: 5
run: npx playwright install --only-shell ${{ matrix.browser }}
- name: Test
run: npm test -- --browsers ${{ matrix.browser }} --reporters dot --reporters github-actions
+6 -4
View File
@@ -19,10 +19,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- name: Verify linked version matches core version
id: version-check
@@ -32,7 +34,7 @@ jobs:
- name: Add comment if versions differ
if: steps.version-check.outputs.version_mismatch == 'true'
uses: marocchino/sticky-pull-request-comment@v3
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3
with:
header: version-mismatch-comment
message: |
@@ -50,7 +52,7 @@ jobs:
- The work package version OR your pull request target branch is correct
- name: Version check passed
if: steps.version-check.outputs.version_mismatch != 'true'
uses: marocchino/sticky-pull-request-comment@v3
uses: marocchino/sticky-pull-request-comment@d4d6b0936434b21bc8345ad45a440c5f7d2c40ff # v3
with:
header: version-mismatch-comment
delete: true
+30
View File
@@ -0,0 +1,30 @@
name: GitHub Actions Security Analysis with zizmor 🌈
on:
pull_request:
paths:
- ".github/workflows/**"
- ".github/actions/**"
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
permissions: {}
jobs:
zizmor:
name: Run zizmor 🌈
runs-on: ubuntu-latest
if: github.repository_owner == 'opf'
permissions:
security-events: write # Required for upload-sarif
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+16
View File
@@ -0,0 +1,16 @@
rules:
artipacked:
ignore:
# These workflows intentionally persist credentials because they push commits/branches
- create-merge-from-previous-release-branch-pr.yml
- create-merge-release-into-dev-pr.yml
- crowdin.yml
- sync-internal-fork.yml
dangerous-triggers:
ignore:
# CLA workflow intentionally uses pull_request_target to handle outside contributors
- cla.yml
cache-poisoning:
ignore:
# Frontend unit tests cache npm packages; only triggered on push/PR from trusted branches
- test-frontend-unit.yml
+1
View File
@@ -27,6 +27,7 @@ targets:
- NODE_ENV=production
- NPM_CONFIG_PRODUCTION=false
- SECRET_KEY_BASE=1
- OPENPROJECT_DISABLE__SECRET_KEY_BASE__CHECK=true
dependencies:
- epel-release
- ImageMagick
+5
View File
@@ -142,6 +142,11 @@ Naming/ClassAndModuleCamelCase:
Naming/FileName:
Enabled: false
Naming/MethodParameterName:
AllowedNames:
- id
- cf # custom fields are commonly abbreviated like that
Naming/PredicatePrefix:
ForbiddenPrefixes:
- is_
+9 -9
View File
@@ -62,14 +62,14 @@ gem "warden-basic_auth", "~> 0.2.1"
gem "pagy"
gem "will_paginate", "~> 4.0.0"
gem "friendly_id", "~> 5.6.0"
gem "friendly_id", "~> 5.7.0"
gem "scimitar", "~> 2.13"
gem "acts_as_list", "~> 1.2.6"
gem "acts_as_tree", "~> 2.9.0"
gem "awesome_nested_set", "~> 3.9.0"
gem "closure_tree", "~> 9.6.1"
gem "closure_tree", "~> 9.7.0"
gem "rubytree", "~> 2.2.0"
gem "addressable", "~> 2.9.0"
@@ -87,7 +87,7 @@ gem "htmldiff"
gem "stringex", "~> 2.8.5"
# CommonMark markdown parser with GFM extension
gem "commonmarker", "~> 2.8.0"
gem "commonmarker", "~> 2.8.2"
# HTML pipeline for transformations on text formatter output
# such as sanitization or additional features
@@ -201,8 +201,8 @@ gem "nokogiri", "~> 1.19.2"
gem "carrierwave", "~> 2.2.6"
gem "carrierwave_direct", "~> 3.0.0"
gem "ssrf_filter", "~> 1.3"
gem "fog-aws"
gem "ssrf_filter", "~> 1.3"
gem "aws-sdk-core", "~> 3.244"
# File upload via fog + screenshots on travis
@@ -219,7 +219,7 @@ gem "mini_magick", "~> 5.3.0", require: false
gem "validate_url"
# Storages support code
gem "dry-container"
gem "dry-core"
gem "dry-monads"
gem "dry-validation"
@@ -236,18 +236,18 @@ gem "yabeda-puma-plugin"
gem "yabeda-rails"
# opentelemetry
gem "opentelemetry-exporter-otlp", "~> 0.33.0", require: false
gem "opentelemetry-exporter-otlp", "~> 0.34.0", require: false
gem "opentelemetry-instrumentation-all", "~> 0.93.0", require: false
gem "opentelemetry-sdk", "~> 1.10", require: false
gem "view_component", "~> 4.9.0"
gem "view_component", "~> 4.10.0"
# Lookbook
gem "lookbook", "2.3.14"
gem "inline_svg", "~> 1.10.0"
# Require factory_bot for usage with openproject plugins testing
gem "factory_bot", "~> 6.5.6", require: false
gem "factory_bot", "~> 6.6.0", require: false
# require factory_bot_rails for convenience in core development
gem "factory_bot_rails", "~> 6.5.0", require: false
@@ -365,7 +365,7 @@ group :development, :test do
gem "rubocop-factory_bot", require: false
gem "rubocop-openproject", require: false
gem "rubocop-performance", require: false
gem "rubocop-rails", "~> 2.34.2"
gem "rubocop-rails", "~> 2.35.1"
gem "rubocop-rspec", require: false
gem "rubocop-rspec_rails", require: false
+100 -103
View File
@@ -242,7 +242,7 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (2.0.1)
action_text-trix (2.1.18)
action_text-trix (2.1.19)
railties
actioncable (8.1.3)
actionpack (= 8.1.3)
@@ -361,8 +361,8 @@ GEM
awesome_nested_set (3.9.0)
activerecord (>= 4.0.0, < 8.2)
aws-eventstream (1.4.0)
aws-partitions (1.1242.0)
aws-sdk-core (3.246.0)
aws-partitions (1.1249.0)
aws-sdk-core (3.247.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -370,11 +370,11 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.124.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (1.125.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.220.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-s3 (1.222.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-sns (1.113.0)
@@ -441,7 +441,7 @@ GEM
childprocess (5.1.0)
logger (~> 1.5)
climate_control (1.2.0)
closure_tree (9.6.1)
closure_tree (9.7.0)
activerecord (>= 7.2.0)
with_advisory_lock (>= 7.5.0)
zeitwerk (~> 2.7)
@@ -450,13 +450,13 @@ GEM
descendants_tracker (~> 0.0.1)
color_conversion (0.1.2)
colored2 (4.0.3)
commonmarker (2.8.1-aarch64-linux)
commonmarker (2.8.1-aarch64-linux-musl)
commonmarker (2.8.1-arm-linux)
commonmarker (2.8.1-arm64-darwin)
commonmarker (2.8.1-x86_64-darwin)
commonmarker (2.8.1-x86_64-linux)
commonmarker (2.8.1-x86_64-linux-musl)
commonmarker (2.8.2-aarch64-linux)
commonmarker (2.8.2-aarch64-linux-musl)
commonmarker (2.8.2-arm-linux)
commonmarker (2.8.2-arm64-darwin)
commonmarker (2.8.2-x86_64-darwin)
commonmarker (2.8.2-x86_64-linux)
commonmarker (2.8.2-x86_64-linux-musl)
compare-xml (0.66)
nokogiri (~> 1.8)
concurrent-ruby (1.3.6)
@@ -472,14 +472,14 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (2.1.0)
css_parser (2.2.0)
addressable
csv (3.3.5)
cuprite (0.17)
capybara (~> 3.0)
ferrum (~> 0.17.0)
daemons (1.4.1)
dalli (5.0.2)
dalli (5.0.4)
logger
date (3.5.1)
date_validator (0.12.0)
@@ -504,11 +504,9 @@ GEM
dotenv (= 3.2.0)
railties (>= 6.1)
drb (2.2.3)
dry-configurable (1.3.0)
dry-core (~> 1.1)
dry-configurable (1.4.0)
dry-core (~> 1.0)
zeitwerk (~> 2.6)
dry-container (0.11.0)
concurrent-ruby (~> 1.0)
dry-core (1.2.0)
concurrent-ruby (~> 1.0)
logger
@@ -578,12 +576,12 @@ GEM
eventmachine_httpserver (0.2.1)
excon (1.4.2)
logger
factory_bot (6.5.6)
factory_bot (6.6.0)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faraday (2.14.1)
faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -624,7 +622,7 @@ GEM
nokogiri (>= 1.5.11, < 2.0.0)
formatador (1.2.3)
reline
friendly_id (5.6.0)
friendly_id (5.7.0)
activerecord (>= 4.0.0)
front_matter_parser (1.0.1)
fugit (1.12.1)
@@ -651,7 +649,7 @@ GEM
mini_mime (~> 1.1)
representable (~> 3.0)
retriable (~> 3.1)
google-apis-gmail_v1 (0.48.0)
google-apis-gmail_v1 (0.49.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-env (2.3.1)
base64 (~> 0.2)
@@ -714,7 +712,7 @@ GEM
htmlentities (4.3.4)
http-2 (1.1.3)
http_parser.rb (0.8.1)
httpx (1.7.6)
httpx (1.7.8)
http-2 (>= 1.1.3)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
@@ -733,7 +731,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
icalendar (2.12.2)
icalendar (2.12.3)
base64
ice_cube (~> 0.16)
logger
@@ -755,8 +753,8 @@ GEM
reline (>= 0.4.2)
iso8601 (0.13.0)
jmespath (1.6.2)
job-iteration (1.13.1)
activejob (>= 7.0)
job-iteration (1.14.0)
activejob (>= 7.1)
json (2.19.5)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -776,7 +774,7 @@ GEM
json_spec (1.1.5)
multi_json (~> 1.0)
rspec (>= 2.0, < 4.0)
jwt (3.1.2)
jwt (3.2.0)
base64
ladle (1.0.1)
open4 (~> 1.0)
@@ -884,7 +882,7 @@ GEM
racc (~> 1.4)
nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4)
oj (3.17.0)
oj (3.17.1)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
okcomputer (1.19.1)
@@ -918,14 +916,14 @@ GEM
view_component (>= 3.1, < 5.0)
openproject-token (8.8.2)
activemodel
openssl (4.0.1)
openssl (4.0.2)
openssl-signature_algorithm (1.3.0)
openssl (> 2.0)
opentelemetry-api (1.9.0)
opentelemetry-api (1.10.0)
logger
opentelemetry-common (0.24.0)
opentelemetry-common (0.25.0)
opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.33.0)
opentelemetry-exporter-otlp (0.34.0)
google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1)
@@ -1086,22 +1084,22 @@ GEM
opentelemetry-helpers-sql-processor
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-semantic_conventions (>= 1.8.0)
opentelemetry-registry (0.5.0)
opentelemetry-registry (0.6.0)
opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.11.0)
opentelemetry-sdk (1.12.0)
logger
opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.37.0)
opentelemetry-semantic_conventions (1.37.1)
opentelemetry-api (~> 1.0)
optimist (3.2.1)
os (1.1.4)
ostruct (0.6.3)
ox (2.14.25)
ox (2.14.26)
bigdecimal (>= 3.0)
pagy (43.5.3)
pagy (43.5.4)
json
uri
yaml
@@ -1199,7 +1197,7 @@ GEM
eventmachine_httpserver
http_parser.rb (~> 0.8.0)
multi_json
puma (8.0.0)
puma (8.0.1)
nio4r (~> 2.0)
puma-plugin-statsd (2.8.0)
puma (>= 5.0, < 9)
@@ -1276,7 +1274,7 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rb_sys (0.9.127)
rb_sys (0.9.128)
rake-compiler-dock (= 1.12.0)
rbtrace (0.5.3)
ffi (>= 1.0.6)
@@ -1291,7 +1289,7 @@ GEM
redcarpet (3.6.1)
redis (5.4.1)
redis-client (>= 0.22.0)
redis-client (0.28.0)
redis-client (0.29.0)
connection_pool
regexp_parser (2.12.0)
reline (0.6.3)
@@ -1337,7 +1335,7 @@ GEM
rspec-support (3.13.7)
rspec-wait (1.0.2)
rspec (>= 3.4)
rubocop (1.86.1)
rubocop (1.86.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -1363,7 +1361,7 @@ GEM
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.34.3)
rubocop-rails (2.35.1)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -1404,9 +1402,9 @@ GEM
scimitar (2.15.0)
rails (>= 7.0)
securerandom (0.4.1)
selenium-devtools (0.147.0)
selenium-devtools (0.148.0)
selenium-webdriver (~> 4.2)
selenium-webdriver (4.43.0)
selenium-webdriver (4.44.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -1427,7 +1425,7 @@ GEM
bigdecimal
logger
ruby-ole
spring (4.4.2)
spring (4.5.0)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
@@ -1494,8 +1492,8 @@ GEM
activemodel (>= 3.0.0)
public_suffix
vcr (6.4.0)
vernier (1.10.0)
view_component (4.9.0)
vernier (1.10.1)
view_component (4.10.0)
actionview (>= 7.1.0)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)
@@ -1599,9 +1597,9 @@ DEPENDENCIES
carrierwave (~> 2.2.6)
carrierwave_direct (~> 3.0.0)
climate_control
closure_tree (~> 9.6.1)
closure_tree (~> 9.7.0)
colored2
commonmarker (~> 2.8.0)
commonmarker (~> 2.8.2)
compare-xml (~> 0.66)
connection_pool (~> 3.0.2)
costs!
@@ -1616,7 +1614,7 @@ DEPENDENCIES
disposable (~> 0.6.2)
doorkeeper (~> 5.9.0)
dotenv-rails
dry-container
dry-core
dry-monads
dry-validation
email_validator (~> 2.2.3)
@@ -1624,12 +1622,12 @@ DEPENDENCIES
erb_lint
erblint-github
escape_utils (~> 1.3)
factory_bot (~> 6.5.6)
factory_bot (~> 6.6.0)
factory_bot_rails (~> 6.5.0)
ffi (~> 1.17)
flamegraph
fog-aws
friendly_id (~> 5.6.0)
friendly_id (~> 5.7.0)
fuubar (~> 2.5.0)
globalid (~> 1.3)
good_job (~> 4.18.2)
@@ -1700,7 +1698,7 @@ DEPENDENCIES
openproject-webhooks!
openproject-wikis!
openproject-xls_export!
opentelemetry-exporter-otlp (~> 0.33.0)
opentelemetry-exporter-otlp (~> 0.34.0)
opentelemetry-instrumentation-all (~> 0.93.0)
opentelemetry-sdk (~> 1.10)
overviews!
@@ -1747,7 +1745,7 @@ DEPENDENCIES
rubocop-factory_bot
rubocop-openproject
rubocop-performance
rubocop-rails (~> 2.34.2)
rubocop-rails (~> 2.35.1)
rubocop-rspec
rubocop-rspec_rails
ruby-duration (~> 3.2.0)
@@ -1785,7 +1783,7 @@ DEPENDENCIES
validate_url
vcr
vernier
view_component (~> 4.9.0)
view_component (~> 4.10.0)
warden (~> 1.2)
warden-basic_auth (~> 0.2.1)
webmock (~> 3.26)
@@ -1798,7 +1796,7 @@ DEPENDENCIES
CHECKSUMS
Ascii85 (2.0.1) sha256=15cb5d941808543cbb9e7e6aea3c8ec3877f154c3461e8b3673e97f7ecedbe5a
action_text-trix (2.1.18) sha256=3fdb83f8bff4145d098be283cdd47ac41caf5110bfa6df4695ed7127d7fb3642
action_text-trix (2.1.19) sha256=7012f59421009cf284aa651294896414d653a61a2417c9b8714c8476d2f74009
actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0
actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59
actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d
@@ -1831,10 +1829,10 @@ CHECKSUMS
auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0
awesome_nested_set (3.9.0) sha256=3ce99e816550f97f4de118e621630070aacf24928b920fe4a68846578a8daaed
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1242.0) sha256=58886ab5484ccf9287a8d55e603c3c0fb004241dfb4c0c7690b67d18e4c39352
aws-sdk-core (3.246.0) sha256=393864ec8948560e69fcccc2e4d256b40c7028eb98930608dd295279e3c4ddcc
aws-sdk-kms (1.124.0) sha256=40d00ab706d7e49fd620270bd0dcb546f266295abdd49b54fec2611e2a41f37c
aws-sdk-s3 (1.220.0) sha256=237fda5e6ac7ecdd9c848e27187bfdc370edad5c5a141aeec389fb450fa28c7c
aws-partitions (1.1249.0) sha256=f0bed070aba353e6ffe439953d6c354b3f51d16770386d2b198e71d34612f2e9
aws-sdk-core (3.247.0) sha256=789864594ce8cef05ee3d81fa8ed506099280bda6ea12a7612b8b7c5e5e62851
aws-sdk-kms (1.125.0) sha256=23f81bc0838ae6ec2e8de3eae88af521d0e29d3a59b6c9dbb4b21343ba476bc8
aws-sdk-s3 (1.222.0) sha256=bae4b06fccf0b81b8d77e7abfc56e5e2146590d43c4ab58db0cee3ff5bbfb7f1
aws-sdk-sns (1.113.0) sha256=15fe37d010e86f4c28b4c2f2133c463ce5c14189ec3673a1f43c30dfee511b0f
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
axe-core-api (4.11.3) sha256=f5f6e802743644a50e2d8ef24c22aefbb6df49dd169024ff0144b47f37e652ba
@@ -1860,18 +1858,18 @@ CHECKSUMS
cgi (0.5.1) sha256=e93fcafc69b8a934fe1e6146121fa35430efa8b4a4047c4893764067036f18e9
childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec
climate_control (1.2.0) sha256=36b21896193fa8c8536fa1cd843a07cf8ddbd03aaba43665e26c53ec1bd70aa5
closure_tree (9.6.1) sha256=f6af11243dea13d888788ffb0fd28014bd1077abe3a4233ea1e7044e52fc6377
closure_tree (9.7.0) sha256=54403254741e70b5da35eefffc6b8172505da71c004ff75635f4e1299e6ba44c
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
coercible (1.0.0) sha256=5081ad24352cc8435ce5472bc2faa30260c7ea7f2102cc6a9f167c4d9bffaadc
color_conversion (0.1.2) sha256=99bea5fa412e1527a11389975aa6ad445ff8528ebae202c11d08c45ea2b94c96
colored2 (4.0.3) sha256=63e1038183976287efc43034f5cca17fb180b4deef207da8ba78d051cbce2b37
commonmarker (2.8.1-aarch64-linux) sha256=f855599cc6855f4137d72dcacae9571451075afe6e6c8522eba353df9b81d0bf
commonmarker (2.8.1-aarch64-linux-musl) sha256=bbc2b3d361403431a5aac737dc86997d3b2843276ef395e7499cf17ad48e1b2c
commonmarker (2.8.1-arm-linux) sha256=ede48564a9c2e29e003361fd7b0b158b3b85bcbd5a15b963fa2b3c78eef3993f
commonmarker (2.8.1-arm64-darwin) sha256=cf75760122d6e1c3f8626b5a6911636d2e0aaa26fa8dd377fea440a49c854b3e
commonmarker (2.8.1-x86_64-darwin) sha256=75afa559c1f1cc201afbb81291a2f2fc77b2941347deaa8d564d008ba04ecb18
commonmarker (2.8.1-x86_64-linux) sha256=c8f2e903c6ee4e2c7e280aa8b49ef37c53202d388e3a2ee06dfdccc8c6fb634f
commonmarker (2.8.1-x86_64-linux-musl) sha256=9b6305ce47e0d444b77dabf1762bf76085c2f7963b63c25c02ec36edcf06c785
commonmarker (2.8.2-aarch64-linux) sha256=715a5df28e5c1ec7b520e6441466062e9717b82193d2a5bd11a9c94f2f1e15f0
commonmarker (2.8.2-aarch64-linux-musl) sha256=c7d587bd7978416978e84d0de7488906b8f29b3f67ce8424942f0ca9aa6899db
commonmarker (2.8.2-arm-linux) sha256=da9da5c08d5133ad429ece13f2e93d3a30e1b23621ffac9040e185d7ea29bde6
commonmarker (2.8.2-arm64-darwin) sha256=cd7364f1fb9735d60218e4af0a4afbbeedbc465eb81cccca56f29a4efe2b5013
commonmarker (2.8.2-x86_64-darwin) sha256=fb946e0bb01f0e5742aef75cc972b51ffe099e5b983ce9cd792b4a78c5d2c297
commonmarker (2.8.2-x86_64-linux) sha256=42cfb6532b15dd5821ce99e47397923255f32dcfc81024ef4c17e01e66ac7d75
commonmarker (2.8.2-x86_64-linux-musl) sha256=66568dce1c6f265e85d57ea7f749a148446527c7dd599c6ded4cdc819997c43e
compare-xml (0.66) sha256=e21aa5c0f69ef1177eced997c688fd4df989084e74a1b612257af32e1dd05319
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
@@ -1881,11 +1879,11 @@ CHECKSUMS
counter_culture (3.13.1) sha256=c297961933d9a9b96683fc298d68fde44039eca7c5876a2b05c3b180fe1c6328
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d
css_parser (2.1.0) sha256=bfb7c9cf3896426b53337e34b4ad391c3cfe8c2f2c839e72f2cdccf615fb5247
css_parser (2.2.0) sha256=23d1b247d7bc78cb2f2fe54629fb755e68d3004f1d1bd5c66d5096d42bff6325
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
cuprite (0.17) sha256=b140d5dc70d08b97ad54bcf45cd95d0bd430e291e9dffe76fff851fddd57c12b
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
dalli (5.0.2) sha256=818469227b9acdd9da3fc65ec5ae75d4020115545879e5e8f95634085e9a8749
dalli (5.0.4) sha256=3b258f100b8e3acd128a095fbd090c5a1324a6f5238fd1f86a2547473e6d49fc
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
date_validator (0.12.0) sha256=68c9834da240347b9c17441c553a183572508617ebfbe8c020020f3192ce3058
deckar01-task_list (2.3.4) sha256=66abdc7e009ea759732bb53867e1ea42de550e2aa03ac30a015cbf42a04c1667
@@ -1898,8 +1896,7 @@ CHECKSUMS
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
dotenv-rails (3.2.0) sha256=657e25554ba622ffc95d8c4f1670286510f47f2edda9f68293c3f661b303beab
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
dry-container (0.11.0) sha256=23be9381644d47343f3bf13b082b4255994ada0bfd88e0737eaaadc99d035229
dry-configurable (1.4.0) sha256=e35d1b5f3c081753ef361f564919db79000f32cfa6f20ee3a3ba5921b41b73ce
dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3
@@ -1923,9 +1920,9 @@ CHECKSUMS
eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972
eventmachine_httpserver (0.2.1) sha256=5db5e8a23754204d43592e5fcc2160457c57c870babe6307c4e61fc95019b809
excon (1.4.2) sha256=32d8d8eda619717d9b8043b4675e096fb5c2139b080e2ad3b267f88c545aaa35
factory_bot (6.5.6) sha256=12beb373214dccc086a7a63763d6718c49769d5606f0501e0a4442676917e077
factory_bot (6.6.0) sha256=1fc1b3b5620ec980a6a27aec1b6ec8c250ca82962e970e8a40f93e8d388d4b89
factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68
faraday (2.14.1) sha256=a43cceedc1e39d188f4d2cdd360a8aaa6a11da0c407052e426ba8d3fb42ef61c
faraday (2.14.2) sha256=73ccb9994a9e8648f010e32eca2ae82e41c57860aa10932cda29418b9e0223ad
faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
ferrum (0.17.2) sha256=2c2540a850b211a46f4d81de21bfd62048f507e4c327d1807225c3823c17e6ee
@@ -1943,7 +1940,7 @@ CHECKSUMS
fog-json (1.3.0) sha256=8c2e4feb221c14f92ceeffb0aa5c8b6e8dd7c614a9141dfe7905f2dffebea217
fog-xml (0.1.5) sha256=52b9fea10701461dd3eaf9d9839702169b418dbbf50426786b9b74fade373bd6
formatador (1.2.3) sha256=19fa898133c2c26cdbb5d09f6998c1e137ad9427a046663e55adfe18b950d894
friendly_id (5.6.0) sha256=28e221cd53fbd21586321164c1c6fd0c9ba8dde13969cb2363679f44726bb0c3
friendly_id (5.7.0) sha256=b8994416ebaceebc7c60bc556e84ef32a3a663e2e582eb04c429216cc98de4c4
front_matter_parser (1.0.1) sha256=bae298bda01db95788a4d6452f1670a3d198c6716c8d3727db9a95533deb7b7b
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
fuubar (2.5.1) sha256=b272a7804b282661c7fab583a3764f92543cb482c365ae39c685cd218fdd4880
@@ -1951,7 +1948,7 @@ CHECKSUMS
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
good_job (4.18.2) sha256=7e557a15865fc7b7ad4ab71644cf1d4189a2a1869d3b381e5e88741c540beca6
google-apis-core (1.0.2) sha256=ba4579aaadc902d6cc7bc8db88f566ab00f5e31ea87ab41e9f9a032c470f2629
google-apis-gmail_v1 (0.48.0) sha256=561534bb3d93610032720d0459153c432dc8e47e7096a1250fbe0ee8dcc6540c
google-apis-gmail_v1 (0.49.0) sha256=f5b5e597c8018d9e4b8df37fb06a8df429bd2de5f0fc94fb21a4d32b139f6ff1
google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7
google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc
@@ -1978,11 +1975,11 @@ CHECKSUMS
htmlentities (4.3.4) sha256=125a73c6c9f2d1b62100b7c3c401e3624441b663762afa7fe428476435a673da
http-2 (1.1.3) sha256=1b2f379d35a11dbae94f8a1a52c053d8c161eb4a0c98b5d1605ff1b2bf171c9c
http_parser.rb (0.8.1) sha256=9ae8df145b39aa5398b2f90090d651c67bd8e2ebfe4507c966579f641e11097a
httpx (1.7.6) sha256=82d825abc9876a132adc3492c56a0c528478ac238dd6f74d3422ab0036c6b5c8
httpx (1.7.8) sha256=6d769465ed608287a272ba0e4700fc22cee6f0335d80bd5c2effaf7fb7bd2a3a
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
i18n-js (4.2.4) sha256=61390d372f8fa68c495c5907d577657e8cc3a7031f4945db1e91f935e1391355
i18n-tasks (1.1.2) sha256=4dcfba49e52a623f30661cb316cb80d84fbba5cb8c6d88ef5e02545fffa3637a
icalendar (2.12.2) sha256=b63dfb784b4d1214bceaec40097e61eaf78c8691929d7f217779956d9019d9f5
icalendar (2.12.3) sha256=37dea76c36f5b21dd745bcd3cb53e7bacfbb4ad0a8d9c2a1c0aa9d39af83c850
ice_cube (0.17.0) sha256=32deb45dda4b4acc53505c2f581f6d32b5afc04d29b9004769944a0df5a5fcbe
ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
@@ -1992,13 +1989,13 @@ CHECKSUMS
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
iso8601 (0.13.0) sha256=298c2b15b7be5fa95a1372813d36a2257656cd8e906dfbc1f5cb409851425aa2
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
job-iteration (1.13.1) sha256=af4d5ac624c35ed2f32ed78de92d4673f0a93212105b96d46877b8422e3ff5a3
job-iteration (1.14.0) sha256=f154f978109acc838c0359ecde2fdd4dccc3382f95a22e03a58ac561a3615224
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
json-jwt (1.17.0) sha256=6ff99026b4c54281a9431179f76ceb81faa14772d710ef6169785199caadc4cc
json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666
json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
json_spec (1.1.5) sha256=7a77b97a92c787e2aa3fbc4a1239afc3342c781151dc98cfb81461b3b7cad10f
jwt (3.1.2) sha256=af6991f19a6bb4060d618d9add7a66f0eeb005ac0bc017cd01f63b42e122d535
jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7
ladle (1.0.1) sha256=e8586964108c798d48bf57d2a65bd5602e8e5223a176b6602a0fb36c0bda90dc
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e
@@ -2045,7 +2042,7 @@ CHECKSUMS
nokogiri (1.19.3-x86_64-darwin) sha256=77f3fba57d46c53ab31e62fc6c28f705109d1bf6264356c76f132b2be5728d4d
nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f
oj (3.17.0) sha256=5684b2127fb70e650fae90df521b91336ff8e55e2e1011ed80eb0283beac5360
oj (3.17.1) sha256=b00687f10bf68a32bfb633b87624174faf0989a5c96aff2f3f96f992717ce782
okcomputer (1.19.1) sha256=7df770e768434816d228407f0786563827cbf34cb379933578829720cb4f1e77
omniauth (1.9.2)
omniauth-openid-connect (0.5.0)
@@ -2082,11 +2079,11 @@ CHECKSUMS
openproject-webhooks (1.0.0)
openproject-wikis (1.0.0)
openproject-xls_export (1.0.0)
openssl (4.0.1) sha256=e27974136b7b02894a1bce46c5397ee889afafe704a839446b54dc81cb9c5f7d
openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2
openssl-signature_algorithm (1.3.0) sha256=a3b40b5e8276162d4a6e50c7c97cdaf1446f9b2c3946a6fa2c14628e0c957e80
opentelemetry-api (1.9.0) sha256=d24065dd26583babd8d498d38ea35f74dfa193fb7102512e6e161649440079fb
opentelemetry-common (0.24.0) sha256=f1647b233b8ac667feeb74d66a65b702008d9ab55aae825c220b4fe2c14fa773
opentelemetry-exporter-otlp (0.33.0) sha256=6e9ce38e393c7eb9aea3fb57b128174a0066767bf495f4fd9e63d7607e0b2ad3
opentelemetry-api (1.10.0) sha256=99ee7c829b18381c31a817ee9bf6a160d737542d99cb8da55d443336d266bfa9
opentelemetry-common (0.25.0) sha256=73915362e58d337fc92acbe1abfdaee1f725442527125fdb2af1420417f1149d
opentelemetry-exporter-otlp (0.34.0) sha256=3b3cdf4329ba30f4389d849c7f13b8f9f983ecb4a030031c03997dffae1e2a60
opentelemetry-helpers-mysql (0.6.0) sha256=7eeb5e6950c434775a8cf28b5fde4defc12e8b865c86479ce3119fcf593d9337
opentelemetry-helpers-sql (0.4.0) sha256=b10e8c3a2cca28a98af951bbb3e4efdc59e68b25ba0825e055574af543420afb
opentelemetry-helpers-sql-processor (0.5.0) sha256=b199241bc9451fcbd9f00b2f454830af19d4ca27c2219ea379c9b0d53cd0e0f1
@@ -2136,15 +2133,15 @@ CHECKSUMS
opentelemetry-instrumentation-sidekiq (0.29.0) sha256=b1d2a0cb9041a5e14239fe7c94d99e3dd07f870e2759460ab63592d7cdd8aadc
opentelemetry-instrumentation-sinatra (0.30.0) sha256=b67301153420f43264a0c68cdb3ca5bd77467cf5054e57b83a2bf891aaaa0361
opentelemetry-instrumentation-trilogy (0.68.0) sha256=24b31efdf21a08644ad26038b574f4b0876195b1502f3f64b1065eff3fe0f588
opentelemetry-registry (0.5.0) sha256=726ca58ada93a23efaa5f7bb81b8ab7a8a1e14602935c9c65dfa2e597a19fb4f
opentelemetry-sdk (1.11.0) sha256=427c6708f4732105ffa46c11afecb91807085c59e92538eaa6cf46b97b1850c6
opentelemetry-semantic_conventions (1.37.0) sha256=1e2dc5ad649e19ba2fb0fa7c6f9303e5cdd8d3952511415cb07efe28a0f8f4c3
opentelemetry-registry (0.6.0) sha256=5d3ed32ab9eee0fbdb30d4f0d0bb61ad11a4040b267b475ae815b80a8498a728
opentelemetry-sdk (1.12.0) sha256=a224abe0c59023d41cb7ac1c634d9d28843907efcd045ed1ae320796c48b864b
opentelemetry-semantic_conventions (1.37.1) sha256=74e87b31ab88c38410c5447d3bc5e692dcc0a73ee2114910c0142fc4546531c1
optimist (3.2.1) sha256=8cf8a0fd69f3aa24ab48885d3a666717c27bc3d9edd6e976e18b9d771e72e34e
os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
overviews (1.0.0)
ox (2.14.25) sha256=c938bcfce4d8ff2bd2bdbffe1277222a76c0a6e62078d6854bd4d40f34f2f7db
pagy (43.5.3) sha256=f9d73e690648d484706661dcb815647775cf8330fcc5c6e62ec87b9df431290b
ox (2.14.26) sha256=d4a9cb49fb55751631d469623cfb2e7dad7cea89119c93749c1e31a3fb2f4896
pagy (43.5.4) sha256=2bdf3fa6b1e0cac5bbafe5d077fb24eb971f72f3194f8c6863a0f3867261ce59
paper_trail (17.0.0) sha256=1c2842061d3874ca7015908e821e2aa14f9b982af2acb2a7974713bf79021c85
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
parallel_tests (5.7.0) sha256=3f1762c46ca2c223b8af8ef877217f9d76974e191bfa934f2580b58bcf1d005c
@@ -2179,7 +2176,7 @@ CHECKSUMS
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
puffing-billy (4.0.4) sha256=87015b0c41e0722b2171a0c5aa8130fd3f58aa1c016a1dc6dc569b2028aa846f
puma (8.0.0) sha256=1681050b8b60fab1d3033255ab58b6aec64cd063e43fc6f8204bcb8bf9364b88
puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c
puma-plugin-statsd (2.8.0) sha256=e515445f93232b6b3571a23b832f93a776d4ce0fc8a5edee798013b82f3488f3
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
@@ -2205,14 +2202,14 @@ CHECKSUMS
rake-compiler-dock (1.12.0) sha256=f13205c2738f3d2053afcd03491a9e4541b22a59a0bfc53fc8bc883bd8188023
rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
rb_sys (0.9.127) sha256=e9f90df3bb0577472d26d96127d5b5774b98f44de881e7d36aeefd28d6337847
rb_sys (0.9.128) sha256=9ab81f4d6d4e1895de18762232362d1264475aa7035756b50441e442130538fd
rbtrace (0.5.3) sha256=c432292f305d9ab12fd47d9722e0d5210d983758a951fe6107c36cc955cb923f
rbtree3 (0.7.1) sha256=ab60ead728a5491b70df4f4065e180b18dbab5319f817ce1dbf5dd906f26d8ba
rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
recaptcha (5.21.2) sha256=8f7bb6cc9f04c523fff3f38126b7c5f1314c35fcb3691fc8e70d2b464533c12a
redcarpet (3.6.1) sha256=d444910e6aa55480c6bcdc0cdb057626e8a32c054c29e793fa642ba2f155f445
redis (5.4.1) sha256=b5e675b57ad22b15c9bcc765d5ac26f60b675408af916d31527af9bd5a81faae
redis-client (0.28.0) sha256=888892f9cd8787a41c0ece00bdf5f556dfff7770326ce40bb2bc11f1bfec824b
redis-client (0.29.0) sha256=0c65bf1f8f6dca22063ddb085c0bb2054feef6f03a84869f4161b18a9a15bea3
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace
@@ -2232,13 +2229,13 @@ CHECKSUMS
rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
rspec-wait (1.0.2) sha256=865f921239325d3d26fc10ded4bdd485d8b58bcaaad1a28dd85ed15266b5a912
rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
rubocop-capybara (2.23.0) sha256=f9ea1ba3a7561ee8e88cf76fc378ce517ce5327155f305ee7b5c2500e5aee357
rubocop-factory_bot (2.28.0) sha256=4b17fc02124444173317e131759d195b0d762844a71a29fe8139c1105d92f0cb
rubocop-openproject (0.4.0) sha256=ce56d9e591f9be5a4d98125b10a73564b0557a5e408f97918f9630fb15ae66ae
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
rubocop-rails (2.34.3) sha256=10d37989024865ecda8199f311f3faca990143fbac967de943f88aca11eb9ad2
rubocop-rails (2.35.1) sha256=4ad270f5fb968ce86ac19f53c4b97354e4f91bc5334177478238674f51568377
rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100
ruby-duration (3.2.3) sha256=eb3d13b1df85067a015a8fb2ed8f1eec842a3b721e47c9b6fd74d2f356069784
@@ -2255,8 +2252,8 @@ CHECKSUMS
sanitize (7.0.0) sha256=269d1b9d7326e69307723af5643ec032ff86ad616e72a3b36d301ac75a273984
scimitar (2.15.0) sha256=700901afab9303b705a8f37644cd733ee3c3819b168d24a14a48dec060b16d63
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-devtools (0.147.0) sha256=8e9262c7c3bfafc7e16b773047912fb952e7bb59c2e6b592ea74a781700d6873
selenium-webdriver (4.43.0) sha256=a634377b964b701c6ac0a009ce3a08fa34ec1e1e7fe9a6d57e3088d14529a65c
selenium-devtools (0.148.0) sha256=3758305cf150b2abbb074085712808395e4369171f775f5c87fc0b9b1d03cf3f
selenium-webdriver (4.44.0) sha256=6f1df072529af369589c46f0e01132952aabb250cfd683c274d74dc1eb5d8477
semantic (1.6.1) sha256=3cdbb48f59198ebb782a3fdfb87b559e0822a311610db153bae22777a7d0c163
shoulda-context (2.0.0) sha256=7adf45342cd800f507d2a053658cb1cce2884b616b26004d39684b912ea32c34
shoulda-matchers (7.0.1) sha256=b4bfd8744c10e0a36c8ac1a687f921ee7e25ed529e50488d61b79a8688749c77
@@ -2264,7 +2261,7 @@ CHECKSUMS
simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29
smart_properties (1.17.0) sha256=f9323f8122e932341756ddec8e0ac9ec6e238408a7661508be99439ca6d6384b
spreadsheet (1.3.5) sha256=cd83ea66803d9cae4ac258dfe16cd8c2b85da33eec18a6d7b48fd4a45840ab7d
spring (4.4.2) sha256=22f61bacd8dc8595cedcdc738de46d7fc18be4d7a770986760344c924f485ce7
spring (4.5.0) sha256=9124b954f6d079cb9aa610d7069cbada7c146c9368599557806a9ff7067b2e1b
spring-commands-rspec (1.0.4) sha256=6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c
spring-commands-rubocop (0.4.0) sha256=3e677a2c8a27ae8a986f04bfb69e66d5d55b017541e8be93bf0dc48a7f5690c1
sprockets (3.7.5) sha256=72c20f256548f8a37fe7db41d96be86c3262fddaf4ebe9d69ec8317394fed383
@@ -2304,8 +2301,8 @@ CHECKSUMS
validate_email (0.1.6) sha256=9dfe9016d527b17a8d3a6e95e4dc50a125400eef899d13d4cc2a254393f82ee4
validate_url (1.0.15) sha256=72fe164c0713d63a9970bd6700bea948babbfbdcec392f2342b6704042f57451
vcr (6.4.0) sha256=077ac92cc16efc5904eb90492a18153b5e6ca5398046d8a249a7c96a9ea24ae6
vernier (1.10.0) sha256=5b1dc57012e08ed23e14f4d2943540140d454aa8434c7c35e7eb97befd4969bf
view_component (4.9.0) sha256=f599f0831ed0148bb19625f914c43cc982746a2252b6ebe8492bd4eea0d7dbaf
vernier (1.10.1) sha256=f2ab2e163b75ac3ef4e3173262d27d27da6219cf04b1b4a7171e4798114275ce
view_component (4.10.0) sha256=9e86960ee7ea4da3d1d07f65b1a66e4a9fb656be5434f1a649eb6fce6ac9baf9
virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
warden-basic_auth (0.2.1) sha256=bfc752e0109c0182c3e69e930284c5e1e81e7b4a354aeb2b5914ead1391f3c6e
+1 -2
View File
@@ -1,5 +1,6 @@
@import "enterprise_edition/banner_component"
@import "filter/filters_component"
@import "open_project/common/border_box_list_component"
@import "op_primer/border_box_table_component"
@import "op_primer/full_page_prompt_component"
@import "op_primer/form_helpers"
@@ -11,8 +12,6 @@
@import "open_project/common/inplace_edit_fields/index"
@import "open_project/common/submenu_component"
@import "open_project/common/main_menu_toggle_component"
@import "open_project/common/work_package_card_list_component"
@import "open_project/common/work_package_card_list_component/header"
@import "open_project/common/work_package_card_component"
@import "portfolios/details_component"
@import "projects/row_component"
@@ -29,25 +29,25 @@ See COPYRIGHT and LICENSE files for more details.
<%= render(Primer::Beta::BorderBox.new(**@system_arguments)) do |border_box| %>
<% if header? %>
<% border_box.with_header(id: header_id) do %>
<% border_box.with_header(**header.row_args) do %>
<%= header %>
<% end %>
<% end %>
<% if items.empty? %>
<% border_box.with_row(data: { empty_list_item: true }) do %>
<%= empty_state %>
<% end %>
<% else %>
<% if items.any? %>
<% items.each do |item| %>
<% border_box.with_row(**item.row_args) do %>
<%= render(item.card) %>
<%= item %>
<% end %>
<% end %>
<% elsif empty_state? %>
<% border_box.with_row(data: { empty_list_item: true }) do %>
<%= empty_state %>
<% end %>
<% end %>
<% if footer? %>
<% border_box.with_row(scheme: :neutral) do %>
<% border_box.with_footer(**footer.footer_args) do %>
<%= footer %>
<% end %>
<% end %>
@@ -0,0 +1,225 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
# A Primer BorderBox-backed list composition with optional header, items,
# empty state, and footer.
#
# Use this component for compact lists that need consistent OpenProject
# header actions, collapsible behavior, and row rendering.
class BorderBoxListComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
attr_reader :container, :collapsible, :current_user, :header_id, :footer_id
alias_method :collapsible?, :collapsible
# Optional header row.
#
# @!parse
# # Adds the optional header row.
# #
# # @param system_arguments [Hash] forwarded to {Header}. List wiring
# # arguments are supplied internally.
# # @return [ViewComponent::Slot]
# def with_header(**system_arguments, &block)
# end
renders_one :header, ->(**system_arguments) {
system_arguments = system_arguments.except(:id, :list_id)
system_arguments[:id] = header_id
system_arguments[:list_id] = list_id
system_arguments[:interactive] = interactive?
system_arguments[:collapsible] = collapsible?
Header.new(**system_arguments)
}
# List row content.
#
# Use:
#
# - `item` for generic row content.
# - `work_package_item` for rows backed by a work package card.
#
# @!parse
# # Adds a generic list row.
# #
# # @param system_arguments [Hash] forwarded to Primer's BorderBox row.
# # @return [ViewComponent::Slot]
# def with_item(**system_arguments, &block)
# end
#
# # Adds a work-package list row.
# #
# # @param work_package [WorkPackage] work package rendered by the row.
# # @param project [Project] project context for the work package.
# # @param params [Hash] request params used by specialized item classes.
# # @param component_klass [Class] item component class to instantiate.
# # Custom classes must accept `work_package:`, `project:`, `params:`,
# # `container:`, `current_user:`, and any forwarded item arguments.
# # @param item_arguments [Hash] forwarded to the item component.
# # @return [ViewComponent::Slot]
# def with_work_package_item(
# work_package:,
# project: work_package.project,
# params: {},
# component_klass: WorkPackageItem,
# **item_arguments,
# &block
# )
# end
renders_many :items, types: {
item: {
renders: ->(**system_arguments) {
Item.new(**system_arguments)
},
as: :item
},
work_package_item: {
renders: ->(
work_package:,
project: work_package.project,
params: {},
component_klass: WorkPackageItem,
**item_arguments
) {
component_klass.new(
work_package:,
project:,
params:,
container:,
current_user:,
**item_arguments
)
},
as: :work_package_item
}
}
# Optional empty-state content rendered when no items are present.
#
# @!parse
# # Adds empty-state content.
# #
# # Interactive lists announce this empty state only when the slot is
# # configured explicitly.
# #
# # @param title [String] empty-state title.
# # @param description [String, nil] optional supporting text.
# # @param icon [Symbol, nil] optional Primer icon.
# # @param system_arguments [Hash] forwarded to `Primer::Beta::Blankslate`.
# # @return [ViewComponent::Slot]
# def with_empty_state(title:, description: nil, icon: nil, **system_arguments)
# end
renders_one :empty_state, ->(title:, description: nil, icon: nil, **system_arguments) {
EmptyState.new(title:, description:, icon:, interactive: interactive?, **system_arguments)
}
# Optional footer row.
#
# @!parse
# # Adds an optional footer row.
# #
# # @param system_arguments [Hash] forwarded to Primer's BorderBox
# # footer. The `id` is generated internally for collapsible header
# # wiring.
# # @return [ViewComponent::Slot]
# def with_footer(**system_arguments, &block)
# end
renders_one :footer, ->(**system_arguments) {
system_arguments = system_arguments.except(:id)
system_arguments[:id] = footer_id
Footer.new(**system_arguments)
}
# @param container [String, Symbol, Class, Object] value passed to
# `dom_target` to derive DOM ids for the list and related controls.
# @param interactive [Boolean] whether dynamic list updates should be
# announced politely to assistive technology. This affects the counter
# and an explicitly configured empty state; it does not create default
# empty-state content for manually composed lists.
# @param collapsible [Boolean] whether the header renders a collapsible
# toggle. Defaults to `false`.
# @param current_user [User] user context passed to work-package items.
# @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox`.
# Pass `id:` to set the box id; related ids are derived from it.
def initialize(
container:,
interactive: false,
collapsible: false,
current_user: User.current,
**system_arguments
)
super()
@container = container
@interactive = interactive
@collapsible = collapsible
@current_user = current_user
@system_arguments = system_arguments.except(:list_id)
@system_arguments[:id] ||= dom_target(container)
@system_arguments[:list_id] = dom_target(@system_arguments[:id], :list)
@header_id = dom_target(@system_arguments[:id], :header)
@footer_id = dom_target(@system_arguments[:id], :footer)
end
def before_render
content
configure_header!
end
def render?
header? || items.any? || empty_state? || footer?
end
private
def interactive?
@interactive == true
end
def configure_header!
return unless header?
header.resolve_count!(items.size)
return unless collapsible? && footer?
header.collapsible_id = [list_id, footer_id].compact.join(" ")
end
def list_id
@system_arguments[:list_id]
end
end
end
end
@@ -0,0 +1,22 @@
//-- copyright
// OpenProject is an open source project management software.
// Copyright (C) the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// See COPYRIGHT and LICENSE files for more details.
//++
.op-border-box-list-header
display: grid
grid-template-columns: 1fr minmax(5rem, max-content) auto
grid-template-areas: "collapsible actions menu"
align-items: center
&--actions,
&--menu
margin-left: var(--stack-gap-normal)
align-self: flex-start
// Unfortunately, the invisible button style bites us here again.
margin-top: -6px
@@ -0,0 +1,76 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class BorderBoxListComponent
# Empty-state content rendered as a Primer Blankslate.
#
# This component is part of {BorderBoxListComponent} and should not be
# used as a standalone component.
#
class EmptyState < ApplicationComponent
include Primer::AttributesHelper
# @param title [String] empty-state heading.
# @param description [String, nil] optional supporting text.
# @param icon [Symbol, nil] optional Primer icon.
# @param interactive [Boolean] whether empty-state updates should be
# announced politely to assistive technology.
# @param system_arguments [Hash] forwarded to `Primer::Beta::Blankslate`.
def initialize(title:, description: nil, icon: nil, interactive: false, **system_arguments)
super()
@title = title
@description = description
@icon = icon
@system_arguments = system_arguments
return unless interactive
@system_arguments[:role] ||= "status"
@system_arguments[:aria] = merge_aria(
{ aria: { live: "polite" } },
@system_arguments
)
end
def call
blankslate = Primer::Beta::Blankslate.new(**@system_arguments)
blankslate.with_heading(tag: :h4).with_content(@title)
blankslate.with_description_content(@description) if @description
blankslate.with_visual_icon(icon: @icon) if @icon
render(blankslate)
end
end
end
end
end
@@ -0,0 +1,60 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class BorderBoxListComponent
# Footer row rendered below list items.
#
# This component is part of {BorderBoxListComponent} and should not be
# used as a standalone component.
class Footer < ApplicationComponent
attr_reader :id
# @param system_arguments [Hash] forwarded to Primer's BorderBox footer.
def initialize(**system_arguments)
super()
@id = system_arguments[:id]
@system_arguments = system_arguments
end
# @return [Hash] arguments forwarded to Primer's BorderBox footer.
def footer_args
@system_arguments.deep_dup
end
def call
content
end
end
end
end
end
@@ -23,55 +23,60 @@
#
# 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.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class WorkPackageCardListComponent
class Header < ApplicationComponent
include OpPrimer::ComponentHelpers
class BorderBoxListComponent
# Adds the standard list action menu slot used by list headers and items.
module HasMenu
extend ActiveSupport::Concern
include Primer::ClassNameHelper
renders_one :description
renders_many :actions, types: {
button: ->(**system_arguments) do
Primer::Beta::Button.new(**system_arguments)
included do
# @!parse
# # Adds a trailing action menu.
# #
# # @param menu_id [String, nil] id prefix for the Primer action menu.
# # @param button_aria_label [String, nil] accessible label for the
# # menu button.
# # @param system_arguments [Hash] forwarded to
# # `Primer::Alpha::ActionMenu`.
# # @return [ViewComponent::Slot]
# def with_menu(menu_id: nil, button_aria_label: nil, **system_arguments, &block)
# end
renders_one :menu, ->(menu_id: nil, button_aria_label: nil, **system_arguments) do
build_menu(menu_id:, button_aria_label:, **system_arguments)
end
}
end
renders_one :menu, ->(menu_id: nil, button_aria_label: nil, **system_arguments) do
private
def build_menu(menu_id: nil, button_aria_label: nil, **system_arguments)
system_arguments[:classes] = class_names(
system_arguments[:classes],
"hide-when-print"
)
menu = Primer::Alpha::ActionMenu.new(
menu_id: menu_id || dom_target(container, :menu),
menu_id: menu_id || default_menu_id,
anchor_align: :end,
**system_arguments
)
menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": button_aria_label || t(".label_actions"),
"aria-label": button_aria_label || I18n.t(:label_actions),
tooltip_direction: :se
)
menu
end
attr_reader :title, :container, :list_id, :collapsed, :count
def initialize(title:, container:, list_id:, collapsed: false, count: nil)
super()
@title = title
@container = container
@list_id = list_id
@collapsed = collapsed
@count = count
def default_menu_id
self.class.generate_id
end
end
end
@@ -0,0 +1,82 @@
<%# -- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++# %>
<%= grid_layout("op-border-box-list-header", tag: :div) do |grid| %>
<% grid.with_area(:collapsible) do %>
<% if collapsible? %>
<%=
render(
Primer::OpenProject::BorderBox::CollapsibleHeader.new(
collapsible_id:,
collapsed:,
multi_line: true
)
) do |collapsible|
%>
<% collapsible.with_title(tag: title_tag) { title } %>
<% if render_count? %>
<% collapsible.with_count(**counter_arguments) %>
<% end %>
<% if description? %>
<% collapsible.with_description do %>
<%= description %>
<% end %>
<% end %>
<% end %>
<% else %>
<%= render(Primer::BaseComponent.new(tag: :div, classes: "CollapsibleHeader CollapsibleHeader--multi-line")) do %>
<%= render(Primer::BaseComponent.new(tag: :div, classes: "CollapsibleHeader-title-line")) do %>
<%= render(Primer::Beta::Truncate.new(tag: title_tag, classes: "CollapsibleHeader-title Box-title")) { title } %>
<% if render_count? %>
<%= render(Primer::Beta::Counter.new(**counter_arguments, classes: class_names(counter_arguments[:classes], "CollapsibleHeader-count"))) %>
<% end %>
<% end %>
<% if description? %>
<%= render(Primer::Beta::Text.new(color: :subtle, trim: true, classes: "CollapsibleHeader-description")) do %>
<%= description %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% if actions? %>
<% grid.with_area(:actions) do %>
<% actions.each do |action| %>
<%= action %>
<% end %>
<% end %>
<% end %>
<% if menu? %>
<% grid.with_area(:menu) do %>
<%= menu %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,183 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class BorderBoxListComponent
# Structured header for {BorderBoxListComponent}.
#
# This component is part of {BorderBoxListComponent} and should not be
# used as a standalone component.
#
# The header renders through `Primer::Beta::BorderBox#with_header` and
# wraps the supplied title, count, description, actions, and menu in an
# `Primer::OpenProject::BorderBox::CollapsibleHeader`.
class Header < ApplicationComponent
include OpPrimer::ComponentHelpers
include Primer::AttributesHelper
include HasMenu
DEFAULT_ACTION_SCHEME = :default
DEFAULT_COUNT_ARGUMENTS = {
scheme: :primary,
round: true,
limit: 1_000,
hide_if_zero: true
}.freeze
# @!parse
# # Adds secondary content below the header title.
# #
# # @return [ViewComponent::Slot]
# def with_description(&block)
# end
renders_one :description
# @!parse
# # Adds a button to the header actions area.
# #
# # @param system_arguments [Hash] forwarded to `Primer::Beta::Button`.
# # @return [ViewComponent::Slot]
# def with_action_button(**system_arguments, &block)
# end
renders_many :actions, types: {
button: ->(scheme: DEFAULT_ACTION_SCHEME, **system_arguments) do
Primer::Beta::Button.new(scheme:, **system_arguments)
end
}
attr_reader :title,
:count,
:count_label,
:count_arguments,
:title_tag,
:list_id,
:interactive,
:collapsed,
:collapsible
attr_writer :collapsible_id
# @param title [String] header title.
# @param count [Integer, Boolean, nil] count badge behavior. Pass
# `nil` or `false` to hide it, `true` to infer the rendered item
# count, or an integer to render an explicit value.
# @param count_label [String, nil] accessible label for the counter
# badge. Defaults to `I18n.t(:label_x_items, count:)` when a count
# is rendered. Pass an explicit string to override.
# @param count_arguments [Hash] forwarded to `Primer::Beta::Counter`.
# Values are merged over the default counter arguments.
# @param title_tag [Symbol] tag used for the title heading.
# @param list_id [String, nil] id of the collapsible list body.
# @param interactive [Boolean] whether counter updates should be
# announced politely to assistive technology.
# @param collapsed [Boolean] whether the collapsible header starts closed.
# @param collapsible [Boolean] whether the header renders a collapsible
# toggle. Defaults to `false`. Pass `true` to render a header
# with a toggle button.
# @param system_arguments [Hash] forwarded to `Primer::Beta::BorderBox#with_header`.
def initialize(
title:,
count: nil,
count_label: nil,
count_arguments: {},
title_tag: :h4,
list_id: nil,
interactive: false,
collapsed: false,
collapsible: false,
**system_arguments
)
super()
@title = title
@count = count
@count_label = count_label
@count_arguments = count_arguments
@title_tag = title_tag
@list_id = list_id
@interactive = interactive
@collapsible_id = list_id
@collapsed = collapsed
@collapsible = collapsible
@system_arguments = system_arguments
end
# @return [Boolean] whether a collapsible toggle should be rendered.
def collapsible?
collapsible
end
# Resolves inferred counts after the list slots have been captured.
#
# @param item_count [Integer] number of rendered item slots.
# @return [void]
def resolve_count!(item_count)
@count = item_count if count == true
@count_label ||= I18n.t(:label_x_items, count: @count) if render_count?
end
# @return [Hash] arguments forwarded to `Primer::Beta::BorderBox#with_header`.
def row_args
@system_arguments.deep_dup
end
# @return [Boolean] whether a counter should be rendered.
def render_count?
!count.nil? && count != false
end
# @return [Hash] merged arguments forwarded to `Primer::Beta::Counter`.
def counter_arguments
merged = DEFAULT_COUNT_ARGUMENTS.merge(count_arguments).merge(count:)
default_aria = { label: count_label }
default_aria[:live] = "polite" if interactive
merged[:aria] = merge_aria(
{ aria: default_aria },
merged
)
merged
end
# @return [String, nil] ids controlled by the collapsible header.
def collapsible_id
@collapsible_id.presence
end
private
def default_menu_id
list_id ? "#{list_id}_menu" : super
end
end
end
end
end
@@ -23,32 +23,28 @@
#
# 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.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class WorkPackageCardListComponent
# Item bridge for caller-provided content.
class ContentItem < ApplicationComponent
class BorderBoxListComponent
# Generic BorderBox list row that renders the slot content directly.
class Item < ApplicationComponent
# @param system_arguments [Hash] forwarded to Primer's BorderBox row.
def initialize(**system_arguments)
super()
@system_arguments = system_arguments
end
# @return [Hash] arguments forwarded to Primer's BorderBox row.
def row_args
@system_arguments.deep_dup
end
def card
self
end
def empty_item? = false
def call
content
end
@@ -23,18 +23,19 @@
#
# 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.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class WorkPackageCardListComponent
# Internal row bridge between the card list and the visual card. It owns
# the surrounding BorderBox row arguments while `WorkPackageCardComponent`
# renders the card body.
class Item < ApplicationComponent
class BorderBoxListComponent
# BorderBox list row that renders a work package card.
#
# Specialized rows can subclass this component and override `build_card`
# to provide a different card component while keeping row behavior.
class WorkPackageItem < ApplicationComponent
include ActionView::RecordIdentifier
include Primer::ClassNameHelper
include Primer::AttributesHelper
@@ -45,8 +46,16 @@ module OpenProject
:params,
:current_user
delegate :with_metric, to: :card
# Delegates card slots so callers can configure the rendered card from
# `with_work_package_item`.
delegate :with_metric, :with_menu, to: :card
# @param work_package [WorkPackage] work package rendered by the card.
# @param project [Project] project context for row behavior.
# @param container [String, Array<String, Symbol>] parent list container id seed.
# @param params [Hash] request params used by specialized item classes.
# @param current_user [User] user context for specialized item classes.
# @param system_arguments [Hash] forwarded to Primer's BorderBox row.
def initialize(
work_package:,
project:,
@@ -65,11 +74,16 @@ module OpenProject
@system_arguments = system_arguments
end
# @return [Hash] arguments forwarded to Primer's BorderBox row.
def row_args
row_arguments = @system_arguments.deep_dup
row_arguments[:id] ||= dom_id(work_package)
row_arguments[:tabindex] ||= 0
row_arguments[:classes] = class_names(row_classes, row_arguments[:classes])
row_arguments[:test_selector] ||= "work-package-#{work_package.id}"
row_arguments[:classes] = class_names(
row_classes,
row_arguments[:classes]
)
row_arguments[:data] = merge_data(
{ data: row_data },
row_arguments
@@ -77,16 +91,28 @@ module OpenProject
row_arguments
end
def card
@card ||= WorkPackageCardComponent.new(work_package:)
def before_render
content
end
def render? = false
def call
render(card)
end
def empty_item? = false
# @return [ApplicationComponent] card component rendered inside the row.
def card
@card ||= build_card
end
private
# Override in subclasses to render a specialized work-package card.
#
# @return [ApplicationComponent]
def build_card
WorkPackageCardComponent.new(work_package:)
end
def row_classes
class_names(
"Box-row--hover-blue",
@@ -97,11 +123,7 @@ module OpenProject
end
def row_data
data = {
test_selector: "work-package-#{work_package.id}"
}
draggable? ? data.merge(draggable_data) : data
draggable? ? draggable_data : {}
end
def draggable?
@@ -1,309 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class WorkPackageCardListComponent < ApplicationComponent
include Primer::AttributesHelper
include OpPrimer::ComponentHelpers
# Renders a `Header` above the card list with the title, count badge, and
# consumer-provided actions/menu/description.
#
# @param title [String] heading text rendered inside the collapsible header.
# @param count [Integer, NilClass] optional count badge displayed alongside
# the title; hidden when zero or nil.
renders_one :header, ->(title:, count: nil) {
Header.new(title:, count:, container:, list_id:, collapsed: folded?)
}
# Renders a `Primer::Beta::Blankslate` when no items are produced — that
# is, when `items.empty?` after slot resolution and automatic item builds.
# The slot is required unless the caller provides manual items, and is
# silently ignored whenever `items` is non-empty.
#
# @param title [String] blankslate heading.
# @param description [String, NilClass] optional secondary text.
# @param icon [Symbol, NilClass] optional Octicon name.
# @param system_arguments [Hash] forwarded to `Primer::Beta::Blankslate`.
renders_one :empty_state, ->(title:, description: nil, icon: nil, **system_arguments) {
system_arguments[:role] = "status"
system_arguments[:aria] = merge_aria(
system_arguments,
aria: { live: "polite" }
)
blankslate = Primer::Beta::Blankslate.new(**system_arguments)
blankslate.with_heading(tag: :h4).with_content(title)
blankslate.with_description_content(description) if description
blankslate.with_visual_icon(icon:) if icon
blankslate
}
# @!parse
# # Adds a work package item row to the list. When at least one item
# # is added manually, the list does not build rows from
# # `work_packages:`.
# #
# # @param work_package [WorkPackage] the work package rendered in the row.
# # @param component_klass [Class] row bridge class used instead of the
# # default item class. Defaults to the list's configured
# # `item_component_klass`. It must accept the arguments documented on
# # `#build_item`, expose `#row_args` with valid
# # `Primer::Beta::BorderBox#with_row` keyword arguments, and expose
# # `#card` returning a renderable object.
# # @param system_arguments [Hash] forwarded to the item class.
# def with_work_package_item(
# work_package:,
# component_klass: Item,
# **system_arguments,
# &block
# )
# end
# @!parse
# # Adds a custom empty item row to the list. This can be used instead of
# # the `empty_state` slot when the caller owns item iteration. It cannot
# # be combined with `work_packages:`, `with_work_package_item`, or
# # `with_item`.
# #
# # @param system_arguments [Hash] forwarded to
# # `Primer::Beta::BorderBox#with_row`.
# def with_empty_item(**system_arguments, &block)
# end
# @!parse
# # Adds a generic item to the list. When at least one item is added
# # manually, the list does not build rows from `work_packages:`.
# #
# # @param system_arguments [Hash] forwarded to
# # `Primer::Beta::BorderBox#with_row`.
# def with_item(**system_arguments, &block)
# end
renders_many :items, types: {
work_package_item: {
renders: lambda { |work_package:, **system_arguments, &block|
build_item(work_package:, **system_arguments).tap do |item|
capture(item, &block) if block
end
},
as: :work_package_item
},
empty_item: {
renders: lambda { |**system_arguments, &block|
build_content_item(EmptyItem, **system_arguments, &block)
},
as: :empty_item
},
item: {
renders: lambda { |**system_arguments, &block|
build_content_item(ContentItem, **system_arguments, &block)
},
as: :item
}
}
# Renders a free-form footer row below the card list.
renders_one :footer
attr_reader :work_packages,
:project,
:container,
:drag_and_drop,
:item_component_klass,
:params,
:current_user
# @param project [Project] the project this card list is rendered in. May
# differ from individual `work_package.project` values when sprints or
# buckets are shared across projects.
# @param container [Symbol, String, Class, ApplicationRecord] drives the
# list DOM id and related ids via `dom_target`.
# @param work_packages [Enumerable<WorkPackage>] the work packages to render
# as cards.
# @param drag_and_drop [Hash, NilClass] optional generic drag-and-drop
# target data. Requires `:target_id` and `:allowed_drag_type` when set.
# @param item_component_klass [Class] item class used for automatically
# built work package items.
# @param params [Hash] optional URL params passed to work package items
# when deriving row arguments.
# @param current_user [User] passed through to each item for permission
# checks; defaults to `User.current`.
# @param system_arguments [Hash] forwarded to the underlying
# `Primer::Beta::BorderBox`.
def initialize(
project:,
container:,
work_packages: [],
drag_and_drop: nil,
item_component_klass: Item,
params: {},
current_user: User.current,
**system_arguments
)
super()
@work_packages = work_packages
@project = project
@container = container
@drag_and_drop = drag_and_drop
@item_component_klass = item_component_klass
@params = params
@current_user = current_user
@automatic_items = false
@system_arguments = system_arguments
@system_arguments[:id] = container_id
@system_arguments[:list_id] = list_id
@system_arguments[:padding] = :condensed
merge_drag_and_drop_data! if drag_and_drop
end
def before_render
# Content must be loaded before mode validation and automatic item builds
# so slot calls have already populated `items`.
content
validate_item_mode!
build_automatic_items if build_automatic_items?
validate_empty_state!
end
# Builds a new work package item without adding it to the list. Use this
# instead of the `#with_work_package_item` slot when rendering additional
# items outside this list, such as in a separately-loaded page.
#
# @param work_package [WorkPackage] the work package rendered in the row.
# @param component_klass [Class] item class used instead of the configured
# default item class. It must accept `work_package:`, `project:`,
# `container:`, `params:`, `current_user:`, and `**system_arguments`.
# @param system_arguments [Hash] forwarded to the item class.
def build_item(
work_package:,
component_klass: item_component_klass,
**system_arguments
)
component_klass.new(
work_package:,
project:,
container:,
params:,
current_user:,
**system_arguments
)
end
private
def folded?
current_user.pref[:backlogs_versions_default_fold_state] == "closed"
end
def build_automatic_items?
non_empty_items.empty? && work_packages.any?
end
def build_automatic_items
@automatic_items = true
work_packages.each do |work_package|
with_work_package_item(work_package:)
end
end
def build_content_item(item_class, **system_arguments, &block)
item_class.new(**system_arguments).tap do |item|
item.with_content(capture(&block)) if block
end
end
def automatic_items?
@automatic_items
end
def validate_item_mode!
return unless empty_items.any?
if work_packages.any?
raise ArgumentError, "empty_item cannot be combined with work_packages"
end
if non_empty_items.any?
raise ArgumentError, "empty_item cannot be combined with other items"
end
end
def validate_empty_state!
return unless items.empty? && !empty_state?
raise ArgumentError, "empty_state slot is required when no work package items are rendered"
end
def container_id
dom_target(container)
end
def list_id
dom_target(container, :list)
end
def header_id
dom_target(container, :header)
end
def empty_items
items.select { |item| item.respond_to?(:empty_item?) && item.empty_item? }
end
def non_empty_items
items - empty_items
end
def merge_drag_and_drop_data!
@system_arguments[:data] = merge_data(
{
data: drag_and_drop_data
},
@system_arguments
)
end
def drag_and_drop_data
{
# Existing callers share one mirror container target on the page until
# parent-specific DnD handling is extracted in follow-up work.
generic_drag_and_drop_target: "container",
target_container_accessor: ":scope > ul",
target_id: drag_and_drop.fetch(:target_id),
target_allowed_drag_type: drag_and_drop.fetch(:allowed_drag_type)
}
end
end
end
end
@@ -1,74 +0,0 @@
<%# -- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++# %>
<%= grid_layout("op-work-package-card-list-header", tag: :div) do |grid| %>
<% grid.with_area(:collapsible) do %>
<%=
render(
Primer::OpenProject::BorderBox::CollapsibleHeader.new(
collapsible_id: list_id,
collapsed:,
multi_line: true
)
) do |collapsible|
%>
<% collapsible.with_title(tag: :h4) { title } %>
<% if count %>
<% collapsible.with_count(
scheme: :primary,
count: count,
round: true,
limit: 1_000,
hide_if_zero: true,
aria: {
label: t(".label_work_package_count", count: count),
live: "polite"
}
) %>
<% end %>
<% if description? %>
<% collapsible.with_description do %>
<%= description %>
<% end %>
<% end %>
<% end %>
<% end %>
<% if actions? %>
<% grid.with_area(:actions) do %>
<% actions.each do |action| %>
<%= action %>
<% end %>
<% end %>
<% end %>
<% grid.with_area(:menu) do %>
<%= menu %>
<% end %>
<% end %>
+21 -18
View File
@@ -80,26 +80,10 @@ module Projects
send(column.attribute)
end
def custom_field_column(column) # rubocop:disable Metrics/AbcSize
def custom_field_column(column)
return nil unless user_can_view_project_attributes?
cf = column.custom_field
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,
format: false # already formatted
)
elsif custom_value.is_a?(Array)
safe_join(Array(custom_value).compact_blank, ", ")
elsif cf.calculated_value?
render_calculated_value(cf, custom_value)
else
custom_value
end
super
end
def render_calculated_value(custom_field, custom_value)
@@ -451,6 +435,25 @@ module Projects
User.current.allowed_in_project?(:view_project_phases, project)
end
def custom_field_column_subject
project
end
def format_custom_field_value(cf, custom_value)
if cf.field_format == "text" && custom_value.present?
render OpenProject::Common::AttributeComponent.new(
"dialog-#{project.id}-cf-#{cf.id}",
cf.name,
custom_value,
format: false
)
elsif cf.calculated_value?
render_calculated_value(cf, custom_value)
else
super
end
end
def custom_field_column?(column)
column.is_a?(::Queries::Projects::Selects::CustomField)
end
+27
View File
@@ -45,9 +45,22 @@ class RowComponent < ApplicationComponent
end
def column_value(column)
return custom_field_column(column) if custom_field_column?(column)
send(column)
end
def custom_field_column?(column)
column.is_a?(Queries::Selects::Shared::CustomFieldSelect)
end
def custom_field_column(column)
cf = column.custom_field
return "" unless cf
format_custom_field_value(cf, custom_field_column_subject.formatted_custom_value_for(cf))
end
def column_css_class(column)
column_css_classes[column]
end
@@ -81,4 +94,18 @@ class RowComponent < ApplicationComponent
helpers.op_icon "icon icon-checkmark"
end
end
private
def custom_field_column_subject
model
end
def format_custom_field_value(_cf, custom_value)
if custom_value.is_a?(Array)
safe_join(Array(custom_value).compact_blank, ", ")
else
custom_value.to_s
end
end
end
+50 -49
View File
@@ -26,55 +26,56 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<div class="generic-table--container <%= container_class %>" data-test-selector="<%= test_selector %>" id="<%= container_id %>">
<div class="generic-table--results-container">
<%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
<colgroup>
<% headers.each do |_name, _options| %>
<col>
<% end %>
<col data-highlight="false">
</colgroup>
<thead>
<tr>
<% headers.each do |name, options| %>
<% if sortable_column?(name) %>
<%= helpers.sort_header_tag(name, **options) %>
<% else %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= options[:caption] %>
</span>
</div>
</div>
</th>
<% end %>
<%= component_wrapper do %>
<div class="generic-table--container <%= container_class %>" data-test-selector="<%= test_selector %>" id="<%= container_id %>">
<div class="generic-table--results-container">
<%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
<colgroup>
<% headers.each do |_name, _options| %>
<col>
<% end %>
<th>
<%# last column for buttons %>
<div class="generic-table--empty-header"></div>
</th>
</tr>
</thead>
<tbody>
<% if rows.empty? %>
<tr class="generic-table--empty-row">
<td colspan="<%= headers.length + 1 %>"><%= empty_row_message %></td>
</tr>
<% end %>
<%= render_collection rows %>
</tbody>
<% end %>
<% if inline_create_link %>
<div class="wp-inline-create-button">
<%= inline_create_link %>
</div>
<% end %>
<col data-highlight="false">
</colgroup>
<thead>
<tr>
<% headers.each do |name, options| %>
<% if sortable_column?(name) %>
<%= helpers.sort_header_tag(name, **options) %>
<% else %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<%= options[:caption] %>
</span>
</div>
</div>
</th>
<% end %>
<% end %>
<th>
<%# last column for buttons %>
<div class="generic-table--empty-header"></div>
</th>
</tr>
</thead>
<tbody>
<% if rows.empty? %>
<tr class="generic-table--empty-row">
<td colspan="<%= headers.length + 1 %>"><%= empty_row_message %></td>
</tr>
<% end %>
<%= render_collection rows %>
</tbody>
<% end %>
<% if inline_create_link %>
<div class="wp-inline-create-button">
<%= inline_create_link %>
</div>
<% end %>
</div>
</div>
</div>
<% if paginated? %>
<%= helpers.pagination_links_full rows %>
<% if paginated? %>
<%= helpers.pagination_links_full rows %>
<% end %>
<% end %>
+1
View File
@@ -32,6 +32,7 @@
# Abstract view component. Subclass this for a concrete table.
class TableComponent < ApplicationComponent
include Primer::AttributesHelper
include OpTurbo::Streamable
def initialize(rows: [], table_arguments: {}, **)
super(rows, **)
@@ -0,0 +1,27 @@
<%= render(Primer::Alpha::Dialog.new(title: t(:"queries.configure_view.heading"), size: :large, id: MODAL_ID, style: "min-height: 360px")) do |d| %>
<% d.with_header(variant: :large) %>
<%= render(Primer::Alpha::Dialog::Body.new) do %>
<%= primer_form_with(url: users_path, id: QUERY_FORM_ID, data: { "turbo-stream" => false }, method: :get) do %>
<% { filters: params[:filters], sortBy: params[:sortBy] }.each do |name, value| %>
<%= hidden_field_tag name, value if value.present? %>
<% end %>
<%= helpers.angular_component_tag "opce-draggable-autocompleter",
inputs: {
options: selectable_columns,
selected: selected_columns,
name: COLUMN_HTML_NAME,
id: COLUMN_HTML_ID,
dragAreaName: "#{COLUMN_HTML_ID}_dragarea",
formControlId: "#{COLUMN_HTML_ID}_autocompleter",
inputLabel: t(:"queries.configure_view.columns.input_label"),
inputPlaceholder: t(:"queries.configure_view.columns.input_placeholder"),
dragAreaLabel: t(:"queries.configure_view.columns.drag_area_label"),
appendToComponent: true
} %>
<% end %>
<% end %>
<%= render(Primer::Alpha::Dialog::Footer.new) do %>
<%= render(Primer::Beta::Button.new(data: { "close-dialog-id": MODAL_ID })) { t(:button_cancel) } %>
<%= render(Primer::Beta::Button.new(scheme: :primary, type: :submit, data: { "test-selector": "#{MODAL_ID}-submit" }, form: QUERY_FORM_ID)) { t(:button_apply) } %>
<% end %>
<% end %>
@@ -0,0 +1,63 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Users
class ConfigureViewModalComponent < ApplicationComponent
include OpTurbo::Streamable
MODAL_ID = "op-user-list-configure-dialog"
QUERY_FORM_ID = "op-user-list-configure-query-form"
COLUMN_HTML_NAME = "columns"
COLUMN_HTML_ID = "columns-select"
options :query
def selectable_columns
@selectable_columns ||= UserQuery.new
.available_selects
.reject { |c| text_format_custom_field?(c) }
.sort_by(&:caption)
.map { |c| { id: c.attribute, name: c.caption } }
end
def selected_columns
@selected_columns ||= query.selects
.reject { |c| text_format_custom_field?(c) }
.map { |c| { id: c.attribute, name: c.caption } }
end
private
def text_format_custom_field?(select)
select.is_a?(Queries::Users::Selects::CustomField) &&
select.custom_field&.field_format == "text"
end
end
end
@@ -1,17 +1,35 @@
<%= render(Primer::OpenProject::PageHeader.new) do |header| %>
<% header.with_title { t(:label_user_plural) } %>
<% header.with_breadcrumbs(breadcrumb_items) %>
<% header.with_description do %>
<%= t(:label_enterprise_active_users, current: OpenProject::Enterprise.active_user_count, limit: user_limit) %>
&nbsp;
<%= render(
Primer::Beta::Button.new(
scheme: :primary,
size: :small,
tag: :a,
href: OpenProject::Enterprise.upgrade_path,
title: t(:title_enterprise_upgrade)
)
) { t(:button_upgrade) } %>
<% end if user_limit %>
<%= component_wrapper do %>
<%= render(Primer::OpenProject::PageHeader.new) do |header| %>
<% header.with_title { t(:label_user_plural) } %>
<% header.with_breadcrumbs(breadcrumb_items) %>
<% header.with_description do %>
<%= t(:label_enterprise_active_users, current: OpenProject::Enterprise.active_user_count, limit: user_limit) %>
&nbsp;
<%= render(
Primer::Beta::Button.new(
scheme: :primary,
size: :small,
tag: :a,
href: OpenProject::Enterprise.upgrade_path,
title: t(:title_enterprise_upgrade)
)
) { t(:button_upgrade) } %>
<% end if user_limit %>
<% header.with_action_menu(
menu_arguments: { anchor_align: :end },
button_arguments: {
icon: "kebab-horizontal",
"aria-label": t(:label_more)
}
) do |menu| %>
<% menu.with_item(
tag: :a,
label: t(:"queries.configure_view.heading"),
href: configure_view_modal_path,
content_arguments: { data: { controller: "async-dialog" }, rel: "nofollow" }
) do |item| %>
<% item.with_leading_visual_icon(icon: :gear) %>
<% end %>
<% end %>
<% end %>
<% end %>
@@ -29,9 +29,12 @@
# ++
class Users::IndexPageHeaderComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
include ApplicationHelper
options :query
delegate :user_limit, to: :"OpenProject::Enterprise"
def breadcrumb_items
@@ -39,4 +42,23 @@ class Users::IndexPageHeaderComponent < ApplicationComponent
{ href: admin_settings_users_path, text: t(:label_user_and_permission) },
t(:label_user_plural)]
end
def configure_view_modal_path
helpers.configure_view_modal_users_path(query_params)
end
private
def query_params
{ filters: helpers.params[:filters],
sortBy: helpers.params[:sortBy],
columns: current_columns }.compact_blank
end
def current_columns
return if query.nil?
cols = query.selects.map { |s| s.attribute.to_s }.join(" ")
cols.presence
end
end
@@ -1,5 +1,5 @@
<%=
render(Primer::OpenProject::SubHeader.new) do |subheader|
render(Primer::OpenProject::SubHeader.new(data: sub_header_data_attributes)) do |subheader|
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
@@ -10,8 +10,21 @@
t("activerecord.models.user")
end
subheader.with_filter_input(
name: "any_name_attribute",
label: t(:label_search),
value: filter_input_value,
placeholder: t(:label_search),
clear_button_id: clear_button_id,
data: filter_input_data_attributes
)
subheader.with_filter_component do
render Users::UserFilterButtonComponent.new(query: @query)
end
subheader.with_bottom_pane_component do
render Users::UserFilterComponent.new(@params, groups: @groups, status: @status)
render Users::UserFiltersComponent.new(query: @query, initially_expanded: filters_expanded?)
end
end
%>
@@ -32,11 +32,39 @@ module Users
class IndexSubHeaderComponent < ApplicationComponent
include ApplicationHelper
def initialize(groups:, status:, params:)
def initialize(query:)
super
@groups = groups
@status = status
@params = params
@query = query
end
def filter_input_value
@query.find_active_filter(:any_name_attribute)&.values&.first
end
def sub_header_data_attributes
{
controller: "filter--filters-form",
"filter--filters-form-perform-turbo-requests-value": true,
"filter--filters-form-clear-button-id-value": clear_button_id,
"filter--filters-form-display-filters-value": filters_expanded?
}
end
def filter_input_data_attributes
{
"filter-name": "any_name_attribute",
"filter-type": "string",
"filter-operator": "~",
"filter--filters-form-target": "simpleFilter filterValueContainer simpleValue"
}
end
def clear_button_id
"user-filters-form-clear-button"
end
def filters_expanded?
params[:filters].present?
end
end
end
+17 -6
View File
@@ -98,14 +98,25 @@ module Users
user.admin? && !table.current_user.admin?
end
def column_value(column)
return custom_field_column(column) if custom_field_column?(column)
send(column.respond_to?(:attribute) ? column.attribute : column)
end
def column_css_class(column)
if column == :mail
"email"
elsif column == :login
"username"
else
super
attr = column.respond_to?(:attribute) ? column.attribute : column
case attr
when :mail then "email"
when :login then "username"
else attr.to_s
end
end
private
def custom_field_column_subject
user
end
end
end
+21 -7
View File
@@ -33,21 +33,35 @@ module Users
columns :login, :firstname, :lastname, :mail, :admin, :created_at, :last_login_on
options :current_user
def before_render
@user_query = model if model.is_a?(Queries::BaseQuery)
super
end
def columns
@columns ||= if @user_query&.selects&.any?
@user_query.selects
else
super
end
end
def initial_sort
%i[id asc]
end
def headers
columns.map do |name|
[name.to_s, header_options(name)]
columns.map do |column|
key = column.respond_to?(:attribute) ? column.attribute.to_s : column.to_s
[key, header_options(column)]
end
end
def header_options(name)
options = { caption: User.human_attribute_name(name) }
options[:default_order] = "desc" if desc_by_default.include? name
def header_options(column)
attr = column.respond_to?(:attribute) ? column.attribute : column
caption = column.respond_to?(:caption) ? column.caption : User.human_attribute_name(attr)
options = { caption: }
options[:default_order] = "desc" if desc_by_default.include?(attr)
options
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Users
class UserFilterButtonComponent < Filter::FilterButtonComponent
HIDDEN_FILTERS = [
Queries::Users::Filters::AnyNameAttributeFilter,
Queries::Users::Filters::BlockedFilter
].freeze
private
def filters_count
query.filters.count { |f| HIDDEN_FILTERS.none? { |klass| f.is_a?(klass) } }
end
end
end
@@ -0,0 +1,58 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Users
class UserFiltersComponent < Filter::FilterComponent
def turbo_requests? = true
def allowed_filters
super
.grep_v(Queries::Users::Filters::AnyNameAttributeFilter)
.grep_v(Queries::Users::Filters::BlockedFilter)
.sort_by(&:human_name)
end
protected
def additional_filter_attributes(filter)
case filter
when Queries::Users::Filters::GroupFilter
{
autocomplete_options: {
component: "opce-group-autocompleter",
resource: "groups"
}
}
else
super
end
end
end
end
@@ -0,0 +1,7 @@
<%=
render(Primer::Beta::Blankslate.new(border: true, test_selector: "type-form-configuration-blankslate")) do |blankslate|
blankslate.with_visual_icon(icon: :rows)
blankslate.with_heading(tag: :h3) { t("types.edit.form_configuration.blankslate_title") }
blankslate.with_description { t("types.edit.form_configuration.blankslate_description") }
end
%>
@@ -28,14 +28,9 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module Users
class UserFilterComponent < ::UserFilterComponent
def filter_role(query, role_id)
super.uniq
end
def clear_url
users_path
module WorkPackageTypes
module FormConfiguration
class BlankslateComponent < ApplicationComponent
end
end
end
@@ -0,0 +1,74 @@
<%=
flex_layout(justify_content: :space_between, align_items: :center) do |row|
row.with_column(flex_layout: true, align_items: :center, flex: 1) do |left|
left.with_column(mr: 2, classes: "hide-when-print type-form-configuration-page--drag-handle") do
render(
Primer::OpenProject::DragHandle.new(
classes: "attribute-handle",
"aria-label": t("types.edit.form_configuration.drag_to_reorder"),
test_selector: "type-form-configuration-attribute-handle-#{@attribute[:key]}"
)
)
end
left.with_column(flex: 1) do
render(Primer::Beta::Text.new) { @attribute[:translation] }
end
end
row.with_column(classes: "hide-when-print type-form-configuration-page--actions") do
render(Primer::Alpha::ActionMenu.new) do |menu|
menu.with_show_button(
icon: "kebab-horizontal",
scheme: :invisible,
size: :small,
classes: "type-form-configuration-page--actions-button",
test_selector: "type-form-configuration-attribute-actions-#{@attribute[:key]}",
"aria-label": t("types.edit.form_configuration.row_actions")
)
if attribute_can_move_up?
move_action(
menu:,
href: row_move_path(:highest),
label: t("label_agenda_item_move_to_top"),
icon: "move-to-top"
)
move_action(
menu:,
href: row_move_path(:higher),
label: t("label_agenda_item_move_up"),
icon: "chevron-up"
)
end
if attribute_can_move_down?
move_action(
menu:,
href: row_move_path(:lower),
label: t("label_agenda_item_move_down"),
icon: "chevron-down"
)
move_action(
menu:,
href: row_move_path(:lowest),
label: t("label_agenda_item_move_to_bottom"),
icon: "move-to-bottom"
)
end
menu.with_divider if show_delete_divider?
menu.with_item(
label: t("button_delete"),
test_selector: "type-form-configuration-delete-attribute-#{@attribute[:key]}",
scheme: :danger,
tag: :a,
href: row_destroy_path,
content_arguments: { data: { turbo_method: :delete, turbo_stream: true } }
) do |item|
item.with_leading_visual_icon(icon: :trash)
end
end
end
end
%>
@@ -0,0 +1,82 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class GroupAttributeRowComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def initialize(attribute:, type:, index:, total_count:)
super
@attribute = attribute
@type = type
@index = index
@total_count = total_count
end
private
def multiple_attributes?
@total_count > 1
end
def attribute_can_move_up?
multiple_attributes? && !@index.zero?
end
def attribute_can_move_down?
multiple_attributes? && @index != @total_count - 1
end
def show_delete_divider?
attribute_can_move_up? || attribute_can_move_down?
end
def row_move_path(move_to)
move_type_form_configuration_row_path(@type, @attribute[:key], move_to:)
end
def row_destroy_path
type_form_configuration_row_path(@type, @attribute[:key])
end
def move_action(menu:, href:, label:, icon:)
menu.with_item(
label:,
tag: :a,
href:,
content_arguments: { data: { turbo_method: :put, turbo_stream: true } }
) do |item|
item.with_leading_visual_icon(icon:)
end
end
end
end
end
@@ -0,0 +1,94 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
component_wrapper(
data: wrapper_data
) do
render(border_box_container(mt: 3, mb: (last? ? 3 : 0), padding: :condensed, data: row_drop_target_config)) do |box|
box.with_header(font_weight: :bold) do
render(
WorkPackageTypes::FormConfiguration::GroupHeaderComponent.new(
group: @group,
type: @type,
ee_available: ee_available?,
first: first?,
last: last?,
edit_mode: edit_mode?,
form_model: @form_model
)
)
end
if query_group?
box.with_row do
render(
WorkPackageTypes::FormConfiguration::GroupQueryRowComponent.new(
group: @group,
ee_available: ee_available?
)
)
end
elsif attributes.empty?
box.with_row(data: { "empty-list-item": true }) do
flex_layout(align_items: :center) do |empty_row|
empty_row.with_column(mr: 2, classes: "type-form-configuration-page--row-alignment-spacer")
empty_row.with_column(flex: 1) do
render(Primer::Beta::Text.new(color: :subtle, font_style: :italic)) do
t("types.edit.form_configuration.empty_group_hint")
end
end
end
end
else
attributes.each_with_index do |attribute, index|
box.with_row(
data: {
attr_key: attribute[:key],
attr_translation: attribute[:translation],
attr_is_cf: attribute[:is_cf],
"draggable-id": attribute[:key],
"draggable-type": "attribute",
"drop-url": row_drop_path(attribute)
}
) do
render(
WorkPackageTypes::FormConfiguration::GroupAttributeRowComponent.new(
attribute:,
type: @type,
index:,
total_count: attributes.length
)
)
end
end
end
end
end
%>
@@ -0,0 +1,143 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class GroupComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(group:, type: nil, ee_available: false, first: false, last: false, edit_mode: false,
form_model: nil)
super(group)
@group = group
@type = type
@ee_available = ee_available
@first = first
@last = last
@edit_mode = edit_mode
@form_model = form_model
@instance_uid = SecureRandom.hex(4)
end
def wrapper_uniq_by
@group[:key].presence || @instance_uid
end
def edit_mode?
@edit_mode
end
def query_group?
@group[:type].to_s == "query"
end
def attributes
@group[:attributes] || []
end
def first?
@first
end
def last?
@last
end
def ee_available?
@ee_available
end
private
def wrapper_data
{
group_type: @group[:type].to_s,
group_key: @group[:key].to_s,
group_query: @group[:query],
edit_mode: (true if edit_mode?)
}.compact.merge(draggable_item_config)
end
def group_name
@group[:name]
end
def temporary_group?
@group[:temporary]
end
def draggable_item_config
return {} if @group[:key].blank? || temporary_group?
{
"draggable-id": @group[:key],
"draggable-type": "group",
"drop-url": drop_type_form_configuration_group_path(@type, @group[:key])
}
end
def row_drop_target_config
return {} if query_group? || @group[:key].blank? || temporary_group?
{
"admin--type-form-configuration--rows-drag-and-drop-target": "container",
"target-container-accessor": ".Box > ul",
"target-id": @group[:key],
"target-allowed-drag-type": "attribute"
}
end
def edit_path
edit_type_form_configuration_group_path(@type, @group[:key])
end
def update_path
type_form_configuration_group_path(@type, @group[:key])
end
def cancel_edit_path
cancel_edit_type_form_configuration_group_path(@type, @group[:key])
end
def move_path(move_to)
move_type_form_configuration_group_path(@type, @group[:key], move_to:)
end
def destroy_path
type_form_configuration_group_path(@type, @group[:key])
end
def row_drop_path(attribute)
drop_type_form_configuration_row_path(@type, attribute[:key])
end
end
end
end
@@ -0,0 +1,99 @@
<% if edit_mode? %>
<%=
primer_form_with(
model: form_model,
scope: :group,
method: form_method,
url: update_path,
test_selector: "type-form-configuration-group-edit-form",
data: {
turbo_stream: true
}
) do |form|
render(
WorkPackageTypes::FormConfiguration::GroupForm.new(
form,
cancel_path: cancel_edit_path
)
)
end
%>
<% else %>
<%=
flex_layout(justify_content: :space_between, align_items: :center) do |header|
header.with_column(flex_layout: true, align_items: :center, flex: 1) do |left|
left.with_column(mr: 2, classes: "hide-when-print type-form-configuration-page--drag-handle") do
render(
Primer::OpenProject::DragHandle.new(
classes: "group-handle",
"aria-label": t("types.edit.form_configuration.drag_to_reorder"),
test_selector: "type-form-configuration-group-handle-#{@group[:key]}"
)
)
end
left.with_column(flex: 1) do
render(Primer::Beta::Text.new(font_weight: :bold)) { group_name }
end
end
header.with_column(classes: "hide-when-print type-form-configuration-page--actions") do
render(Primer::Alpha::ActionMenu.new) do |menu|
menu.with_show_button(
icon: "kebab-horizontal",
scheme: :invisible,
size: :small,
classes: "type-form-configuration-page--actions-button",
test_selector: "type-form-configuration-group-actions-#{@group[:key]}",
"aria-label": t("types.edit.form_configuration.group_actions")
)
if ee_available?
with_item_group(menu) do
menu.with_item(
label: t("types.edit.form_configuration.rename_group"),
test_selector: "type-form-configuration-group-rename-#{@group[:key]}",
tag: :a,
href: edit_path,
content_arguments: { data: { turbo_stream: true } }
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
end
with_item_group(menu) do
unless first?
move_action(menu:, href: move_path(:highest), label: t("label_agenda_item_move_to_top"), icon: "move-to-top")
move_action(menu:, href: move_path(:higher), label: t("label_agenda_item_move_up"), icon: "chevron-up")
end
unless last?
move_action(menu:, href: move_path(:lower), label: t("label_agenda_item_move_down"), icon: "chevron-down")
move_action(menu:, href: move_path(:lowest), label: t("label_agenda_item_move_to_bottom"), icon: "move-to-bottom")
end
end
if ee_available?
with_item_group(menu) do
menu.with_item(
label: t("button_delete"),
scheme: :danger,
tag: :a,
href: destroy_path,
content_arguments: {
data: {
turbo_method: :delete,
turbo_stream: true,
turbo_confirm: t("types.edit.form_configuration.confirm_delete_group")
}
}
) do |item|
item.with_leading_visual_icon(icon: :trash)
end
end
end
end
end
end
%>
<% end %>
@@ -0,0 +1,119 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class GroupHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def initialize(group:, type:, ee_available:, first:, last:, edit_mode:, form_model: nil)
super
@group = group
@type = type
@ee_available = ee_available
@first = first
@last = last
@edit_mode = edit_mode
@form_model = form_model
end
def edit_mode?
@edit_mode
end
private
def group_name
@group[:name]
end
def form_model
@form_model || WorkPackageTypes::FormConfiguration::GroupFormModel.from_group(@group)
end
def ee_available?
@ee_available
end
def first?
@first
end
def last?
@last
end
def edit_path
edit_type_form_configuration_group_path(@type, @group[:key])
end
def update_path
return create_path if temporary_group?
type_form_configuration_group_path(@type, @group[:key])
end
def form_method
temporary_group? ? :post : :patch
end
def cancel_edit_path
cancel_edit_type_form_configuration_group_path(@type, @group[:key])
end
def move_path(move_to)
move_type_form_configuration_group_path(@type, @group[:key], move_to:)
end
def destroy_path
type_form_configuration_group_path(@type, @group[:key])
end
def temporary_group?
@group[:temporary]
end
def create_path
type_form_configuration_groups_path(@type)
end
def move_action(menu:, href:, label:, icon:)
menu.with_item(
label:,
tag: :a,
href:,
content_arguments: { data: { turbo_method: :put, turbo_stream: true } }
) do |item|
item.with_leading_visual_icon(icon:)
end
end
end
end
end
@@ -0,0 +1,49 @@
<%=
flex_layout(justify_content: :space_between, align_items: :center) do |row|
row.with_column(flex_layout: true, align_items: :center, flex: 1) do |left|
left.with_column(mr: 2, classes: "type-form-configuration-page--row-alignment-spacer")
left.with_column(flex: 1) do
if ee_available?
render(
Primer::Beta::Button.new(
scheme: :link,
data: { action: "click->admin--type-form-configuration--main#editQuery" }
)
) do
t("types.edit.form_configuration.query_group_label")
end
else
render(Primer::Beta::Text.new(color: :subtle, font_style: :italic)) do
t("types.edit.form_configuration.query_group_label")
end
end
end
end
if ee_available?
row.with_column(classes: "hide-when-print type-form-configuration-page--actions") do
render(Primer::Alpha::ActionMenu.new) do |menu|
menu.with_show_button(
icon: "kebab-horizontal",
scheme: :invisible,
size: :small,
classes: "type-form-configuration-page--actions-button",
test_selector: "type-form-configuration-query-actions-#{@group[:key]}",
"aria-label": t("types.edit.form_configuration.row_actions")
)
menu.with_item(
label: t("types.edit.form_configuration.edit_query"),
test_selector: "type-form-configuration-edit-query-#{@group[:key]}",
tag: :button,
content_arguments: {
data: { action: "click->admin--type-form-configuration--main#editQuery" }
}
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
end
end
end
%>
@@ -28,23 +28,21 @@
# See COPYRIGHT and LICENSE files for more details.
#++
module OpenProject
module Common
class WorkPackageCardListComponent
# Row bridge for caller-provided empty content.
class EmptyItem < ContentItem
include Primer::AttributesHelper
module WorkPackageTypes
module FormConfiguration
class GroupQueryRowComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def row_args
system_arguments = @system_arguments.deep_dup
system_arguments[:data] = merge_data(
{ data: { empty_list_item: true } },
system_arguments
)
system_arguments
end
def initialize(group:, ee_available:)
super
@group = group
@ee_available = ee_available
end
def empty_item? = true
private
def ee_available?
@ee_available
end
end
end
@@ -0,0 +1,43 @@
<%=
component_wrapper(
id: "type-form-configuration-inactive-container",
data: container_data
) do
render(
Primer::Alpha::ActionList.new(
role: :list,
classes: "type-form-configuration-page--inactive-list",
data: { "test-selector": "type-form-configuration-inactive-list" }
)
) do |list|
if @inactive_attributes.empty?
list.with_item(
label: t("types.edit.form_configuration.no_inactive_attributes"),
disabled: true,
classes: "type-form-configuration-page--inactive-empty",
content_arguments: { tag: :div },
data: { "empty-list-item": true, "filter--filter-list-target": "searchItem" }
)
else
@inactive_attributes.each do |attribute|
list.with_item(
label: attribute[:translation],
classes: "type-form-configuration-page--inactive-item",
content_arguments: { tag: :div },
data: item_data(attribute).merge("filter--filter-list-target": "searchItem")
) do |item|
item.with_leading_visual_raw_content do
render(
Primer::OpenProject::DragHandle.new(
classes: "attribute-handle",
"aria-label": t("types.edit.form_configuration.drag_to_reorder"),
test_selector: "type-form-configuration-attribute-handle-#{attribute[:key]}"
)
)
end
end
end
end
end
end
%>
@@ -0,0 +1,67 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class InactiveAttributesListComponent < ApplicationComponent
include OpTurbo::Streamable
def initialize(type:, inactive_attributes:)
super
@type = type
@inactive_attributes = inactive_attributes
end
private
def container_data
{
"test-selector": "type-form-configuration-inactive-container",
"admin--type-form-configuration--main-target": "inactiveContainer",
"admin--type-form-configuration--rows-drag-and-drop-target": "container",
"target-container-accessor": "[data-test-selector='type-form-configuration-inactive-list']",
"target-id": "inactive",
"target-allowed-drag-type": "attribute"
}
end
def item_data(attribute)
{
attr_key: attribute[:key],
attr_translation: attribute[:translation],
attr_is_cf: attribute[:is_cf],
"draggable-id": attribute[:key],
"draggable-type": "attribute",
"drop-url": drop_type_form_configuration_row_path(@type, attribute[:key])
}
end
end
end
end
@@ -0,0 +1,42 @@
<%=
flex_layout(data: { controller: "filter--filter-list" }) do |flex|
flex.with_row(mb: 1) do
render(Primer::Beta::Heading.new(tag: :h3, font_size: 4)) do
t("types.edit.form_configuration.inactive_attributes_heading")
end
end
flex.with_row(mb: 3) do
render(Primer::Beta::Text.new(color: :subtle, font_size: :small)) do
t("types.edit.form_configuration.drag_to_activate")
end
end
flex.with_row(mb: 3) do
render(
Primer::Alpha::TextField.new(
name: "inactive-filter",
label: t("types.edit.form_configuration.filter_inactive"),
visually_hide_label: true,
placeholder: t("types.edit.form_configuration.filter_inactive"),
leading_visual: { icon: :search },
size: :medium,
"full-width": true,
data: {
action: "input->filter--filter-list#filterLists",
"filter--filter-list-target": "filter"
}
)
)
end
flex.with_row do
render(
WorkPackageTypes::FormConfiguration::InactiveAttributesListComponent.new(
inactive_attributes: @inactive_attributes,
type: @type
)
)
end
end
%>
@@ -0,0 +1,43 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class InactiveAttributesSidebarComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def initialize(type:, inactive_attributes:)
super
@type = type
@inactive_attributes = inactive_attributes
end
end
end
end
@@ -0,0 +1,73 @@
<%=
component_wrapper(
class: "type-form-configuration-page--main-inner",
data: main_inner_data
) do
flex_layout do |main|
main.with_row do
render(Primer::OpenProject::SubHeader.new(mb: 0)) do |subheader|
subheader.with_action_button(
tag: :a,
scheme: :secondary,
leading_icon: :undo,
label: t("types.edit.form_configuration.reset_to_defaults"),
href: reset_dialog_type_form_configuration_path(@type),
"data-test-selector": "type-form-configuration-reset-button",
data: { controller: "async-dialog" }
) do
t("types.edit.form_configuration.reset_to_defaults")
end
if ee_available?
subheader.with_action_menu(
leading_icon: :plus,
trailing_icon: :"triangle-down",
label: t(:button_add),
anchor_align: :end,
button_arguments: {
scheme: :primary,
"aria-label": t(:button_add),
"data-test-selector": "type-form-configuration-add-button"
}
) do |menu|
menu.with_item(
label: t("types.edit.form_configuration.add_attribute_group"),
tag: :a,
href: add_group_type_form_configuration_groups_path(@type, group_type: :attribute),
content_arguments: {
data: { turbo_method: :post, turbo_stream: true }
}
) do |item|
item.with_leading_visual_icon(icon: :rows)
end
menu.with_item(
label: t("types.edit.form_configuration.add_query_group"),
tag: :button,
content_arguments: {
data: { action: "click->admin--type-form-configuration--main#addQueryGroup" }
}
) do |item|
item.with_leading_visual_icon(icon: :table)
end
end
end
end
end
main.with_row do
content_tag(
:div,
id: "type-form-configuration-groups-container",
data: groups_container_data
) do
if @group_components.empty?
render(WorkPackageTypes::FormConfiguration::BlankslateComponent.new)
else
safe_join(@group_components.map { |group| render(group) })
end
end
end
end
end
%>
@@ -0,0 +1,67 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class MainContentComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(type:, group_components:, ee_available:)
super
@type = type
@group_components = group_components
@ee_available = ee_available
end
private
def ee_available?
@ee_available
end
def main_inner_data
{
controller: "admin--type-form-configuration--drag-and-drop",
"admin--type-form-configuration--drag-and-drop-handle-selector-value": ".group-handle"
}
end
def groups_container_data
{
"test-selector": "type-form-configuration-groups-container",
"admin--type-form-configuration--main-target": "groupsContainer",
"admin--type-form-configuration--drag-and-drop-target": "container",
"target-allowed-drag-type": "group"
}
end
end
end
end
@@ -0,0 +1,24 @@
<%=
render(
Primer::OpenProject::DangerDialog.new(
id: "type-form-configuration-reset-dialog",
test_selector: "type-form-configuration-reset-dialog",
title: t("types.edit.form_configuration.reset_title"),
confirm_button_text: t("button_reset"),
form_arguments: {
action: type_form_configuration_path(@type),
method: :patch,
data: { turbo_stream: true }
}
)
) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { t("types.edit.form_configuration.confirm_reset") }
message.with_description_content(t("types.edit.form_configuration.reset_description"))
end
dialog.with_additional_details do
hidden_field_tag("type[attribute_groups]", "[]")
end
end
%>
@@ -0,0 +1,42 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class ResetDialogComponent < ApplicationComponent
include OpTurbo::Streamable
def initialize(type:)
super
@type = type
end
end
end
end
@@ -27,10 +27,28 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= I18n.t("meeting.email.participant_added.summary_series", title: @series.title, actor: @actor, participant: @added_participant) %>
<%= component_wrapper(
class: "type-form-configuration-page--wrapper",
data: wrapper_data
) do %>
<%= render(Primer::Alpha::Layout.new(stacking_breakpoint: :md, overflow: :hidden, h: :full, classes: "type-form-configuration-page")) do |content| %>
<% content.with_sidebar(row_placement: :start, col_placement: :start, border: true, border_bottom: 0, p: 3, overflow: :auto, classes: "type-form-configuration-page--sidebar") do %>
<%= render(
WorkPackageTypes::FormConfiguration::InactiveAttributesSidebarComponent.new(
type: @type,
inactive_attributes: @inactive_attributes
)
) %>
<% end %>
<%= @series.project.name %>: <%= @series.title %> (<%= recurring_meeting_url(@series) %>)
<%= t :label_recurring_meeting_schedule %>: <%= @series.full_schedule_in_words %> (<%= formatted_time_zone_offset %>)
<%= Meeting.human_attribute_name(:location) %>: <%= @template.location %>
<%= Meeting.human_attribute_name(:participants_invited) %>: <%= @template.participants.invited.sort.join("; ") %>
<% content.with_main(overflow: :auto, classes: "type-form-configuration-page--main") do %>
<%= render(
WorkPackageTypes::FormConfiguration::MainContentComponent.new(
type: @type,
group_components: group_components,
ee_available: ee_available?
)
) %>
<% end %>
<% end %>
<% end %>
@@ -0,0 +1,70 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
class FormConfigurationComponent < ApplicationComponent
include OpTurbo::Streamable
include OpPrimer::ComponentHelpers
def initialize(type:, form_attributes:, no_filter_query:)
super(type)
@type = type
@groups = form_attributes[:actives].reject { |g| g[:key].to_s == "__empty" }
@inactive_attributes = form_attributes[:inactives]
@no_filter_query = no_filter_query
end
def ee_available?
EnterpriseToken.allows_to?(:edit_attribute_groups)
end
def wrapper_data
{
controller: "admin--type-form-configuration--main admin--type-form-configuration--rows-drag-and-drop",
"admin--type-form-configuration--main-no-filter-query-value": @no_filter_query,
"admin--type-form-configuration--main-add-group-url-value": add_group_type_form_configuration_groups_path(@type),
"admin--type-form-configuration--main-groups-url-value": type_form_configuration_groups_path(@type),
"admin--type-form-configuration--rows-drag-and-drop-handle-selector-value": ".attribute-handle"
}
end
def group_components
@groups.map.with_index do |group, i|
WorkPackageTypes::FormConfiguration::GroupComponent.new(
group:,
type: @type,
ee_available: ee_available?,
first: i == 0,
last: i == @groups.length - 1
)
end
end
end
end
@@ -94,7 +94,10 @@ module WorkPackages
end
def projects
@projects ||= work_packages.filter_map(&:project).uniq
@projects ||= begin
all_work_packages = work_packages + descendants_by_work_package.values.flatten
all_work_packages.filter_map(&:project).uniq
end
end
def descendants_by_work_package
@@ -48,7 +48,9 @@ module WorkPackageTypes
seen = Set.new
model.attribute_groups.each do |group|
errors.add(:attribute_groups, :group_without_name) if group.key.blank?
errors.add(:attribute_groups, :duplicate_group, group: group.key) if seen.add?(group.key).nil?
group_name = visible_group_name(group)
errors.add(:attribute_groups, :duplicate_group, group: group_name) if seen.add?(group_name).nil?
end
end
@@ -65,14 +67,10 @@ module WorkPackageTypes
end
def validate_query_group(group)
query = group.query
query_call = rebuild_query_group(group)
return add_invalid_query_error(group, query_call.errors) unless query_call.success?
contract_class = query.persisted? ? Queries::UpdateContract : Queries::CreateContract
contract = contract_class.new(query, user)
unless contract.validate
errors.add(:attribute_groups, :query_invalid, group: group.key, details: contract.errors.full_messages.join)
end
validate_rebuilt_query_group(group, query_call.result)
end
def validate_attribute_group(group)
@@ -92,10 +90,42 @@ module WorkPackageTypes
def custom_groups_modified?
return false unless model.attribute_groups_changed?
old_keys = model.attribute_groups_was.map(&:first)
old_keys = normalized_old_keys
new_keys = model.attribute_groups.map(&:key)
(new_keys - old_keys - Type.default_groups.keys).any?
end
def normalized_old_keys
seen_keys = model.attribute_groups_was.filter_map(&:first).compact_blank.map(&:to_s)
model.attribute_groups_was.map do |group|
key = group.first.presence&.to_s
key || normalized_legacy_group_key(seen_keys).tap { |legacy_key| seen_keys << legacy_key }
end
end
def normalized_legacy_group_key(seen_keys)
Type::FormGroup.next_untitled_key(seen_keys)
end
def visible_group_name(group)
group.translated_key.to_s.strip
end
def rebuild_query_group(group)
::WorkPackageTypes::FormConfiguration::EmbeddedQueryBuilder.rebuild(query: group.query, user:)
end
def validate_rebuilt_query_group(group, query)
contract = Queries::CreateContract.new(query, user)
return if contract.validate
add_invalid_query_error(group, contract.errors)
end
def add_invalid_query_error(group, error_collection)
errors.add(:attribute_groups, :query_invalid, group: group.key, details: error_collection.full_messages.to_sentence)
end
end
end
@@ -0,0 +1,94 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfigurationComponentStreams
extend ActiveSupport::Concern
private
def update_form_configuration_via_turbo_stream(**)
update_main_content_via_turbo_stream(**)
update_inactive_attributes_via_turbo_stream
end
def update_main_content_via_turbo_stream(groups: active_groups_for_form, editing_group_key: nil, form_model: nil)
ee_available = EnterpriseToken.allows_to?(:edit_attribute_groups)
group_components = build_group_components(
groups:,
ee_available:,
editing_group_key:,
form_model:
)
update_via_turbo_stream(
component: WorkPackageTypes::FormConfiguration::MainContentComponent.new(
type: @type,
group_components:,
ee_available:
)
)
end
def update_inactive_attributes_via_turbo_stream
replace_via_turbo_stream(
component: WorkPackageTypes::FormConfiguration::InactiveAttributesListComponent.new(
inactive_attributes: form_configuration_groups(@type)[:inactives],
type: @type
),
target: "type-form-configuration-inactive-container"
)
end
def render_form_configuration_error(call)
render_error_flash_message_via_turbo_stream(message: call.errors.full_messages.to_sentence)
end
def build_group_components(groups:, ee_available:, editing_group_key:, form_model:)
groups.map.with_index do |group, index|
is_editing = editing_group_key.present? && group[:key].to_s == editing_group_key.to_s
WorkPackageTypes::FormConfiguration::GroupComponent.new(
group:,
type: @type,
ee_available:,
first: index.zero?,
last: index == groups.length - 1,
edit_mode: is_editing,
form_model: (form_model if is_editing)
)
end
end
def active_groups_for_form
form_configuration_groups(@type)[:actives].reject { |group| group[:key].to_s == "__empty" }
end
end
end
+20 -3
View File
@@ -79,11 +79,19 @@ class UsersController < ApplicationController
include SortHelper
include CustomFieldsHelper
include PaginationHelper
include Queries::Loading
before_action :load_query_or_deny_access, only: %i[index configure_view_modal]
def index
@groups = Group.visible.sort
@status = Users::UserFilterComponent.status_param params
@users = Users::UserFilterComponent.filter params
respond_to do |format|
format.html
format.turbo_stream { render_index_turbo_stream }
end
end
def configure_view_modal
respond_with_dialog Users::ConfigureViewModalComponent.new(query: @query)
end
def show
@@ -423,6 +431,15 @@ class UsersController < ApplicationController
status: User.statuses[:invited])
end
def render_index_turbo_stream # rubocop:disable Metrics/AbcSize
replace_via_turbo_stream(component: Users::IndexPageHeaderComponent.new(query: @query))
update_via_turbo_stream(component: Users::UserFilterButtonComponent.new(query: @query))
replace_via_turbo_stream(component: Users::TableComponent.new(rows: @query, current_user:))
turbo_streams << turbo_stream.push_state(url_for(params.permit(:filters, :sortBy, :sort, :page, :per_page, :columns)))
turbo_streams << turbo_stream.replace("primerized-flash-messages", helpers.render_flash_messages)
render turbo_stream: turbo_streams
end
def prepare_views_for_tab # rubocop:disable Metrics/AbcSize
if params[:tab] == "non_working_times"
authorize_manage_working_times
@@ -0,0 +1,240 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
class FormConfigurationGroupsTabController < BaseTabController
include TypesHelper
include OpTurbo::ComponentStream
include WorkPackageTypes::FormConfigurationComponentStreams
TEMPORARY_GROUP_KEY = "__new_form_configuration_group__"
def edit
update_main_content_via_turbo_stream(editing_group_key: group_key_param)
respond_with_turbo_streams
end
def add_group
render_temporary_group_editor
respond_with_turbo_streams
end
def create
call = create_group_call
if call.success?
update_form_configuration_via_turbo_stream
else
render_create_error(call)
end
respond_with_turbo_streams(status: turbo_status_for(call))
end
def cancel_edit
if temporary_group_key?(group_key_param)
update_form_configuration_via_turbo_stream
respond_with_turbo_streams
return
end
group = find_group(group_key_param)
return head :not_found if group.nil?
update_main_content_via_turbo_stream
respond_with_turbo_streams
end
def update
call = rename_group_call
if call.success?
update_form_configuration_via_turbo_stream
else
render_existing_group_update_error(call)
end
respond_with_turbo_streams(status: turbo_status_for(call))
end
def destroy
call = ::WorkPackageTypes::FormConfigurationGroups::DeleteService
.new(user: current_user, type: @type, group_key: group_key_param)
.call
if call.success?
update_form_configuration_via_turbo_stream
else
render_form_configuration_error(call)
end
respond_with_turbo_streams(status: turbo_status_for(call))
end
def drop
call = ::WorkPackageTypes::FormConfigurationGroups::UpdateService
.new(user: current_user, type: @type, group_key: group_key_param)
.call(position: params[:position])
if call.success?
update_main_content_via_turbo_stream
else
render_form_configuration_error(call)
end
respond_with_turbo_streams(status: turbo_status_for(call))
end
def move
call = ::WorkPackageTypes::FormConfigurationGroups::UpdateService
.new(user: current_user, type: @type, group_key: group_key_param)
.call(move_to: params[:move_to])
if call.success?
update_main_content_via_turbo_stream
else
render_form_configuration_error(call)
end
respond_with_turbo_streams(status: turbo_status_for(call))
end
def update_query
call = ::WorkPackageTypes::FormConfigurationGroups::UpdateService
.new(user: current_user, type: @type, group_key: group_key_param)
.call(query_props: params[:query])
if call.success?
head :ok
else
render_form_configuration_error(call)
respond_with_turbo_streams(status: turbo_status_for(call))
end
end
private
def group_params
params.expect(group: %i[name group_type query])
end
def find_group(key)
@type.attribute_groups.find do |group|
[
group.key,
group.display_name,
group.translated_key
].compact.map(&:to_s).include?(key.to_s)
end
end
def group_key_param
params[:key] || params[:id]
end
def temporary_group_key?(key)
key.to_s == TEMPORARY_GROUP_KEY
end
def temporary_group(group_type:, query:, name: "")
{
key: TEMPORARY_GROUP_KEY,
type: group_type.to_s,
name:,
attributes: [],
query:,
temporary: true
}
end
def turbo_status_for(call)
call.success? ? :ok : :unprocessable_entity
end
def create_group_call
::WorkPackageTypes::FormConfigurationGroups::CreateService
.new(user: current_user, type: @type)
.call(
group_type: group_params[:group_type],
name: group_params[:name],
query_props: group_params[:query]
)
end
def rename_group_call
::WorkPackageTypes::FormConfigurationGroups::UpdateService
.new(user: current_user, type: @type, group_key: group_key_param)
.call(name: group_params[:name])
end
def render_create_error(call)
@type.reload
group = temporary_group(
group_type: group_params[:group_type],
query: group_params[:query],
name: group_params[:name].to_s
)
render_temporary_group_editor(
group:,
form_model: group_form_model(group:, validation_message: call.errors.map(&:message).to_sentence)
)
end
def render_existing_group_update_error(call)
@type.reload
group = active_groups_for_form.find { |active_group| active_group[:key].to_s == group_key_param.to_s }
update_main_content_via_turbo_stream(
editing_group_key: group_key_param,
form_model: group_form_model(
group:,
name: group_params[:name].to_s,
validation_message: call.errors.map(&:message).to_sentence
)
)
end
def render_temporary_group_editor(group: temporary_group(group_type: params[:group_type], query: params[:query]),
form_model: nil)
update_main_content_via_turbo_stream(
groups: [group] + active_groups_for_form,
editing_group_key: TEMPORARY_GROUP_KEY,
form_model:
)
end
def group_form_model(group:, name: group[:name], validation_message: nil)
WorkPackageTypes::FormConfiguration::GroupFormModel.from_group(group, name:, validation_message:)
end
end
end
@@ -30,36 +30,101 @@
module WorkPackageTypes
class FormConfigurationTabController < BaseTabController
include PaginationHelper
include TypesHelper
include OpTurbo::ComponentStream
include WorkPackageTypes::FormConfigurationComponentStreams
layout "admin"
current_menu_item [:edit, :update] do
current_menu_item [:edit, :update, :reset_dialog, :move, :drop, :destroy] do
:types
end
def edit; end
def reset_dialog
respond_with_dialog(
WorkPackageTypes::FormConfiguration::ResetDialogComponent.new(type: @type)
)
end
def update
result = WorkPackageTypes::UpdateService
.new(user: current_user, model: @type, contract_class: UpdateFormConfigurationContract)
.call(permitted_type_params)
if result.success?
redirect_to edit_type_form_configuration_path(@type), notice: t(:notice_successful_update)
respond_to_update_success
else
flash.now[:error] = result.errors[:attribute_groups].to_sentence
render :edit, status: :unprocessable_entity
respond_to_update_failure(result)
end
end
def move
call = ::WorkPackageTypes::FormConfigurationRows::UpdateService
.new(user: current_user, type: @type, row_key: row_key_param)
.call(move_to: params[:move_to])
handle_row_update_response(call)
end
def drop
call = ::WorkPackageTypes::FormConfigurationRows::UpdateService
.new(user: current_user, type: @type, row_key: row_key_param)
.call(target_id: params[:target_id], position: params[:position])
handle_row_update_response(call)
end
def destroy
call = ::WorkPackageTypes::FormConfigurationRows::DeleteService
.new(user: current_user, type: @type, row_key: row_key_param)
.call
handle_row_update_response(call)
end
private
def respond_to_update_success
respond_to do |format|
format.html { redirect_to edit_type_form_configuration_path(@type), notice: t(:notice_successful_update) }
format.turbo_stream do
update_form_configuration_via_turbo_stream
respond_with_turbo_streams
end
end
end
def respond_to_update_failure(result)
respond_to do |format|
format.html do
flash.now[:error] = result.errors[:attribute_groups].to_sentence
render :edit, status: :unprocessable_entity
end
format.turbo_stream { head :unprocessable_entity }
end
end
def handle_row_update_response(call)
if call.success?
update_form_configuration_via_turbo_stream
else
render_form_configuration_error(call)
end
respond_with_turbo_streams(status: call.success? ? :ok : :unprocessable_entity)
end
def find_type
@type = ::Type.includes(:projects, :custom_fields).find(params[:type_id])
show_error_not_found unless @type
end
def row_key_param
params[:row_key] || params[:id]
end
def permitted_type_params
# having to call #to_unsafe_h as a query hash the attribute_groups
# parameters would otherwise still be an ActiveSupport::Parameter
+1 -1
View File
@@ -286,7 +286,7 @@ class WorkPackagesController < ApplicationController
end
def login_back_url_params
params.permit(:query_id, :state, :query_props)
params.permit(:query_id, :state, :query_props, :type, :parent_id)
end
def redirect_to_complete_route
@@ -0,0 +1,83 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module WorkPackageTypes
module FormConfiguration
class GroupForm < ApplicationForm
form do |group_form|
group_form.hidden(name: :group_type, value: model.group_type)
group_form.hidden(name: :query, value: model.query) if model.query.present?
group_form.group(layout: :horizontal) do |row|
row.text_field(
name: :name,
label: I18n.t("types.edit.form_configuration.group_name_label"),
visually_hide_label: true,
value: model.name,
required: true,
autofocus: true,
autocomplete: "off",
validation_message: validation_message_for(:name),
data: { "test-selector": "type-form-configuration-group-name-input" }
)
row.button(
name: :cancel,
tag: :a,
label: I18n.t("button_cancel"),
scheme: :secondary,
href: @cancel_path,
data: {
turbo_method: :post,
turbo_stream: true
},
test_selector: "type-form-configuration-group-cancel"
)
row.submit(
name: :submit,
label: I18n.t("button_save"),
scheme: :primary,
test_selector: "type-form-configuration-group-save"
)
end
end
def initialize(cancel_path:)
super()
@cancel_path = cancel_path
end
private
def validation_message_for(attribute)
model.errors.messages_for(attribute).to_sentence.presence
end
end
end
end
+2 -3
View File
@@ -136,13 +136,12 @@ module ::TypesHelper
def get_active_groups(type, available, inactive)
type.attribute_groups.map do |group|
{
key: group.key,
type: group.group_type,
name: group.translated_key,
attributes: active_group_attributes_map(group, available, inactive),
query: query_to_query_props(group)
}.tap do |group_obj|
group_obj[:key] = group.key if group.internal_key?
end
}
end
end
+4 -4
View File
@@ -35,9 +35,9 @@ module WorkPackagesHelper
# Displays a link to +work_package+ with its subject.
# Examples:
#
# link_to_work_package(package) # => Defect #6: This is the subject
# link_to_work_package(package, link_subject: true) # => Defect #6: This is the subject (everything within the link)
# link_to_work_package(package, display_project: true) # => Foo - Defect #6: This is the subject
# link_to_work_package(package) # => Defect <id>: This is the subject
# link_to_work_package(package, link_subject: true) # => Defect <id>: This is the subject (everything within the link)
# link_to_work_package(package, display_project: true) # => Foo - Defect <id>: This is the subject
def link_to_work_package(work_package, display_project: false, link_subject: false) # rubocop:disable Metrics/AbcSize
output = ActiveSupport::SafeBuffer.new
output << "#{work_package.project} - " if display_project && work_package.project_id
@@ -47,7 +47,7 @@ module WorkPackagesHelper
class: link_to_work_package_css_classes(work_package)) do
link_parts = []
link_parts << work_package.type.to_s if work_package.type_id
link_parts << "##{work_package.id}:"
link_parts << "#{work_package.formatted_id}:"
link_parts << content_tag(:span, I18n.t(:label_closed_work_packages), class: "sr-only") if work_package.closed?
link_parts << work_package.subject if link_subject
+3 -3
View File
@@ -428,9 +428,9 @@ module Exports::PDF::Common::Common
"<color rgb='#{styles.link_color}'>#{make_link_href(href, caption)}</color>"
end
def get_id_column_cell(work_package, value)
def get_id_column_cell(work_package)
href = url_helpers.work_package_url(work_package)
make_link_href_cell(href, value)
make_link_href_cell(href, work_package.display_id)
end
def get_subject_column_cell(work_package, value)
@@ -469,7 +469,7 @@ module Exports::PDF::Common::Common
def get_value_cell_by_column(work_package, column_name, format_subject)
value = get_column_value(work_package, column_name)
return get_cf_link_cell(value) if value.is_a?(::Exports::Formatters::LinkFormatter)
return get_id_column_cell(work_package, value) if column_name == :id
return get_id_column_cell(work_package) if column_name == :id
return get_subject_column_cell(work_package, value) if format_subject && column_name == :subject
escape_tags(value)
+3 -33
View File
@@ -35,6 +35,7 @@ module Exports::PDF::Common::Markdown
include MarkdownToPDF::Core
include MarkdownToPDF::Parser
include Exports::PDF::Common::Common
include Exports::PDF::Common::WorkPackageMentions
def initialize(styling_yml, pdf, hyphenation_language)
@styles = MarkdownToPDF::Styles.new(styling_yml)
@@ -94,45 +95,14 @@ module Exports::PDF::Common::Markdown
wp_mention_macro(tag.attr("data-text") || "", tag.attr("data-id") || "", opts)
end
def expand_wp_mention(work_package, content)
detail_level = content.count("#")
return content if detail_level == 1
# ##1234: {Type} #{ID}: {Subject}
content = "#{work_package.type} ##{work_package.id}: #{work_package.subject}"
return content if detail_level == 2
# ###1234: {Status} {Type} #{ID}: {Subject} ({Start Date} - {End Date})
"#{work_package.status.name} #{content}#{work_package_dates(work_package)}"
end
def wp_mention_macro(content, id, opts)
id = id[/\d+/]
return [text_hash(content, opts)] if id.blank?
work_package = WorkPackage.find_by(id: id)
work_package = WorkPackage.find_by_display_id(id)
return [text_hash(content, opts)] unless work_package&.visible?
content = expand_wp_mention(work_package, content)
[text_hash(content, opts.merge({ link: url_helpers.work_package_url(id) }))]
end
def work_package_dates(work_package)
return "" if work_package.start_date.blank? && work_package.due_date.blank?
if work_package.due_date.present? && work_package.start_date == work_package.due_date
return " (#{format_date(work_package.due_date)})"
end
work_package_date_range(work_package)
end
def work_package_date_range(work_package)
content = [
work_package.start_date.present? ? format_date(work_package.start_date) : I18n.t("label_no_start_date"),
work_package.due_date.present? ? format_date(work_package.due_date) : I18n.t("label_no_due_date")
].join(" - ")
" (#{content})"
[text_hash(content, opts.merge({ link: url_helpers.work_package_url(work_package) }))]
end
def handle_mention_html_tag(tag, node, opts)
@@ -0,0 +1,63 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Exports::PDF::Common::WorkPackageMentions
include Redmine::I18n
def expand_wp_mention(work_package, content)
detail_level = content.count("#")
return work_package.formatted_id if detail_level == 1
# ##: {Type} {formatted_id}: {Subject}
content = "#{work_package.type} #{work_package.formatted_id}: #{work_package.subject}"
return content if detail_level == 2
# ###: {Status} {Type} {formatted_id}: {Subject} ({Start Date} - {End Date})
"#{work_package.status.name} #{content}#{work_package_dates(work_package)}"
end
def work_package_dates(work_package)
return "" if work_package.start_date.blank? && work_package.due_date.blank?
if work_package.due_date.present? && work_package.start_date == work_package.due_date
return " (#{format_date(work_package.due_date)})"
end
work_package_date_range(work_package)
end
def work_package_date_range(work_package)
content = [
work_package.start_date.present? ? format_date(work_package.start_date) : I18n.t("label_no_start_date"),
work_package.due_date.present? ? format_date(work_package.due_date) : I18n.t("label_no_due_date")
].join(" - ")
" (#{content})"
end
end
@@ -742,7 +742,7 @@ module Exports::PDF::Components::Gantt
# @param [WorkPackage] work_package
# @return [String]
def work_package_info_line(work_package)
"#{work_package.type} ##{work_package.id}#{work_package.status}#{work_package_info_line_date work_package}"
"#{work_package.type} #{work_package.formatted_id}#{work_package.status}#{work_package_info_line_date work_package}"
end
def work_package_info_line_date(work_package)
+8 -6
View File
@@ -59,6 +59,7 @@ class Journal < ApplicationRecord
register_journal_formatter OpenProject::JournalFormatter::MeetingStartTime
register_journal_formatter OpenProject::JournalFormatter::MeetingState
register_journal_formatter OpenProject::JournalFormatter::MeetingWorkPackageId
register_journal_formatter OpenProject::JournalFormatter::ParticipantChange
register_journal_formatter OpenProject::JournalFormatter::ProjectPhaseActive
register_journal_formatter OpenProject::JournalFormatter::ProjectPhaseDates
register_journal_formatter OpenProject::JournalFormatter::ProjectPhaseDefinition
@@ -110,6 +111,7 @@ class Journal < ApplicationRecord
belongs_to :data, polymorphic: true, dependent: :destroy
has_many :agenda_item_journals, class_name: "Journal::MeetingAgendaItemJournal", dependent: :delete_all
has_many :participant_journals, class_name: "Journal::MeetingParticipantJournal", dependent: :delete_all
has_many :attachable_journals, class_name: "Journal::AttachableJournal", dependent: :delete_all
has_many :customizable_journals, class_name: "Journal::CustomizableJournal", dependent: :delete_all
has_many :custom_comment_journals, class_name: "Journal::CustomCommentJournal", dependent: :delete_all
@@ -223,12 +225,6 @@ class Journal < ApplicationRecord
end
end
private
def has_file_links?
journable.respond_to?(:file_links)
end
def predecessor
return @predecessor if defined?(@predecessor)
@@ -242,4 +238,10 @@ class Journal < ApplicationRecord
.first
end
end
private
def has_file_links?
journable.respond_to?(:file_links)
end
end
+1 -1
View File
@@ -235,7 +235,7 @@ class PermittedParams
whitelisted = type_params.permit(*permitted)
if type_params[:attribute_groups]
whitelisted[:attribute_groups] = JSON.parse(type_params[:attribute_groups])
whitelisted[:attribute_groups] = type_params[:attribute_groups]
end
whitelisted
+19 -35
View File
@@ -36,6 +36,10 @@ module Projects::Identifier
# Classic identifier format: lowercase letters, digits, hyphens, underscores — but not all-numeric.
CLASSIC_IDENTIFIER_FORMAT = /\A(?!\d+\z)[a-z0-9\-_]+\z/
# Unanchored shape of a semantic project identifier ("PROJ", "MY_PROJECT_1").
# Composed into `WorkPackage::SemanticIdentifier::SEMANTIC_ID_PATTERN`.
SEMANTIC_FORMAT = /[A-Z][A-Z0-9_]*/
RESERVED_IDENTIFIERS = %w[new menu queries filters identifier_update_dialog identifier_suggestion].freeze
included do
@@ -43,31 +47,18 @@ module Projects::Identifier
normalizes :identifier, with: OpenProject::RemoveInvisibleCharacters
# Generators
# There are two supported formats:
# 1. slug identifiers (e.g. "project_one"), generated by acts_as_url
# * work package ID = global ID (e.g. "#123")
# 2. semantic identifiers (e.g. "PROJ1"), generated by the :generate_semantic_identifier hook
# * work package ID = {project identifier + dash + project-local sequence number ID} (e.g. "PROJ1-123")
acts_as_url :name,
url_attribute: :identifier,
sync_url: false, # Don't update identifier when name changes
only_when_blank: true, # Only generate when identifier not set
limit: CLASSIC_IDENTIFIER_MAX_LENGTH,
blacklist: RESERVED_IDENTIFIERS,
adapter: OpenProject::ActsAsUrl::Adapter::OpActiveRecord, # use a custom adapter able to handle edge cases
skip_if: -> { Setting::WorkPackageIdentifier.semantic? }
# Generate semantic identifier (when in the semantic mode)
before_validation :generate_semantic_identifier,
on: :create,
if: -> { Setting::WorkPackageIdentifier.semantic? && identifier.blank? }
# Generate identifier from name unless specified
before_validation on: :create, if: -> { identifier.blank? && name.present? } do
self.identifier = suggest_identifier
end
# Basic mode-agnostic identifier validation
validates :identifier,
presence: true,
uniqueness: { case_sensitive: false },
if: ->(p) { p.persisted? || p.identifier.present? }
# Extended validation that runs checks specific to each format (classic & semantic)
validates :identifier, "projects/identifier" => true, if: :identifier_changed?
friendly_id :identifier, use: %i[finders history], slug_column: :identifier
@@ -75,7 +66,7 @@ module Projects::Identifier
# FriendlyId::Slugged adds after_validation :unset_slug_if_invalid, which reverts the
# slug column to its previous value when validation fails. With slug_column: :identifier,
# this would reset a manually-set identifier back to nil on new records. Since the
# identifier is managed by acts_as_url and user input (not FriendlyId's slug generator),
# identifier is managed by our own callbacks and user input (not FriendlyId's slug generator),
# we disable this behaviour entirely.
# Must be inside `included` to override FriendlyId::Slugged in the MRO.
def unset_slug_if_invalid; end
@@ -121,6 +112,11 @@ module Projects::Identifier
FriendlyId::Slug.where(sluggable_type: name).extending(IdentifierSlugScopes)
end
# There are two supported formats:
# 1. slug identifiers (e.g. "project_one"), generated by ClassicIdentifierSuggestionGenerator
# * work package ID = global ID (e.g. "#123")
# 2. semantic identifiers (e.g. "PROJ1"), generated by ProjectIdentifierSuggestionGenerator
# * work package ID = {project identifier + dash + project-local sequence number ID} (e.g. "PROJ1-123")
def suggest_identifier(name, mode: Setting[:work_packages_identifier])
if mode == Setting::WorkPackageIdentifier::SEMANTIC
exclude = ProjectIdentifiers::IdentifierAutofix::ProblematicIdentifiers.reserved_identifiers
@@ -137,14 +133,9 @@ module Projects::Identifier
end
# Override the `validation_context` getter to include the `default_validation_context` when the
# context is `:saving_custom_fields`. This is required, because the `acts_as_url` plugin from
# `stringex` defines a callback on the `:create` context for initialising the `identifier` field.
# Providing a custom context while creating the project, will not execute the callbacks on the
# `:create` or `:update` contexts, meaning the identifier will not get initialised.
# In order to initialise the identifier, the `default_validation_context` (`:create`, or `:update`)
# should be included when validating via the `:saving_custom_fields`. This way every create
# or update callback will also be executed alongside the `:saving_custom_fields` callbacks.
# This problem does not affect the contextless callbacks, they are always executed.
# context is `:saving_custom_fields`. Our identifier-generation callbacks fire on `:create`, so
# providing only a custom context would skip them, leaving identifier blank on new records.
# Including the default context ensures `:create` callbacks run alongside `:saving_custom_fields`.
def validation_context
case Array(super)
in [*, :saving_custom_fields, *] => context
@@ -154,11 +145,4 @@ module Projects::Identifier
end
end
private
def generate_semantic_identifier
return if name.blank?
self.identifier = self.class.suggest_identifier(name)
end
end
@@ -32,6 +32,6 @@ class Queries::Users::Orders::DefaultOrder < Queries::Orders::Base
self.model = User
def self.key
/\A(id|lastname|firstname|mail|login)\z/
/\A(id|lastname|firstname|mail|login|admin|created_at|last_login_on)\z/
end
end
@@ -0,0 +1,45 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
class Queries::Users::Selects::Default < Queries::Selects::Base
KEYS = %i[login firstname lastname mail admin created_at last_login_on].freeze
def self.key
/\A(#{Regexp.union(KEYS.map(&:to_s))})\z/
end
def self.all_available
KEYS.map { new(it) }
end
def caption
User.human_attribute_name(attribute)
end
end
+2 -1
View File
@@ -47,7 +47,8 @@ class Type::AttributeGroup < Type::FormGroup
other.is_a?(self.class) &&
key == other.key &&
type == other.type &&
attributes == other.attributes
attributes == other.attributes &&
display_name == other.display_name
end
def active_members(project)
+12 -9
View File
@@ -190,14 +190,15 @@ module Type::AttributeGroups
attributes = group[1]
first_attribute = attributes[0]
key = group[0]
display_name = group[2] if group.length > 2
if first_attribute.is_a?(Query)
new_query_group(key, first_attribute)
new_query_group(key, first_attribute, display_name:)
elsif first_attribute.is_a?(Symbol) && Type::QueryGroup.query_attribute?(first_attribute)
query = Query.find_by(id: Type::QueryGroup.query_attribute_id(first_attribute))
new_query_group(key, query)
new_query_group(key, query, display_name:)
else
new_attribute_group(key, attributes)
new_attribute_group(key, attributes, display_name:)
end
end
end
@@ -213,16 +214,18 @@ module Type::AttributeGroups
else
group.attributes
end
[group.key, attributes]
result = [group.key, attributes]
result << group.display_name if group.display_name.present?
result
end
end
def new_attribute_group(key, attributes)
Type::AttributeGroup.new(self, key, attributes)
def new_attribute_group(key, attributes, display_name: nil)
Type::AttributeGroup.new(self, key, attributes, display_name:)
end
def new_query_group(key, query)
Type::QueryGroup.new(self, key, query)
def new_query_group(key, query, display_name: nil)
Type::QueryGroup.new(self, key, query, display_name:)
end
def cleanup_query_groups_queries
@@ -231,7 +234,7 @@ module Type::AttributeGroups
new_groups = self[:attribute_groups]
old_groups = attribute_groups_was
ids = (old_groups.map(&:last).flatten - new_groups.map(&:last).flatten)
ids = (old_groups.map { |g| g[1] }.flatten - new_groups.map { |g| g[1] }.flatten)
.filter_map { |k| ::Type::QueryGroup.query_attribute_id(k) }
Query.where(id: ids).destroy_all

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