mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Merge remote-tracking branch 'origin/dev' into feature/73089-restyled-work-package-card-in-backlogs-and-sprints-view
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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/'
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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
@@ -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,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"
|
||||
|
||||
+8
-8
@@ -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
|
||||
+28
-23
@@ -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
|
||||
+6
-10
@@ -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
|
||||
+39
-17
@@ -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 %>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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) %>
|
||||
|
||||
<%= 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) %>
|
||||
|
||||
<%= 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
%>
|
||||
+3
-8
@@ -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
|
||||
+74
@@ -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
|
||||
+49
@@ -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
|
||||
%>
|
||||
+13
-15
@@ -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
|
||||
+43
@@ -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
|
||||
%>
|
||||
+67
@@ -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
|
||||
+42
@@ -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
|
||||
%>
|
||||
+43
@@ -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
|
||||
+24
-6
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user