mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge branch 'dev' into document_keycloak_with_openproject
This commit is contained in:
@@ -33,3 +33,5 @@ frontend/node_modules
|
||||
node_modules
|
||||
# travis
|
||||
vendor/bundle
|
||||
/public/assets
|
||||
/config/frontend_assets.manifest.json
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
48a4f1b6adb1e847a90a61f2ab277f28bcd77608
|
||||
# Update copyright information for 2023
|
||||
21a696ef9b170e14ad2daf53364a4c2113822c2f
|
||||
# Update copyright information for 2023
|
||||
# Update copyright information for 2024
|
||||
c795874f7f281297bbd3bad2fdb58b24cb4ce624
|
||||
# switch to double quotes
|
||||
f3c99ee5dded81ad55f2b6f3706216d5fa765677
|
||||
5c72ea0046a6b5230bf456f55a296ed6fd579535
|
||||
9e4934cd0a468f46d8f0fc0f11ebc2d4216f789c
|
||||
6678cab48d443b5782fa93b171d62093819ee4fc
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- id: find_latest_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.OPENPROJECT_CI_GH_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \
|
||||
@@ -79,19 +79,32 @@ jobs:
|
||||
echo "Successfully merged $RELEASE_BRANCH into $BASE_BRANCH and pushed"
|
||||
else
|
||||
# Close all previous PRs with label
|
||||
for pr_number in $(gh pr list --label create-merge-release-into-dev-pr --json number --jq='.[].number'); do
|
||||
gh pr close "$pr_number"
|
||||
pr_numbers=$(gh pr list --label create-merge-release-into-dev-pr --json number --jq='.[].number')
|
||||
for pr_number in $pr_numbers; do
|
||||
gh pr close "$pr_number" --delete-branch
|
||||
done
|
||||
|
||||
pr_body=$(
|
||||
echo 'Created by GitHub action'
|
||||
for pr_number in $pr_numbers; do
|
||||
echo "Replaces #$pr_number"
|
||||
done
|
||||
)
|
||||
|
||||
TEMP_BRANCH="merge-$RELEASE_BRANCH-$(date "+%Y%m%d%H%M%S")"
|
||||
|
||||
git branch "$TEMP_BRANCH" "$RELEASE_BRANCH"
|
||||
|
||||
git push origin "$TEMP_BRANCH"
|
||||
|
||||
gh pr create \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$RELEASE_BRANCH" \
|
||||
--head "$TEMP_BRANCH" \
|
||||
--title "Merge $RELEASE_BRANCH into $BASE_BRANCH" \
|
||||
--body 'Created by GitHub action' \
|
||||
--label create-merge-release-into-dev-pr \
|
||||
--reviewer opf/backend
|
||||
echo "Created a PR to merge $RELEASE_BRANCH into $BASE_BRANCH"
|
||||
--body "$pr_body" \
|
||||
--label create-merge-release-into-dev-pr
|
||||
echo "Created a PR to merge $RELEASE_BRANCH ($TEMP_BRANCH) into $BASE_BRANCH"
|
||||
fi
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.OPENPROJECT_CI_GH_TOKEN }}
|
||||
|
||||
@@ -82,16 +82,18 @@ jobs:
|
||||
run: |
|
||||
script/i18n/fix_crowdin_pt_language_root_key
|
||||
- name: "Commit translations"
|
||||
env:
|
||||
BRANCH: ${{ matrix.branch }}
|
||||
run: |
|
||||
git config user.name "OpenProject Actions CI"
|
||||
git config user.email "operations+ci@openproject.com"
|
||||
echo "Updating combined translations"
|
||||
git ls-files -m -o | grep 'crowdin\/.*\.yml$' | xargs git add
|
||||
git diff --staged --name-only
|
||||
git diff --staged --exit-code --quiet || (
|
||||
git commit -m "update locales from crowdin [ci skip]" &&
|
||||
echo "Run git pull --rebase" &&
|
||||
git pull --rebase &&
|
||||
echo "Run git push origin $BRANCH" &&
|
||||
git push origin $BRANCH
|
||||
)
|
||||
if ! git diff --staged --exit-code --quiet; then
|
||||
git commit -m "update locales from crowdin [ci skip]"
|
||||
echo "Run git pull --rebase origin $BRANCH"
|
||||
git pull --rebase origin "$BRANCH"
|
||||
echo "Run git push origin $BRANCH"
|
||||
git push origin "$BRANCH"
|
||||
fi
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.2.3'
|
||||
ruby-version: '3.3.1'
|
||||
- uses: MeilCli/danger-action@v5
|
||||
with:
|
||||
danger_file: 'Dangerfile'
|
||||
|
||||
+102
-45
@@ -3,7 +3,7 @@ name: Docker
|
||||
on:
|
||||
# build dev daily
|
||||
schedule:
|
||||
- cron: '20 2 * * *' # Daily at 02:20
|
||||
- cron: "20 2 * * *" # Daily at 02:20
|
||||
|
||||
push:
|
||||
tags:
|
||||
@@ -18,10 +18,10 @@ permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
env:
|
||||
REGISTRY_IMAGE: openproject/community
|
||||
REGISTRY_IMAGE: openproject/openproject
|
||||
|
||||
jobs:
|
||||
extract_version:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract version
|
||||
@@ -36,6 +36,11 @@ jobs:
|
||||
elif [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
|
||||
TAG_REF=${{ inputs.tag }}
|
||||
CHECKOUT_REF=${{ inputs.tag }}
|
||||
|
||||
if [ -z "$TAG_REF" ]; then
|
||||
TAG_REF=dev
|
||||
CHECKOUT_REF=dev
|
||||
fi
|
||||
else
|
||||
echo "Unsupported event"
|
||||
exit 1
|
||||
@@ -50,70 +55,110 @@ jobs:
|
||||
echo "Version: $VERSION"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "checkout_ref=$CHECKOUT_REF" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/checkout@v4
|
||||
- name: Cache NPM
|
||||
uses: runs-on/cache@v4
|
||||
with:
|
||||
path: |
|
||||
frontend/node_modules
|
||||
node_modules
|
||||
key: nodejs-x64-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: nodejs-x64-
|
||||
- name: Cache angular
|
||||
uses: runs-on/cache@v4
|
||||
with:
|
||||
path: frontend/.angular
|
||||
key: angular-${{ github.ref }}
|
||||
restore-keys: angular-
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
frontend/package-lock.json
|
||||
- name: Precompile assets
|
||||
run: |
|
||||
./docker/prod/setup/precompile-assets.sh
|
||||
# public/assets will be saved as artifact, so temporarily copying config file there as well
|
||||
cp config/frontend_assets.manifest.json public/assets/frontend_assets.manifest.json
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: public/
|
||||
name: public-assets-${{ github.sha }}
|
||||
outputs:
|
||||
version: ${{ steps.extract_version.outputs.version }}
|
||||
checkout_ref: ${{ steps.extract_version.outputs.checkout_ref }}
|
||||
build:
|
||||
needs:
|
||||
- extract_version
|
||||
- setup
|
||||
if: github.repository == 'opf/openproject'
|
||||
runs-on: runs-on,runner=8cpu-linux,family=m7i+m7a,run-id=${{ github.run_id }}
|
||||
runs-on:
|
||||
labels:
|
||||
- runs-on
|
||||
- ssh=false
|
||||
- run-id=${{ github.run_id }}
|
||||
- ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
bim_support: true
|
||||
target: slim
|
||||
runner: runner=4cpu-linux-x64
|
||||
- platform: linux/arm64/v8
|
||||
bim_support: false
|
||||
target: slim
|
||||
runner: runner=4cpu-linux-arm64
|
||||
- platform: linux/amd64
|
||||
bim_support: true
|
||||
target: all-in-one
|
||||
runner: runner=4cpu-linux-x64
|
||||
- platform: linux/arm64/v8
|
||||
bim_support: false
|
||||
target: all-in-one
|
||||
runner: runner=4cpu-linux-arm64
|
||||
- platform: linux/ppc64le
|
||||
bim_support: false
|
||||
target: all-in-one
|
||||
- platform: linux/arm64/v8
|
||||
bim_support: false
|
||||
target: all-in-one
|
||||
runner: runner=4cpu-linux-x64
|
||||
steps:
|
||||
- name: Extract version
|
||||
id: extract_version
|
||||
run: |
|
||||
if [[ ${{ github.event_name }} == 'push' ]]; then
|
||||
TAG_REF=${GITHUB_REF#refs/tags/}
|
||||
CHECKOUT_REF=$GITHUB_REF
|
||||
elif [[ ${{ github.event_name }} == 'schedule' ]]; then
|
||||
TAG_REF=dev
|
||||
CHECKOUT_REF=refs/heads/dev
|
||||
elif [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
|
||||
TAG_REF=${{ inputs.tag }}
|
||||
CHECKOUT_REF=${{ inputs.tag }}
|
||||
else
|
||||
echo "Unsupported event"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$TAG_REF" ] || [ -z "$CHECKOUT_REF" ]; then
|
||||
echo "No TAG_REF or CHECKOUT_REF set. Aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=${TAG_REF#v}
|
||||
echo "Version: $VERSION"
|
||||
echo "::set-output name=version::$VERSION"
|
||||
echo "::set-output name=checkout_ref::$CHECKOUT_REF"
|
||||
- name: Checkout
|
||||
with:
|
||||
ref: ${{ steps.extract_version.outputs.checkout_ref }}
|
||||
ref: ${{ needs.setup.outputs.checkout_ref }}
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare docker files
|
||||
run: |
|
||||
cp ./docker/prod/Dockerfile ./Dockerfile
|
||||
- name: Download precompiled public assets
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: public-assets-${{ github.sha }}
|
||||
path: public/
|
||||
- name: Setup precompiled assets
|
||||
run: |
|
||||
ls -al public/
|
||||
mv public/assets/frontend_assets.manifest.json config/frontend_assets.manifest.json
|
||||
ls -al config/frontend_assets.manifest.json
|
||||
# allow precompiled assets to be injected into docker image
|
||||
echo '' >> .dockerignore
|
||||
echo '!/public/assets' >> .dockerignore
|
||||
echo '!/config/frontend_assets.manifest.json' >> .dockerignore
|
||||
- name: Add build information
|
||||
run: |
|
||||
echo "${{ needs.setup.outputs.checkout_ref }}" > PRODUCT_VERSION
|
||||
echo "https://github.com/opf/openproject/commits/${{ needs.setup.outputs.checkout_ref }}" > PRODUCT_URL
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" > RELEASE_DATE
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -127,7 +172,7 @@ jobs:
|
||||
org.opencontainers.image.documentation=https://www.openproject.org/docs/
|
||||
org.opencontainers.image.vendor=OpenProject GmbH
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.extract_version.outputs.version }}
|
||||
type=semver,pattern={{version}},value=${{ needs.setup.outputs.version }}
|
||||
images: |
|
||||
${{ env.REGISTRY_IMAGE }}
|
||||
- name: Build image
|
||||
@@ -143,6 +188,8 @@ jobs:
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
|
||||
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
|
||||
- name: Test
|
||||
# We only test the native container. If that fails the builds for the others
|
||||
# will be cancelled as well.
|
||||
@@ -171,6 +218,8 @@ jobs:
|
||||
BIM_SUPPORT=${{ matrix.bim_support }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
|
||||
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
@@ -189,7 +238,7 @@ jobs:
|
||||
matrix:
|
||||
target: [slim, all-in-one]
|
||||
needs:
|
||||
- extract_version
|
||||
- setup
|
||||
- build
|
||||
steps:
|
||||
- name: Download digests
|
||||
@@ -204,7 +253,7 @@ jobs:
|
||||
if [ "$suffix" = "-all-in-one" ]; then suffix="" ; fi
|
||||
echo "suffix=$suffix" >> "$GITHUB_OUTPUT"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
@@ -218,12 +267,12 @@ jobs:
|
||||
latest=false
|
||||
suffix=${{ steps.set_suffix.outputs.suffix }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ needs.extract_version.outputs.version }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.extract_version.outputs.version }}
|
||||
type=semver,pattern={{major}},value=${{ needs.extract_version.outputs.version }}
|
||||
type=semver,pattern={{version}},value=${{ needs.setup.outputs.version }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.setup.outputs.version }}
|
||||
type=semver,pattern={{major}},value=${{ needs.setup.outputs.version }}
|
||||
type=raw,value=dev,priority=200,enable={{is_default_branch}}
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -235,3 +284,11 @@ jobs:
|
||||
- name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
notify:
|
||||
needs: [setup, build, merge]
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') }}
|
||||
uses: ./.github/workflows/email-notification.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
subject: "Docker build failed"
|
||||
body: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: email-notification
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
subject:
|
||||
type: string
|
||||
required: true
|
||||
body:
|
||||
type: string
|
||||
required: true
|
||||
from:
|
||||
type: string
|
||||
default: Github CI <github-ci@openproject.com>
|
||||
to:
|
||||
type: string
|
||||
default: operations@openproject.com
|
||||
secrets:
|
||||
OPS_MAIL_SMTP_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
name: Notify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send mail
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
subject: ${{ inputs.subject }}
|
||||
body: ${{ inputs.body }}
|
||||
from: ${{ inputs.from }}
|
||||
to: ${{ inputs.to }}
|
||||
secure: false
|
||||
server_port: 587
|
||||
server_address: smtp.postmarkapp.com
|
||||
username: PM-T-outbound-bw6Xf6uoKnU76g1RcuydgZ
|
||||
password: ${{secrets.OPS_MAIL_SMTP_TOKEN }}
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
pull-requests: write # to remove labels
|
||||
statuses: write # to create commit status
|
||||
|
||||
if: github.repository == 'opf/openproject' && ( github.event_name == 'schedule' || github.event_name == 'push' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') )
|
||||
if: github.repository == 'opf/openproject' && ( github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') )
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
|
||||
@@ -2,11 +2,6 @@ name: rubocop
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- release/*
|
||||
paths:
|
||||
- '**.rb'
|
||||
|
||||
jobs:
|
||||
rubocop:
|
||||
@@ -14,12 +9,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Fetch all commits for PR branch plus head commit of base branch
|
||||
run: |
|
||||
# fetch all commits of the PR branch
|
||||
git fetch --shallow-exclude "${{ github.base_ref }}" origin "${{ github.ref }}"
|
||||
# fix for "fatal: error in object: unshallow"
|
||||
git repack -d
|
||||
# fetch head commit of base branch
|
||||
git fetch --deepen 1 origin "${{ github.ref }}"
|
||||
- uses: ruby/setup-ruby@v1
|
||||
- uses: opf/action-rubocop@v2
|
||||
- uses: opf/action-rubocop@master
|
||||
with:
|
||||
github_token: ${{ secrets.github_token }}
|
||||
rubocop_version: gemfile
|
||||
rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile
|
||||
rubocop_extensions: rubocop-inflector:gemfile rubocop-performance:gemfile rubocop-rails:gemfile rubocop-rspec:gemfile
|
||||
reporter: github-pr-check
|
||||
only_changed: true
|
||||
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
uses: runs-on/cache@v4
|
||||
with:
|
||||
path: cache/bundle
|
||||
key: gem-${{ hashFiles('Gemfile.lock') }}
|
||||
key: gem-bookworm-${{ hashFiles('.ruby-version') }}-${{ hashFiles('Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
gem-
|
||||
gem-bookworm-${{ hashFiles('.ruby-version') }}-
|
||||
- name: Cache NPM
|
||||
uses: runs-on/cache@v4
|
||||
with:
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
require:
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- rubocop-rspec_rails
|
||||
- ./lib_static/rubocop/cop/open_project/add_preview_for_view_component.rb
|
||||
- ./lib_static/rubocop/cop/open_project/no_do_end_block_with_rspec_capybara_matcher_in_expect.rb
|
||||
- ./lib_static/rubocop/cop/open_project/use_service_result_factory_methods.rb
|
||||
@@ -21,7 +22,7 @@ inherit_mode:
|
||||
- Exclude
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 3.2
|
||||
TargetRubyVersion: 3.3
|
||||
# Enable any new cops in new versions by default
|
||||
NewCops: enable
|
||||
Exclude:
|
||||
@@ -257,8 +258,10 @@ RSpec/ContextWording:
|
||||
- as
|
||||
- even
|
||||
- for
|
||||
- given
|
||||
- having
|
||||
- if
|
||||
- in
|
||||
- 'on'
|
||||
- to
|
||||
- unless
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.2.3
|
||||
3.3.1
|
||||
|
||||
@@ -36,7 +36,7 @@ ruby File.read(".ruby-version").strip
|
||||
|
||||
gem "actionpack-xml_parser", "~> 2.0.0"
|
||||
gem "activemodel-serializers-xml", "~> 1.0.1"
|
||||
gem "activerecord-import", "~> 1.6.0"
|
||||
gem "activerecord-import", "~> 1.7.0"
|
||||
gem "activerecord-session_store", "~> 2.1.0"
|
||||
gem "ox"
|
||||
gem "rails", "~> 7.1.3"
|
||||
@@ -46,11 +46,11 @@ gem "ffi", "~> 1.15"
|
||||
|
||||
gem "rdoc", ">= 2.4.2"
|
||||
|
||||
gem "doorkeeper", "~> 5.6.6"
|
||||
gem "doorkeeper", "~> 5.7.0"
|
||||
# Maintain our own omniauth due to relative URL root issues
|
||||
# see upstream PR: https://github.com/omniauth/omniauth/pull/903
|
||||
gem "omniauth", git: "https://github.com/opf/omniauth", ref: "fe862f986b2e846e291784d2caa3d90a658c67f0"
|
||||
gem "request_store", "~> 1.6.0"
|
||||
gem "request_store", "~> 1.7.0"
|
||||
|
||||
gem "warden", "~> 1.2"
|
||||
gem "warden-basic_auth", "~> 0.2.1"
|
||||
@@ -83,7 +83,7 @@ gem "htmldiff"
|
||||
gem "stringex", "~> 2.8.5"
|
||||
|
||||
# CommonMark markdown parser with GFM extension
|
||||
gem "commonmarker", "~> 1.0.3"
|
||||
gem "commonmarker", "~> 1.1.3"
|
||||
|
||||
# HTML pipeline for transformations on text formatter output
|
||||
# such as sanitization or additional features
|
||||
@@ -115,6 +115,8 @@ gem "ruby-duration", "~> 3.2.0"
|
||||
# released.
|
||||
gem "mail", "= 2.8.1"
|
||||
|
||||
gem "csv", "~> 3.3"
|
||||
|
||||
# provide compatible filesystem information for available storage
|
||||
gem "sys-filesystem", "~> 1.4.0", require: false
|
||||
|
||||
@@ -138,7 +140,7 @@ gem "rack-attack", "~> 6.7.0"
|
||||
gem "secure_headers", "~> 6.5.0"
|
||||
|
||||
# Browser detection for incompatibility checks
|
||||
gem "browser", "~> 5.3.0"
|
||||
gem "browser", "~> 6.0.0"
|
||||
|
||||
# Providing health checks
|
||||
gem "okcomputer", "~> 1.18.1"
|
||||
@@ -157,20 +159,21 @@ gem "airbrake", "~> 13.0.0", require: false
|
||||
|
||||
gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "8f14736a88ad0064d2a97be108fe7061ffbcee91"
|
||||
gem "prawn", "~> 2.4"
|
||||
gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved.
|
||||
# prawn implicitly depends on matrix gem no longer in ruby core with 3.1
|
||||
gem "matrix", "~> 0.4.2"
|
||||
|
||||
gem "meta-tags", "~> 2.20.0"
|
||||
gem "meta-tags", "~> 2.21.0"
|
||||
|
||||
gem "paper_trail", "~> 15.1.0"
|
||||
|
||||
gem "clamav-client", github: "honestica/clamav-client", ref: "29e78ae94307cb34e79ddd29c5da79752239d8b7"
|
||||
gem "op-clamav-client", "~> 3.4", require: "clamav"
|
||||
|
||||
group :production do
|
||||
# we use dalli as standard memcache client
|
||||
# requires memcached 1.4+
|
||||
gem "dalli", "~> 3.2.0"
|
||||
gem "redis", "~> 5.1.0"
|
||||
gem "redis", "~> 5.2.0"
|
||||
end
|
||||
|
||||
gem "i18n-js", "~> 4.2.3"
|
||||
@@ -181,11 +184,11 @@ gem "sprockets-rails", "~> 3.4.2"
|
||||
|
||||
gem "puma", "~> 6.4"
|
||||
gem "puma-plugin-statsd", "~> 2.0"
|
||||
gem "rack-timeout", "~> 0.6.3", require: "rack/timeout/base"
|
||||
gem "rack-timeout", "~> 0.7.0", require: "rack/timeout/base"
|
||||
|
||||
gem "nokogiri", "~> 1.16.0"
|
||||
|
||||
gem "carrierwave", "~> 1.3.1"
|
||||
gem "carrierwave", "~> 1.3.4"
|
||||
gem "carrierwave_direct", "~> 2.1.0"
|
||||
gem "fog-aws"
|
||||
|
||||
@@ -214,7 +217,7 @@ gem "appsignal", "~> 3.0", require: false
|
||||
|
||||
gem "view_component"
|
||||
# Lookbook
|
||||
gem "lookbook", "~> 2.2.1"
|
||||
gem "lookbook", "~> 2.3.0"
|
||||
|
||||
# Require factory_bot for usage with openproject plugins testing
|
||||
gem "factory_bot", "~> 6.4.0", require: false
|
||||
@@ -233,7 +236,7 @@ group :test do
|
||||
# Test prof provides factories from code
|
||||
# and other niceties
|
||||
gem "test-prof", "~> 1.3.0"
|
||||
gem "turbo_tests", github: "crohr/turbo_tests", ref: "fix/runtime-info"
|
||||
gem "turbo_tests", github: "opf/turbo_tests", ref: "with-patches"
|
||||
|
||||
gem "rack_session_access"
|
||||
gem "rspec", "~> 3.13.0"
|
||||
@@ -264,7 +267,7 @@ group :test do
|
||||
gem "capybara-screenshot", "~> 1.0.17"
|
||||
gem "cuprite", "~> 0.15.0"
|
||||
gem "selenium-devtools"
|
||||
gem "selenium-webdriver", "~> 4.18.0"
|
||||
gem "selenium-webdriver", "~> 4.20"
|
||||
|
||||
gem "fuubar", "~> 2.5.0"
|
||||
gem "timecop", "~> 0.9.0"
|
||||
@@ -292,7 +295,7 @@ end
|
||||
group :development do
|
||||
gem "listen", "~> 3.9.0" # Use for event-based reloaders
|
||||
|
||||
gem 'letter_opener_web'
|
||||
gem "letter_opener_web"
|
||||
|
||||
gem "spring"
|
||||
gem "spring-commands-rspec"
|
||||
@@ -381,6 +384,6 @@ gemfiles.each do |file|
|
||||
send(:eval_gemfile, file) if File.readable?(file)
|
||||
end
|
||||
|
||||
gem "openproject-octicons", "~>19.8.0"
|
||||
gem "openproject-octicons_helper", "~>19.8.0"
|
||||
gem "openproject-primer_view_components", "~>0.23.0"
|
||||
gem "openproject-octicons", "~>19.12.0"
|
||||
gem "openproject-octicons_helper", "~>19.12.0"
|
||||
gem "openproject-primer_view_components", "~>0.31.0"
|
||||
|
||||
+198
-198
@@ -1,28 +1,11 @@
|
||||
GIT
|
||||
remote: https://github.com/citizensadvice/capybara_accessible_selectors
|
||||
revision: 755b3d9b43695aeaae7edb09a24da0e3cda97ee4
|
||||
revision: 347bbe06cb420416855e80bb4e3a3016b2d5872c
|
||||
branch: main
|
||||
specs:
|
||||
capybara_accessible_selectors (0.11.0)
|
||||
capybara (~> 3.36)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/crohr/turbo_tests.git
|
||||
revision: b700cdb344e1ed8e100fd1f56525ec12fa792b45
|
||||
ref: fix/runtime-info
|
||||
specs:
|
||||
turbo_tests (2.0.0)
|
||||
bundler (>= 2.1)
|
||||
parallel_tests (>= 3.3.0, < 5)
|
||||
rspec (>= 3.10)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/honestica/clamav-client.git
|
||||
revision: 29e78ae94307cb34e79ddd29c5da79752239d8b7
|
||||
ref: 29e78ae94307cb34e79ddd29c5da79752239d8b7
|
||||
specs:
|
||||
clamav-client (3.4.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/opf/md-to-pdf
|
||||
revision: 8f14736a88ad0064d2a97be108fe7061ffbcee91
|
||||
@@ -66,6 +49,15 @@ GIT
|
||||
omniauth-openid_connect-providers (0.2.0)
|
||||
omniauth-openid-connect (>= 0.2.1)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/opf/turbo_tests.git
|
||||
revision: c1c4707f536a5642a168650d273d714dfb62d842
|
||||
ref: with-patches
|
||||
specs:
|
||||
turbo_tests (2.2.0)
|
||||
parallel_tests (>= 3.3.0, < 5)
|
||||
rspec (>= 3.10)
|
||||
|
||||
PATH
|
||||
remote: modules/auth_plugins
|
||||
specs:
|
||||
@@ -82,7 +74,7 @@ PATH
|
||||
remote: modules/avatars
|
||||
specs:
|
||||
openproject-avatars (1.0.0)
|
||||
fastimage (~> 2.2.0)
|
||||
fastimage (~> 2.3.0)
|
||||
gravatar_image_tag (~> 1.2.0)
|
||||
|
||||
PATH
|
||||
@@ -214,7 +206,7 @@ PATH
|
||||
remote: modules/two_factor_authentication
|
||||
specs:
|
||||
openproject-two_factor_authentication (1.0.0)
|
||||
aws-sdk-sns (~> 1.72.0)
|
||||
aws-sdk-sns (~> 1.75.0)
|
||||
messagebird-rest (~> 1.4.2)
|
||||
rotp (~> 6.1)
|
||||
webauthn (~> 3.0)
|
||||
@@ -233,36 +225,36 @@ PATH
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.1.0)
|
||||
actioncable (7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
Ascii85 (1.1.1)
|
||||
actioncable (7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
activejob (= 7.1.3.2)
|
||||
activerecord (= 7.1.3.2)
|
||||
activestorage (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
actionmailbox (7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
activejob (= 7.1.3.3)
|
||||
activerecord (= 7.1.3.3)
|
||||
activestorage (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
actionview (= 7.1.3.2)
|
||||
activejob (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
actionmailer (7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
actionview (= 7.1.3.3)
|
||||
activejob (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.3.2)
|
||||
actionview (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
actionpack (7.1.3.3)
|
||||
actionview (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
@@ -273,33 +265,33 @@ GEM
|
||||
actionpack-xml_parser (2.0.1)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
actiontext (7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
activerecord (= 7.1.3.2)
|
||||
activestorage (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
actiontext (7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
activerecord (= 7.1.3.3)
|
||||
activestorage (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
actionview (7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
activejob (7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
activemodel (7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
activemodel-serializers-xml (1.0.2)
|
||||
activemodel (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (7.1.3.2)
|
||||
activemodel (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
activerecord (7.1.3.3)
|
||||
activemodel (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
timeout (>= 0.4.0)
|
||||
activerecord-import (1.6.0)
|
||||
activerecord-import (1.7.0)
|
||||
activerecord (>= 4.2)
|
||||
activerecord-nulldb-adapter (1.0.1)
|
||||
activerecord (>= 5.2.0, < 7.2)
|
||||
@@ -310,13 +302,13 @@ GEM
|
||||
multi_json (~> 1.11, >= 1.11.2)
|
||||
rack (>= 2.0.8, < 4)
|
||||
railties (>= 6.1)
|
||||
activestorage (7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
activejob (= 7.1.3.2)
|
||||
activerecord (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
activestorage (7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
activejob (= 7.1.3.3)
|
||||
activerecord (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.3.2)
|
||||
activesupport (7.1.3.3)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
@@ -339,7 +331,7 @@ GEM
|
||||
airbrake-ruby (6.2.2)
|
||||
rbtree3 (~> 0.6)
|
||||
android_key_attestation (0.3.0)
|
||||
appsignal (3.6.4)
|
||||
appsignal (3.7.5)
|
||||
rack
|
||||
ast (2.4.2)
|
||||
attr_required (1.0.2)
|
||||
@@ -349,29 +341,29 @@ GEM
|
||||
activerecord (>= 4.0.0, < 7.2)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.901.0)
|
||||
aws-sdk-core (3.191.5)
|
||||
aws-partitions (1.934.0)
|
||||
aws-sdk-core (3.196.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.78.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (1.82.0)
|
||||
aws-sdk-core (~> 3, >= 3.193.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.146.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-s3 (1.151.0)
|
||||
aws-sdk-core (~> 3, >= 3.194.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sdk-sns (1.72.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-sns (1.75.0)
|
||||
aws-sdk-core (~> 3, >= 3.193.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axe-core-api (4.8.2)
|
||||
axe-core-api (4.9.1)
|
||||
dumb_delegator
|
||||
virtus
|
||||
axe-core-rspec (4.8.2)
|
||||
axe-core-api
|
||||
axe-core-rspec (4.9.1)
|
||||
axe-core-api (= 4.9.1)
|
||||
dumb_delegator
|
||||
virtus
|
||||
axiom-types (0.1.1)
|
||||
@@ -387,13 +379,13 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.7)
|
||||
bigdecimal (3.1.8)
|
||||
bindata (2.5.0)
|
||||
bootsnap (1.18.3)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.1.2)
|
||||
racc
|
||||
browser (5.3.1)
|
||||
browser (6.0.0)
|
||||
builder (3.2.4)
|
||||
byebug (11.1.3)
|
||||
capybara (3.40.0)
|
||||
@@ -408,11 +400,11 @@ GEM
|
||||
capybara-screenshot (1.0.26)
|
||||
capybara (>= 1.0, < 4)
|
||||
launchy
|
||||
carrierwave (1.3.2)
|
||||
carrierwave (1.3.4)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
ssrf_filter (~> 1.0)
|
||||
ssrf_filter (~> 1.0, < 1.1.0)
|
||||
carrierwave_direct (2.1.0)
|
||||
carrierwave (>= 1.0.0)
|
||||
fog-aws
|
||||
@@ -428,7 +420,8 @@ GEM
|
||||
descendants_tracker (~> 0.0.1)
|
||||
color_conversion (0.1.1)
|
||||
colored2 (4.0.0)
|
||||
commonmarker (1.0.4)
|
||||
commonmarker (1.1.3)
|
||||
rb_sys (~> 0.9)
|
||||
compare-xml (0.66)
|
||||
nokogiri (~> 1.8)
|
||||
concurrent-ruby (1.2.3)
|
||||
@@ -441,8 +434,9 @@ GEM
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.16.0)
|
||||
css_parser (1.17.1)
|
||||
addressable
|
||||
csv (3.3.0)
|
||||
cuprite (0.15)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.14.0)
|
||||
@@ -452,7 +446,7 @@ GEM
|
||||
date_validator (0.12.0)
|
||||
activemodel (>= 3)
|
||||
activesupport (>= 3)
|
||||
debug (1.9.1)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
deckar01-task_list (2.3.4)
|
||||
@@ -464,11 +458,11 @@ GEM
|
||||
disposable (0.6.3)
|
||||
declarative (>= 0.0.9, < 1.0.0)
|
||||
representable (>= 3.1.1, < 4)
|
||||
doorkeeper (5.6.9)
|
||||
doorkeeper (5.7.0)
|
||||
railties (>= 5)
|
||||
dotenv (3.1.0)
|
||||
dotenv-rails (3.1.0)
|
||||
dotenv (= 3.1.0)
|
||||
dotenv (3.1.2)
|
||||
dotenv-rails (3.1.2)
|
||||
dotenv (= 3.1.2)
|
||||
railties (>= 6.1)
|
||||
drb (2.2.1)
|
||||
dry-container (0.11.0)
|
||||
@@ -513,7 +507,7 @@ GEM
|
||||
erblint-github (1.0.1)
|
||||
erubi (1.12.0)
|
||||
escape_utils (1.3.0)
|
||||
et-orbi (1.2.9)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
eventmachine (1.2.7)
|
||||
eventmachine_httpserver (0.2.1)
|
||||
@@ -529,7 +523,7 @@ GEM
|
||||
faraday (>= 1, < 3)
|
||||
faraday-net_http (3.1.0)
|
||||
net-http
|
||||
fastimage (2.2.7)
|
||||
fastimage (2.3.1)
|
||||
ferrum (0.14)
|
||||
addressable (~> 2.5)
|
||||
concurrent-ruby (~> 1.1)
|
||||
@@ -556,8 +550,8 @@ GEM
|
||||
friendly_id (5.5.1)
|
||||
activerecord (>= 4.0.0)
|
||||
front_matter_parser (1.0.1)
|
||||
fugit (1.10.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
fugit (1.11.0)
|
||||
et-orbi (~> 1, >= 1.2.11)
|
||||
raabro (~> 1.4)
|
||||
fuubar (2.5.1)
|
||||
rspec-core (~> 3.0)
|
||||
@@ -577,7 +571,7 @@ GEM
|
||||
fugit (>= 1.1)
|
||||
railties (>= 6.0.0)
|
||||
thor (>= 0.14.1)
|
||||
google-apis-core (0.14.1)
|
||||
google-apis-core (0.15.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -585,8 +579,8 @@ GEM
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
google-apis-gmail_v1 (0.39.0)
|
||||
google-apis-core (>= 0.14.0, < 2.a)
|
||||
google-apis-gmail_v1 (0.41.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-env (2.1.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
googleauth (1.11.0)
|
||||
@@ -621,17 +615,16 @@ GEM
|
||||
http-2-next (1.0.3)
|
||||
http_parser.rb (0.6.0)
|
||||
httpclient (2.8.3)
|
||||
httpx (1.2.3)
|
||||
httpx (1.2.5)
|
||||
http-2-next (>= 1.0.3)
|
||||
i18n (1.14.4)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (4.2.3)
|
||||
glob (>= 0.4.0)
|
||||
i18n
|
||||
i18n-tasks (1.0.13)
|
||||
i18n-tasks (1.0.14)
|
||||
activesupport (>= 4.0.2)
|
||||
ast (>= 2.1.0)
|
||||
better_html (>= 1.0, < 3.0)
|
||||
erubi
|
||||
highline (>= 2.0.0)
|
||||
i18n
|
||||
@@ -645,12 +638,12 @@ GEM
|
||||
ice_nine (0.11.2)
|
||||
interception (0.5)
|
||||
io-console (0.7.2)
|
||||
irb (1.12.0)
|
||||
rdoc
|
||||
irb (1.13.1)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
iso8601 (0.13.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.1)
|
||||
json (2.7.2)
|
||||
json-jwt (1.16.6)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
@@ -658,7 +651,7 @@ GEM
|
||||
bindata
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
json-schema (4.2.0)
|
||||
json-schema (4.3.0)
|
||||
addressable (>= 2.8)
|
||||
json_schemer (2.2.1)
|
||||
base64
|
||||
@@ -674,16 +667,17 @@ GEM
|
||||
ladle (1.0.1)
|
||||
open4 (~> 1.0)
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (3.0.0)
|
||||
launchy (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
lefthook (1.6.7)
|
||||
letter_opener (1.0.0)
|
||||
launchy (>= 2.0.4)
|
||||
letter_opener_web (1.4.1)
|
||||
actionmailer (>= 3.2)
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
lefthook (1.6.13)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (3.0.0)
|
||||
actionmailer (>= 6.1)
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
listen (3.9.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
@@ -699,7 +693,7 @@ GEM
|
||||
loofah (2.22.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
lookbook (2.2.2)
|
||||
lookbook (2.3.1)
|
||||
activemodel
|
||||
css_parser
|
||||
htmlbeautifier (~> 1.3)
|
||||
@@ -720,15 +714,16 @@ GEM
|
||||
markly (0.10.0)
|
||||
matrix (0.4.2)
|
||||
messagebird-rest (1.4.2)
|
||||
meta-tags (2.20.0)
|
||||
meta-tags (2.21.0)
|
||||
actionpack (>= 6.0.0, < 7.2)
|
||||
method_source (1.0.0)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.0305)
|
||||
mime-types-data (3.2024.0507)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.22.3)
|
||||
mini_portile2 (2.8.6)
|
||||
minitest (5.23.1)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
mustermann (3.0.0)
|
||||
@@ -738,7 +733,7 @@ GEM
|
||||
mutex_m (0.2.0)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-imap (0.4.10)
|
||||
net-imap (0.4.11)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.19.0)
|
||||
@@ -746,10 +741,11 @@ GEM
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0.1)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.3)
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.5)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.3)
|
||||
bigdecimal (>= 3.0)
|
||||
@@ -757,6 +753,7 @@ GEM
|
||||
omniauth-saml (1.10.3)
|
||||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.9)
|
||||
op-clamav-client (3.4.2)
|
||||
open4 (1.3.4)
|
||||
openid_connect (2.2.1)
|
||||
activemodel
|
||||
@@ -770,15 +767,15 @@ GEM
|
||||
validate_email
|
||||
validate_url
|
||||
webfinger (~> 2.0)
|
||||
openproject-octicons (19.8.0)
|
||||
openproject-octicons_helper (19.8.0)
|
||||
openproject-octicons (19.12.0)
|
||||
openproject-octicons_helper (19.12.0)
|
||||
actionview
|
||||
openproject-octicons (= 19.8.0)
|
||||
openproject-octicons (= 19.12.0)
|
||||
railties
|
||||
openproject-primer_view_components (0.23.0)
|
||||
openproject-primer_view_components (0.31.0)
|
||||
actionview (>= 5.0.0)
|
||||
activesupport (>= 5.0.0)
|
||||
openproject-octicons (>= 19.8.0)
|
||||
openproject-octicons (>= 19.12.0)
|
||||
view_component (>= 3.1, < 4.0)
|
||||
openproject-token (4.0.0)
|
||||
activemodel
|
||||
@@ -791,12 +788,12 @@ GEM
|
||||
activerecord (>= 6.1)
|
||||
request_store (~> 1.4)
|
||||
parallel (1.24.0)
|
||||
parallel_tests (4.6.0)
|
||||
parallel_tests (4.7.1)
|
||||
parallel
|
||||
parser (3.3.0.5)
|
||||
parser (3.3.1.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-core (0.10.0)
|
||||
pdf-core (0.9.0)
|
||||
pdf-inspector (1.3.0)
|
||||
pdf-reader (>= 1.0, < 3.0.a)
|
||||
pdf-reader (2.12.0)
|
||||
@@ -810,10 +807,9 @@ GEM
|
||||
activesupport (> 2.2.1)
|
||||
nokogiri (~> 1.10, >= 1.10.4)
|
||||
rubyzip (>= 1.2.0)
|
||||
prawn (2.5.0)
|
||||
matrix (~> 0.4)
|
||||
pdf-core (~> 0.10.0)
|
||||
ttfunk (~> 1.8)
|
||||
prawn (2.4.0)
|
||||
pdf-core (~> 0.9.0)
|
||||
ttfunk (~> 1.7)
|
||||
prawn-table (0.2.2)
|
||||
prawn (>= 1.3.0, < 3.0.0)
|
||||
pry (0.14.2)
|
||||
@@ -832,7 +828,7 @@ GEM
|
||||
pry (>= 0.12.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.0.4)
|
||||
public_suffix (5.0.5)
|
||||
puffing-billy (4.0.0)
|
||||
addressable (~> 2.5)
|
||||
em-http-request (~> 1.1, >= 1.1.0)
|
||||
@@ -846,7 +842,7 @@ GEM
|
||||
puma-plugin-statsd (2.6.0)
|
||||
puma (>= 5.0, < 7)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.3)
|
||||
racc (1.8.0)
|
||||
rack (2.2.9)
|
||||
rack-accept (0.4.5)
|
||||
rack (>= 0.4)
|
||||
@@ -870,27 +866,27 @@ GEM
|
||||
rack (< 3)
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rack-timeout (0.7.0)
|
||||
rack_session_access (0.2.0)
|
||||
builder (>= 2.0.0)
|
||||
rack (>= 1.0.0)
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.3.2)
|
||||
actioncable (= 7.1.3.2)
|
||||
actionmailbox (= 7.1.3.2)
|
||||
actionmailer (= 7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
actiontext (= 7.1.3.2)
|
||||
actionview (= 7.1.3.2)
|
||||
activejob (= 7.1.3.2)
|
||||
activemodel (= 7.1.3.2)
|
||||
activerecord (= 7.1.3.2)
|
||||
activestorage (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
rails (7.1.3.3)
|
||||
actioncable (= 7.1.3.3)
|
||||
actionmailbox (= 7.1.3.3)
|
||||
actionmailer (= 7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
actiontext (= 7.1.3.3)
|
||||
actionview (= 7.1.3.3)
|
||||
activejob (= 7.1.3.3)
|
||||
activemodel (= 7.1.3.3)
|
||||
activerecord (= 7.1.3.3)
|
||||
activestorage (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.3.2)
|
||||
railties (= 7.1.3.3)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
@@ -905,42 +901,44 @@ GEM
|
||||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.1.3.2)
|
||||
actionpack (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
railties (7.1.3.3)
|
||||
actionpack (= 7.1.3.3)
|
||||
activesupport (= 7.1.3.3)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.1.0)
|
||||
rake (13.2.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rb_sys (0.9.97)
|
||||
rbtree3 (0.7.1)
|
||||
rdoc (6.6.3.1)
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
recaptcha (5.16.0)
|
||||
redcarpet (3.6.0)
|
||||
redis (5.1.0)
|
||||
redis-client (>= 0.17.0)
|
||||
redis-client (0.21.0)
|
||||
redis (5.2.0)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.22.2)
|
||||
connection_pool
|
||||
regexp_parser (2.9.0)
|
||||
reline (0.4.3)
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.7)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.6.0)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.6)
|
||||
rexml (3.2.8)
|
||||
strscan (>= 3.0.9)
|
||||
rinku (2.0.6)
|
||||
roar (1.2.0)
|
||||
representable (~> 3.1)
|
||||
@@ -955,7 +953,7 @@ GEM
|
||||
rspec-expectations (3.13.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.0)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.2)
|
||||
@@ -969,7 +967,7 @@ GEM
|
||||
rspec-retry (0.6.2)
|
||||
rspec-core (> 3.3)
|
||||
rspec-support (3.13.1)
|
||||
rubocop (1.62.1)
|
||||
rubocop (1.64.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
@@ -980,8 +978,8 @@ GEM
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.2)
|
||||
parser (>= 3.3.0.4)
|
||||
rubocop-ast (1.31.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.20.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.25.1)
|
||||
@@ -990,23 +988,26 @@ GEM
|
||||
activesupport
|
||||
rubocop
|
||||
rubocop-rspec
|
||||
rubocop-performance (1.20.2)
|
||||
rubocop-performance (1.21.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-rails (2.24.1)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.25.0)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (2.27.1)
|
||||
rubocop-rspec (2.29.2)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
rubocop-rspec_rails (~> 2.28)
|
||||
rubocop-rspec_rails (2.28.3)
|
||||
rubocop (~> 1.40)
|
||||
ruby-duration (3.2.3)
|
||||
activesupport (>= 3.0.0)
|
||||
i18n
|
||||
iso8601
|
||||
ruby-ole (1.2.12.2)
|
||||
ruby-ole (1.2.13.1)
|
||||
ruby-prof (1.7.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
@@ -1023,9 +1024,9 @@ GEM
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
secure_headers (6.5.0)
|
||||
selenium-devtools (0.123.0)
|
||||
selenium-devtools (0.125.0)
|
||||
selenium-webdriver (~> 4.2)
|
||||
selenium-webdriver (4.18.1)
|
||||
selenium-webdriver (4.21.1)
|
||||
base64 (~> 0.2)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
@@ -1039,30 +1040,31 @@ GEM
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simpleidn (0.2.1)
|
||||
unf (~> 0.1.4)
|
||||
simpleidn (0.2.3)
|
||||
smart_properties (1.17.0)
|
||||
spreadsheet (1.3.1)
|
||||
bigdecimal
|
||||
ruby-ole
|
||||
spring (4.1.3)
|
||||
spring (4.2.1)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
spring-commands-rubocop (0.4.0)
|
||||
spring (>= 1.0)
|
||||
sprockets (3.7.2)
|
||||
sprockets (3.7.3)
|
||||
base64
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
ssrf_filter (1.1.2)
|
||||
ssrf_filter (1.0.8)
|
||||
stackprof (0.2.26)
|
||||
store_attribute (1.2.0)
|
||||
activerecord (>= 6.0)
|
||||
stringex (2.8.6)
|
||||
stringio (3.1.0)
|
||||
strscan (3.1.0)
|
||||
structured_warnings (0.4.0)
|
||||
svg-graph (2.2.2)
|
||||
swd (2.0.3)
|
||||
@@ -1070,12 +1072,12 @@ GEM
|
||||
attr_required (>= 0.0.5)
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
sys-filesystem (1.4.4)
|
||||
sys-filesystem (1.4.5)
|
||||
ffi (~> 1.1)
|
||||
table_print (1.5.7)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
test-prof (1.3.2)
|
||||
test-prof (1.3.3)
|
||||
text-hyphen (1.5.0)
|
||||
thor (1.3.1)
|
||||
thread_safe (0.3.6)
|
||||
@@ -1086,8 +1088,7 @@ GEM
|
||||
openssl (> 2.0)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
trailblazer-option (0.1.2)
|
||||
ttfunk (1.8.0)
|
||||
bigdecimal (~> 3.1)
|
||||
ttfunk (1.7.0)
|
||||
turbo-rails (2.0.5)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
@@ -1099,9 +1100,6 @@ GEM
|
||||
tzinfo-data (1.2024.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.9.1)
|
||||
unicode-display_width (2.5.0)
|
||||
uri (0.13.0)
|
||||
validate_email (0.1.6)
|
||||
@@ -1111,7 +1109,7 @@ GEM
|
||||
activemodel (>= 3.0.0)
|
||||
public_suffix
|
||||
vcr (6.2.0)
|
||||
view_component (3.11.0)
|
||||
view_component (3.12.1)
|
||||
activesupport (>= 5.2.0, < 8.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
method_source (~> 1.0)
|
||||
@@ -1136,7 +1134,7 @@ GEM
|
||||
activesupport
|
||||
faraday (~> 2.0)
|
||||
faraday-follow_redirects
|
||||
webmock (3.23.0)
|
||||
webmock (3.23.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@@ -1152,7 +1150,7 @@ GEM
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
yard (0.9.36)
|
||||
zeitwerk (2.6.13)
|
||||
zeitwerk (2.6.14)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -1160,7 +1158,7 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
actionpack-xml_parser (~> 2.0.0)
|
||||
activemodel-serializers-xml (~> 1.0.1)
|
||||
activerecord-import (~> 1.6.0)
|
||||
activerecord-import (~> 1.7.0)
|
||||
activerecord-nulldb-adapter (~> 1.0.0)
|
||||
activerecord-session_store (~> 2.1.0)
|
||||
acts_as_list (~> 1.1.0)
|
||||
@@ -1176,20 +1174,20 @@ DEPENDENCIES
|
||||
bcrypt (~> 3.1.6)
|
||||
bootsnap (~> 1.18.0)
|
||||
brakeman (~> 6.1.0)
|
||||
browser (~> 5.3.0)
|
||||
browser (~> 6.0.0)
|
||||
budgets!
|
||||
capybara (~> 3.40.0)
|
||||
capybara-screenshot (~> 1.0.17)
|
||||
capybara_accessible_selectors!
|
||||
carrierwave (~> 1.3.1)
|
||||
carrierwave (~> 1.3.4)
|
||||
carrierwave_direct (~> 2.1.0)
|
||||
clamav-client!
|
||||
climate_control
|
||||
closure_tree (~> 7.4.0)
|
||||
colored2
|
||||
commonmarker (~> 1.0.3)
|
||||
commonmarker (~> 1.1.3)
|
||||
compare-xml (~> 0.66)
|
||||
costs!
|
||||
csv (~> 3.3)
|
||||
cuprite (~> 0.15.0)
|
||||
daemons
|
||||
dalli (~> 3.2.0)
|
||||
@@ -1198,7 +1196,7 @@ DEPENDENCIES
|
||||
debug
|
||||
deckar01-task_list (~> 2.3.1)
|
||||
disposable (~> 0.6.2)
|
||||
doorkeeper (~> 5.6.6)
|
||||
doorkeeper (~> 5.7.0)
|
||||
dotenv-rails
|
||||
dry-container
|
||||
email_validator (~> 2.2.3)
|
||||
@@ -1233,11 +1231,11 @@ DEPENDENCIES
|
||||
letter_opener_web
|
||||
listen (~> 3.9.0)
|
||||
lograge (~> 0.14.0)
|
||||
lookbook (~> 2.2.1)
|
||||
lookbook (~> 2.3.0)
|
||||
mail (= 2.8.1)
|
||||
matrix (~> 0.4.2)
|
||||
md_to_pdf!
|
||||
meta-tags (~> 2.20.0)
|
||||
meta-tags (~> 2.21.0)
|
||||
mini_magick (~> 4.12.0)
|
||||
multi_json (~> 1.15.0)
|
||||
my_page!
|
||||
@@ -1249,6 +1247,7 @@ DEPENDENCIES
|
||||
omniauth-openid-connect!
|
||||
omniauth-openid_connect-providers!
|
||||
omniauth-saml (~> 1.10.1)
|
||||
op-clamav-client (~> 3.4)
|
||||
openproject-auth_plugins!
|
||||
openproject-auth_saml!
|
||||
openproject-avatars!
|
||||
@@ -1263,10 +1262,10 @@ DEPENDENCIES
|
||||
openproject-job_status!
|
||||
openproject-ldap_groups!
|
||||
openproject-meeting!
|
||||
openproject-octicons (~> 19.8.0)
|
||||
openproject-octicons_helper (~> 19.8.0)
|
||||
openproject-octicons (~> 19.12.0)
|
||||
openproject-octicons_helper (~> 19.12.0)
|
||||
openproject-openid_connect!
|
||||
openproject-primer_view_components (~> 0.23.0)
|
||||
openproject-primer_view_components (~> 0.31.0)
|
||||
openproject-recaptcha!
|
||||
openproject-reporting!
|
||||
openproject-storages!
|
||||
@@ -1295,14 +1294,14 @@ DEPENDENCIES
|
||||
rack-mini-profiler
|
||||
rack-protection (~> 3.2.0)
|
||||
rack-test (~> 2.1.0)
|
||||
rack-timeout (~> 0.6.3)
|
||||
rack-timeout (~> 0.7.0)
|
||||
rack_session_access
|
||||
rails (~> 7.1.3)
|
||||
rails-controller-testing (~> 1.0.2)
|
||||
rails-i18n (~> 7.0.0)
|
||||
rdoc (>= 2.4.2)
|
||||
redis (~> 5.1.0)
|
||||
request_store (~> 1.6.0)
|
||||
redis (~> 5.2.0)
|
||||
request_store (~> 1.7.0)
|
||||
responders (~> 3.0)
|
||||
retriable (~> 3.1.1)
|
||||
rinku (~> 2.0.4)
|
||||
@@ -1323,7 +1322,7 @@ DEPENDENCIES
|
||||
sanitize (~> 6.1.0)
|
||||
secure_headers (~> 6.5.0)
|
||||
selenium-devtools
|
||||
selenium-webdriver (~> 4.18.0)
|
||||
selenium-webdriver (~> 4.20)
|
||||
semantic (~> 1.6.1)
|
||||
shoulda-context (~> 2.0)
|
||||
shoulda-matchers (~> 6.0)
|
||||
@@ -1341,6 +1340,7 @@ DEPENDENCIES
|
||||
table_print (~> 1.5.6)
|
||||
test-prof (~> 1.3.0)
|
||||
timecop (~> 0.9.0)
|
||||
ttfunk (~> 1.7.0)
|
||||
turbo-rails (~> 2.0.0)
|
||||
turbo_tests!
|
||||
typed_dag (~> 2.0.2)
|
||||
@@ -1355,7 +1355,7 @@ DEPENDENCIES
|
||||
with_advisory_lock (~> 5.1.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.3p157
|
||||
ruby 3.3.1p55
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.5
|
||||
2.5.10
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@import "work_packages/share/modal_body_component"
|
||||
@import "work_packages/share/invite_user_form_component"
|
||||
@import "work_packages/progress/modal_body_component"
|
||||
@import "open_project/common/attribute_component"
|
||||
@import "filters_component"
|
||||
@import "projects/settings/project_custom_field_sections/index_component"
|
||||
@import "projects/row_component"
|
||||
|
||||
@@ -29,14 +29,22 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<li class="op-activity-list--item">
|
||||
<div class="op-activity-list--item-title">
|
||||
<%= link_to @event.event_title.squish, @event.event_path %>
|
||||
<% if @event.event_path %>
|
||||
<%= link_to @event.event_title.squish, @event.event_path %>
|
||||
<% else %>
|
||||
<%= @event.event_title %>
|
||||
<% end %>
|
||||
<%= project_suffix %>
|
||||
</div>
|
||||
<%=
|
||||
render(Activities::ItemSubtitleComponent.new(user: display_user? && @event.event_author,
|
||||
datetime: @event.event_datetime,
|
||||
is_creation: @event.journal.initial?,
|
||||
journable_type: @event.journal.journable_type))
|
||||
render(Activities::ItemSubtitleComponent.new(
|
||||
user: display_user? && @event.event_author,
|
||||
datetime: @event.event_datetime,
|
||||
is_creation: initial?,
|
||||
is_deletion: deletion?,
|
||||
is_work_package: work_package?,
|
||||
journable_type: @event.journal.journable_type)
|
||||
)
|
||||
%>
|
||||
<% if comment.present? -%>
|
||||
<ul class="op-activity-list--item-details">
|
||||
@@ -60,5 +68,11 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<li class="op-activity-list--item-detail"><%= link_to "Details", @event.event_url %></li>
|
||||
<% end -%>
|
||||
</ul>
|
||||
<% elsif noop? -%>
|
||||
<ul class="op-activity-list--item-details">
|
||||
<li class="op-activity-list--item-detail">
|
||||
<em><%= I18n.t(:"journals.changes_retracted") %></em>
|
||||
</li>
|
||||
</ul>
|
||||
<% end -%>
|
||||
</li>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class Activities::ItemComponent < ViewComponent::Base
|
||||
class Activities::ItemComponent < ViewComponent::Base # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
with_collection_parameter :event
|
||||
strip_trailing_whitespace
|
||||
|
||||
@@ -44,7 +44,7 @@ class Activities::ItemComponent < ViewComponent::Base
|
||||
return if activity?(Project)
|
||||
return if activity_is_from_current_project?
|
||||
|
||||
kind = activity_is_from_subproject? ? 'subproject' : 'project'
|
||||
kind = activity_is_from_subproject? ? "subproject" : "project"
|
||||
suffix = I18n.t("events.title.#{kind}", name: link_to(@event.project.name, @event.project))
|
||||
"(#{suffix})".html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
@@ -54,13 +54,23 @@ class Activities::ItemComponent < ViewComponent::Base
|
||||
end
|
||||
|
||||
def display_details?
|
||||
return false if @event.journal.initial? && @event.journal.journable_type != 'TimeEntry'
|
||||
journal = @event.journal
|
||||
return false if (initial? && journal.journable_type != "TimeEntry") || deletion?
|
||||
|
||||
rendered_details.present?
|
||||
end
|
||||
|
||||
def noop?
|
||||
!initial? && !deletion? && @event.journal.noop?
|
||||
end
|
||||
|
||||
def rendered_details
|
||||
filter_details.filter_map { |detail| @event.journal.render_detail(detail, activity_page: @activity_page) }
|
||||
if data[:details]
|
||||
render_event_details
|
||||
else
|
||||
# this doesn't do anything for details of type "agenda_items_10_title"
|
||||
filter_journal_details.filter_map { |detail| @event.journal.render_detail(detail, activity_page: @activity_page) }
|
||||
end
|
||||
end
|
||||
|
||||
def comment
|
||||
@@ -81,6 +91,22 @@ class Activities::ItemComponent < ViewComponent::Base
|
||||
@event.event_url
|
||||
end
|
||||
|
||||
def initial?
|
||||
@event.journal.initial? || data[:initial]
|
||||
end
|
||||
|
||||
def deletion?
|
||||
data[:deleted]
|
||||
end
|
||||
|
||||
def work_package?
|
||||
data[:work_package]
|
||||
end
|
||||
|
||||
def data
|
||||
@event.data || {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def activity?(type)
|
||||
@@ -95,7 +121,7 @@ class Activities::ItemComponent < ViewComponent::Base
|
||||
@current_project && (@event.project != @current_project)
|
||||
end
|
||||
|
||||
def filter_details
|
||||
def filter_journal_details
|
||||
details = @event.journal.details
|
||||
|
||||
details.delete(:user_id) if details[:logged_by_id] == details[:user_id]
|
||||
@@ -110,4 +136,10 @@ class Activities::ItemComponent < ViewComponent::Base
|
||||
def delete_detail(details, field)
|
||||
details.delete(field) if details[field] && details[field].first.nil?
|
||||
end
|
||||
|
||||
def render_event_details
|
||||
data[:details].filter_map do |detail|
|
||||
@event.journal.render_detail(detail, activity_page: @activity_page)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,11 +29,13 @@
|
||||
#++
|
||||
|
||||
class Activities::ItemSubtitleComponent < ViewComponent::Base
|
||||
def initialize(user:, datetime:, is_creation:, journable_type:)
|
||||
def initialize(user:, datetime:, is_creation:, is_deletion:, is_work_package:, journable_type:)
|
||||
super()
|
||||
@user = user
|
||||
@datetime = datetime
|
||||
@is_creation = is_creation
|
||||
@is_deletion = is_deletion
|
||||
@is_work_package = is_work_package
|
||||
@journable_type = journable_type
|
||||
end
|
||||
|
||||
@@ -41,9 +43,9 @@ class Activities::ItemSubtitleComponent < ViewComponent::Base
|
||||
return unless @user
|
||||
|
||||
[
|
||||
helpers.avatar(@user, size: 'mini'),
|
||||
helpers.content_tag('span', helpers.link_to_user(@user), class: %w[spot-caption spot-caption_bold])
|
||||
].join(' ')
|
||||
helpers.avatar(@user, size: "mini"),
|
||||
helpers.content_tag("span", helpers.link_to_user(@user), class: %w[spot-caption spot-caption_bold])
|
||||
].join(" ")
|
||||
end
|
||||
|
||||
def datetime_html
|
||||
@@ -51,15 +53,27 @@ class Activities::ItemSubtitleComponent < ViewComponent::Base
|
||||
end
|
||||
|
||||
def time_entry?
|
||||
@journable_type == 'TimeEntry'
|
||||
@journable_type == "TimeEntry"
|
||||
end
|
||||
|
||||
def i18n_key
|
||||
i18n_key = 'activity.item.'.dup
|
||||
i18n_key << (@is_creation ? 'created_' : 'updated_')
|
||||
i18n_key << 'by_' if @user
|
||||
i18n_key << 'on'
|
||||
i18n_key << '_time_entry' if time_entry?
|
||||
i18n_key = +"activity.item."
|
||||
i18n_key << (@is_deletion ? deletion_selector : creation_selector)
|
||||
i18n_key << "by_" if @user
|
||||
i18n_key << "on"
|
||||
i18n_key << "_time_entry" if time_entry?
|
||||
i18n_key
|
||||
end
|
||||
|
||||
def deletion_selector
|
||||
@is_work_package ? "removed_" : "deleted_"
|
||||
end
|
||||
|
||||
def creation_selector
|
||||
if @is_creation
|
||||
@is_work_package ? "added_" : "created_"
|
||||
else
|
||||
"updated_"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<% helpers.html_title t(:label_administration), @title %>
|
||||
|
||||
<%= render(Primer::OpenProject::PageHeader.new(border_bottom: 0)) do |header| %>
|
||||
<% header.with_title { t(:"attributes.attachments") } %>
|
||||
<% header.with_breadcrumbs([{ href: admin_index_path, text: t("label_administration") },
|
||||
{ href: admin_settings_storages_path, text: t("project_module_storages") },
|
||||
t(:"attributes.attachments")]) %>
|
||||
<% end %>
|
||||
|
||||
<%= render(Primer::Alpha::TabNav.new(label: "label")) do |component|
|
||||
component.with_tab(selected: @selected == 1, href: admin_settings_attachments_path) do |tab|
|
||||
tab.with_text { t("settings.general") }
|
||||
end
|
||||
component.with_tab(selected: @selected == 2, href: admin_settings_virus_scanning_path) do |tab|
|
||||
tab.with_icon(icon: :"op-enterprise-addons") unless EnterpriseToken.allows_to?("virus_scanning")
|
||||
tab.with_text { t(:"settings.antivirus.title") }
|
||||
end
|
||||
if User.current.admin? && (EnterpriseToken.allows_to?(:virus_scanning) || Attachment.status_quarantined.any?)
|
||||
component.with_tab(selected: @selected == 3, href: admin_quarantined_attachments_path) do |tab|
|
||||
tab.with_text { t(:"antivirus_scan.quarantined_attachments.title") }
|
||||
end
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Admin
|
||||
class AttachmentsSettingsHeaderComponent < ApplicationComponent
|
||||
def initialize(title:, selected:)
|
||||
raise 'selected must 1, 2 or 3' if [1, 2, 3].exclude?(selected)
|
||||
@title = title
|
||||
@selected = selected
|
||||
end
|
||||
end
|
||||
end
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
<%= render(Settings::ProjectCustomFields::EditFormHeaderComponent.new(custom_field: @custom_field)) %>
|
||||
|
||||
<%=
|
||||
render(Primer::Alpha::TabNav.new(label: nil, test_selector: :project_attribute_detail_header)) do |component|
|
||||
component.with_tab(
|
||||
selected: project_custom_field_edit_selected?,
|
||||
href: edit_admin_settings_project_custom_field_path(@custom_field)
|
||||
) do |tab|
|
||||
tab.with_text { t(:label_details) }
|
||||
end
|
||||
|
||||
component.with_tab(
|
||||
selected: project_custom_field_project_mappings_selected?,
|
||||
href: project_mappings_admin_settings_project_custom_field_path(@custom_field)
|
||||
) do |tab|
|
||||
tab.with_text { I18n.t(:label_project_plural) }
|
||||
end
|
||||
end
|
||||
%>
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Admin
|
||||
module Settings
|
||||
module ProjectCustomFields
|
||||
class ProjectAttributeDetailHeaderComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
TAB_NAVS = %i[
|
||||
project_custom_field_edit
|
||||
project_custom_field_project_mappings
|
||||
].freeze
|
||||
|
||||
def initialize(custom_field:, selected:)
|
||||
selected = selected.to_sym
|
||||
raise "selected must be one of the following: #{TAB_NAVS.join(', ')}" unless TAB_NAVS.include?(selected)
|
||||
|
||||
super
|
||||
@custom_field = custom_field
|
||||
@selected = selected
|
||||
end
|
||||
|
||||
TAB_NAVS.each do |tab_nav|
|
||||
define_method(:"#{tab_nav}_selected?") do
|
||||
@selected == tab_nav
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -43,7 +43,7 @@ module OpTurbo
|
||||
included do
|
||||
def render_as_turbo_stream(view_context:, action: :update)
|
||||
case action
|
||||
when :update
|
||||
when :update, :dialog
|
||||
@inner_html_only = true
|
||||
template = render_in(view_context)
|
||||
when :replace
|
||||
@@ -56,7 +56,7 @@ module OpTurbo
|
||||
raise ArgumentError, "Unsupported action #{action}"
|
||||
end
|
||||
|
||||
unless wrapped?
|
||||
if action != :dialog && !wrapped?
|
||||
raise MissingComponentWrapper,
|
||||
"Wrap your component in a `component_wrapper` block in order to use turbo-stream methods"
|
||||
end
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
<div data-controller="filters"
|
||||
data-application-target="dynamic"
|
||||
data-filters-output-format-value="<%= output_format %>"
|
||||
data-filters-display-filters-value="<%= show_filters_section? %>">
|
||||
|
||||
<div class="op-filters-header">
|
||||
<%= render(Primer::Beta::Button.new(
|
||||
scheme: :secondary,
|
||||
disabled:,
|
||||
data: { 'filters-target': 'filterFormToggle',
|
||||
'action': 'filters#toggleDisplayFilters',
|
||||
'test-selector': 'filter-component-toggle' })) do %>
|
||||
<%= t(:label_filter) %>
|
||||
<%= render(Primer::Beta::Counter.new(count: filters_count, round: true, hide_if_zero: false, scheme: :default)) %>
|
||||
<%= render(Primer::Beta::Counter.new(count: filters_count,
|
||||
round: true,
|
||||
hide_if_zero: false,
|
||||
scheme: :default,
|
||||
test_selector: "filters-button-counter" )) %>
|
||||
<% end %>
|
||||
<div class="op-filters-header-actions">
|
||||
<% buttons.each do |button| %>
|
||||
@@ -31,8 +37,9 @@
|
||||
data-action="filters#toggleDisplayFilters"></a>
|
||||
<legend><%= t(:label_filter_plural) %></legend>
|
||||
<ul class="advanced-filters--filters">
|
||||
<% each_filter do |filter, filter_active| %>
|
||||
<% each_filter do |filter, filter_active, additional_options| %>
|
||||
<% filter_boolean = filter.is_a?(Queries::Filters::Shared::BooleanFilter) %>
|
||||
<% autocomplete_filter = additional_options.key?(:autocomplete_options) %>
|
||||
|
||||
<li class="advanced-filters--filter <%= filter_active ? '' : 'hidden' %>"
|
||||
filter-name="<%= filter.name %>"
|
||||
@@ -58,7 +65,12 @@
|
||||
} %>
|
||||
<% end %>
|
||||
<% value_visibility = operators_without_values.include?(selected_operator) ? 'hidden' : '' %>
|
||||
<% if filter_boolean %>
|
||||
<% if autocomplete_filter %>
|
||||
<%= render partial: 'filters/autocomplete',
|
||||
locals: { value_visibility: value_visibility,
|
||||
filter: filter,
|
||||
autocomplete_options: additional_options[:autocomplete_options] } %>
|
||||
<% elsif filter_boolean %>
|
||||
<%= render partial: 'filters/boolean',
|
||||
locals: { value_visibility: value_visibility,
|
||||
filter: filter } %>
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
class FiltersComponent < ApplicationComponent
|
||||
options :query
|
||||
options :disabled
|
||||
options output_format: "params"
|
||||
|
||||
renders_many :buttons, lambda { |**system_arguments|
|
||||
system_arguments[:ml] ||= 2
|
||||
@@ -43,15 +45,11 @@ class FiltersComponent < ApplicationComponent
|
||||
# Returns filters, active and inactive.
|
||||
# In case a filter is active, the active one will be preferred over the inactive one.
|
||||
def each_filter
|
||||
allowed_filters.map do |filter|
|
||||
allowed_filters.each do |filter|
|
||||
active_filter = query.find_active_filter(filter.name)
|
||||
filter_active = active_filter.present?
|
||||
additional_attributes = additional_filter_attributes(filter)
|
||||
|
||||
if filter_active
|
||||
yield active_filter, filter_active
|
||||
else
|
||||
yield filter, filter_active
|
||||
end
|
||||
yield active_filter.presence || filter, active_filter.present?, additional_attributes
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,6 +59,32 @@ class FiltersComponent < ApplicationComponent
|
||||
end
|
||||
|
||||
def filters_count
|
||||
query.filters.count
|
||||
@filters_count ||= query.filters.count
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# With this method we can pass additional options for each type of filter into the frontend. This is especially
|
||||
# useful when we want to pass options for the autocompleter components.
|
||||
#
|
||||
# When the method is overwritten in a subclass, the subclass should call super(filter) to get the default attributes.
|
||||
#
|
||||
# @param filter [QueryFilter] the filter for which we want to pass additional attributes
|
||||
# @return [Hash] the additional attributes for the filter, that will be yielded in the each_filter method
|
||||
def additional_filter_attributes(filter)
|
||||
case filter
|
||||
when Queries::Filters::Shared::ProjectFilter
|
||||
{
|
||||
autocomplete_options: {
|
||||
component: "opce-project-autocompleter",
|
||||
resource: "projects",
|
||||
filters: [
|
||||
{ name: "active", operator: "=", values: ["t"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= render(Primer::Alpha::Dialog.new(id:, title: scoped_t(:title), size: :large)) do |dialog| %>
|
||||
<% dialog.with_header(variant: :large) %>
|
||||
|
||||
<% case %>
|
||||
<% when can_delete_roles? && may_delete_shares? && shared_work_packages? %>
|
||||
<% dialog.with_body do %>
|
||||
<% if !can_delete? %>
|
||||
<%= paragraph { scoped_t(:can_remove_direct_but_not_shared_roles) } %>
|
||||
<%= paragraph { scoped_t(:also_work_packages_shared_with_user_html, shared_work_packages_link:, count: shared_work_packages_count) } %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:remove_project_membership_or_work_package_shares_too) %>
|
||||
<%= scoped_t(:will_not_affect_inherited_shares) if inherited_shared_work_packages_count? %>
|
||||
<% end %>
|
||||
<% elsif principal.is_a?(Group) %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:will_remove_the_groups_role) %>
|
||||
<%= scoped_t(:however_work_packages_shared_with_group_html, shared_work_packages_link:, count: shared_work_packages_count) %>
|
||||
<% end %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:remove_work_packages_shared_with_group_too) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:will_remove_the_users_role) %>
|
||||
<%= scoped_t(:however_work_packages_shared_with_user_html, shared_work_packages_link:, count: shared_work_packages_count) %>
|
||||
<% end %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:remove_work_packages_shared_with_user_too) %>
|
||||
<%= scoped_t(:will_not_affect_inherited_shares) if inherited_shared_work_packages_count? %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_footer do %>
|
||||
<%= cancel_button %>
|
||||
<%= button(scheme: :danger, tag: :a, href: delete_url(project: true), data: { method: :delete }) { t(:button_remove_member) } %>
|
||||
<%= button(scheme: :danger, tag: :a, href: delete_url(project: true, work_package_shares_role_id: "all"), data: { method: :delete }) { t(:button_remove_member_and_shares) } %>
|
||||
<% end %>
|
||||
<% when can_delete_roles? %>
|
||||
<% dialog.with_body do %>
|
||||
<% if !can_delete? %>
|
||||
<%= paragraph { scoped_t(:can_remove_direct_but_not_shared_roles) } %>
|
||||
<% elsif principal.is_a?(Group) %>
|
||||
<%= paragraph { scoped_t(:will_remove_all_group_access_priveleges) } %>
|
||||
<% else %>
|
||||
<%= paragraph { scoped_t(:will_remove_all_user_access_priveleges) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_footer do %>
|
||||
<%= cancel_button %>
|
||||
<%= button(scheme: :danger, tag: :a, href: delete_url(project: true), data: { method: :delete }) { t(:button_remove) } %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% dialog.with_body do %>
|
||||
<%= paragraph { scoped_t(:cannot_delete_inherited_membership) } %>
|
||||
<% if may_manage_user? %>
|
||||
<%= paragraph { scoped_t(:cannot_delete_inherited_membership_note_admin_html, administration_settings_link:) } %>
|
||||
<% else %>
|
||||
<%= paragraph { scoped_t(:cannot_delete_inherited_membership_note_non_admin) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_footer do %>
|
||||
<%= cancel_button %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,73 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Members
|
||||
class DeleteMemberDialogComponent < ::ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
options :row
|
||||
|
||||
delegate :shared_work_packages_count,
|
||||
:shared_work_packages_link,
|
||||
:administration_settings_link,
|
||||
:can_delete?,
|
||||
:can_delete_roles?,
|
||||
:may_delete_shares?,
|
||||
:shared_work_packages?,
|
||||
:may_manage_user?,
|
||||
to: :row
|
||||
|
||||
delegate :principal,
|
||||
:inherited_shared_work_packages_count?,
|
||||
to: :model
|
||||
|
||||
def paragraph(&) = render(Primer::Beta::Text.new(tag: "p"), &)
|
||||
def button(**, &) = render(Primer::Beta::Button.new(**), &)
|
||||
|
||||
def cancel_button = button(data: { close_dialog_id: id }) { t(:button_cancel) }
|
||||
|
||||
def scoped_t(key, **)
|
||||
t(key, scope: "members.delete_member_dialog", **)
|
||||
end
|
||||
|
||||
def id
|
||||
"principal-#{principal.id}-delete-member-dialog"
|
||||
end
|
||||
|
||||
def delete_url(project: nil, work_package_shares_role_id: nil)
|
||||
url_for(
|
||||
controller: "/members",
|
||||
action: "destroy_by_principal",
|
||||
project_id: row.project,
|
||||
principal_id: row.principal,
|
||||
project:,
|
||||
work_package_shares_role_id:
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= render(Primer::Alpha::Dialog.new(id:, title: scoped_t(:title), size: :large)) do |dialog| %>
|
||||
<% dialog.with_header(variant: :large) %>
|
||||
|
||||
<% case %>
|
||||
<% when direct_shared_work_packages_count? && other_shared_work_packages_count? %>
|
||||
<% dialog.with_body do %>
|
||||
<% if inherited_shared_work_packages_count? %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
|
||||
<%= scoped_t(:shared_with_permission_html, shared_work_packages_link:, count: shared_work_packages_count, shared_role_name:) %>
|
||||
<% end %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:revoke_all_or_with_role, shared_role_name:) %>
|
||||
<%= scoped_t(:will_not_affect_inherited_shares) %>
|
||||
<% end %>
|
||||
<% elsif principal.is_a?(Group) %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:shared_with_this_group_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
|
||||
<%= scoped_t(:shared_with_permission_html, shared_work_packages_link:, count: shared_work_packages_count, shared_role_name:) %>
|
||||
<% end %>
|
||||
<%= paragraph { scoped_t(:revoke_all_or_with_role, shared_role_name:) } %>
|
||||
<% else %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
|
||||
<%= scoped_t(:shared_with_permission_html, shared_work_packages_link:, count: shared_work_packages_count, shared_role_name:) %>
|
||||
<% end %>
|
||||
<%= paragraph { scoped_t(:revoke_all_or_with_role, shared_role_name:) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_footer do %>
|
||||
<%= cancel_button %>
|
||||
<%= button(scheme: :danger, tag: :a, href: delete_url(work_package_shares_role_id: "all"), data: { method: :delete }) { t(:button_revoke_all) } %>
|
||||
<%= button(scheme: :danger, tag: :a, href: delete_url(work_package_shares_role_id: shared_role_id), data: { method: :delete }) { t(:button_revoke_only, shared_role_name:) } %>
|
||||
<% end %>
|
||||
<% when direct_shared_work_packages_count? %>
|
||||
<% dialog.with_body do %>
|
||||
<% if inherited_shared_work_packages_count? %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
|
||||
<%= scoped_t(:will_revoke_directly_granted_access) %>
|
||||
<% end %>
|
||||
<% elsif principal.is_a?(Group) %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:shared_with_this_group_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
|
||||
<%= scoped_t(:will_revoke_access_to_all) %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= paragraph do %>
|
||||
<%= scoped_t(:shared_with_this_user_html, all_shared_work_packages_link:, count: all_shared_work_packages_count) %>
|
||||
<%= scoped_t(:will_revoke_access_to_all) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_footer do %>
|
||||
<%= cancel_button %>
|
||||
<%= button(scheme: :danger, tag: :a, href: delete_url(work_package_shares_role_id: "all"), data: { method: :delete }) { t(:button_revoke_access) } %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% dialog.with_body do %>
|
||||
<% if other_shared_work_packages_count? %>
|
||||
<%= paragraph { scoped_t(:cannot_remove_inherited_with_role, shared_role_name:) } %>
|
||||
<% else %>
|
||||
<%= paragraph { scoped_t(:cannot_remove_inherited, shared_role_name:) } %>
|
||||
<% end %>
|
||||
<% if may_manage_user? %>
|
||||
<%= paragraph { scoped_t(:cannot_remove_inherited_note_admin_html, administration_settings_link:) } %>
|
||||
<% else %>
|
||||
<%= paragraph { scoped_t(:cannot_remove_inherited_note_non_admin) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% dialog.with_footer do %>
|
||||
<%= cancel_button %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,77 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Members
|
||||
class DeleteWorkPackageSharesDialogComponent < ::ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
options :row
|
||||
|
||||
delegate :table,
|
||||
:shared_work_packages_count,
|
||||
:all_shared_work_packages_count,
|
||||
:shared_work_packages_link,
|
||||
:all_shared_work_packages_link,
|
||||
:administration_settings_link,
|
||||
:may_manage_user?,
|
||||
to: :row
|
||||
|
||||
delegate :shared_role_id,
|
||||
:shared_role_name,
|
||||
to: :table
|
||||
|
||||
delegate :principal,
|
||||
:other_shared_work_packages_count?,
|
||||
:direct_shared_work_packages_count?,
|
||||
:inherited_shared_work_packages_count?,
|
||||
to: :model
|
||||
|
||||
def paragraph(&) = render(Primer::Beta::Text.new(tag: "p"), &)
|
||||
def button(**, &) = render(Primer::Beta::Button.new(**), &)
|
||||
|
||||
def cancel_button = button(data: { close_dialog_id: id }) { t(:button_cancel) }
|
||||
|
||||
def scoped_t(key, **)
|
||||
t(key, scope: "members.delete_work_package_shares_dialog", **)
|
||||
end
|
||||
|
||||
def id
|
||||
"principal-#{principal.id}-delete-work-package-shares-dialog"
|
||||
end
|
||||
|
||||
def delete_url(work_package_shares_role_id: nil)
|
||||
url_for(
|
||||
controller: "/members",
|
||||
action: "destroy_by_principal",
|
||||
project_id: row.project,
|
||||
principal_id: row.principal,
|
||||
work_package_shares_role_id:
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +1,27 @@
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title { t(:label_member_plural) }
|
||||
header.with_title { page_title }
|
||||
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
|
||||
|
||||
header.with_actions do
|
||||
flex_layout do |actions|
|
||||
if User.current.allowed_in_project?(:manage_members, @project)
|
||||
actions.with_column(mr: BUTTON_MARGIN_RIGHT) do
|
||||
render(
|
||||
Primer::Beta::Button.new(
|
||||
scheme: :primary,
|
||||
size: :medium,
|
||||
aria: { label: I18n.t(:button_add_member) },
|
||||
title: I18n.t(:button_add_member),
|
||||
id: 'add-member-button',
|
||||
data: add_button_data_attributes
|
||||
)
|
||||
) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t('activerecord.models.member')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
actions.with_column do
|
||||
render(
|
||||
Primer::Beta::IconButton.new(
|
||||
icon: 'filter',
|
||||
id: 'filter-member-button',
|
||||
aria: { label: I18n.t(:description_filter) },
|
||||
class: 'toggle-member-filter-link',
|
||||
data: filter_button_data_attributes
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
header.with_action_button(scheme: :primary,
|
||||
mobile_icon: :plus,
|
||||
mobile_label: t('activerecord.models.member'),
|
||||
size: :medium,
|
||||
aria: { label: I18n.t(:button_add_member) },
|
||||
title: I18n.t(:button_add_member),
|
||||
id: "add-member-button",
|
||||
data: add_button_data_attributes) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t('activerecord.models.member')
|
||||
end
|
||||
|
||||
header.with_action_icon_button(mobile_icon: "filter",
|
||||
scheme: :default,
|
||||
icon: "filter",
|
||||
label: I18n.t(:description_filter),
|
||||
id: "filter-member-button",
|
||||
aria: { label: I18n.t(:description_filter) },
|
||||
class: "toggle-member-filter-link",
|
||||
data: filter_button_data_attributes)
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
class Members::IndexPageHeaderComponent < ApplicationComponent
|
||||
include OpPrimer::ComponentHelpers
|
||||
include ApplicationHelper
|
||||
|
||||
BUTTON_MARGIN_RIGHT = 2
|
||||
include Menus::MembersHelper
|
||||
|
||||
def initialize(project: nil)
|
||||
super
|
||||
@@ -57,4 +56,55 @@ class Members::IndexPageHeaderComponent < ApplicationComponent
|
||||
action: "members-form#toggleMemberFilter"
|
||||
}
|
||||
end
|
||||
|
||||
def breadcrumb_items
|
||||
[{ href: project_overview_path(@project.id), text: @project.name },
|
||||
{ href: project_members_path(@project), text: t(:label_member_plural) },
|
||||
current_breadcrumb_element]
|
||||
end
|
||||
|
||||
def page_title
|
||||
# Rework this, when the Members page actually works with queries
|
||||
@query ||= current_query
|
||||
query_name = @query[:query_name]
|
||||
|
||||
if @query && query_name
|
||||
query_name
|
||||
else
|
||||
t(:label_member_plural)
|
||||
end
|
||||
end
|
||||
|
||||
def current_breadcrumb_element
|
||||
# Rework this, when the Members page actually works with queries
|
||||
@query ||= current_query
|
||||
query_name = @query[:query_name]
|
||||
menu_header = @query[:menu_header]
|
||||
|
||||
if @query && query_name
|
||||
if menu_header.present?
|
||||
I18n.t("menus.breadcrumb.nested_element", section_header: menu_header, title: query_name).html_safe
|
||||
else
|
||||
query_name
|
||||
end
|
||||
else
|
||||
t(:label_member_plural)
|
||||
end
|
||||
end
|
||||
|
||||
def current_query
|
||||
query_name = nil
|
||||
menu_header = nil
|
||||
|
||||
first_level_menu_items.find do |section|
|
||||
section.children.find do |menu_query|
|
||||
if !!menu_query.selected
|
||||
query_name = menu_query.title
|
||||
menu_header = section.header
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
{ query_name:, menu_header: }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,12 +27,14 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= form_with model: member,
|
||||
<%= form_with model: member.new_record? ? [member.project, member] : member,
|
||||
builder: TabularFormBuilder,
|
||||
html: form_html_options do |f| %>
|
||||
<%= hidden_field_tag :page, params[:page].to_i %>
|
||||
<%= hidden_field_tag :per_page, params[:per_page].to_i %>
|
||||
|
||||
<%= hidden_field_tag f.field_name(:user_ids, multiple: true), member.principal.id if member.new_record? %>
|
||||
|
||||
<p>
|
||||
<% roles.each do |role| %>
|
||||
<label class="form--label-with-check-box">
|
||||
@@ -40,6 +42,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
role.id,
|
||||
member.roles.include?(role),
|
||||
role.name,
|
||||
disabled: role_disabled?(role),
|
||||
no_label: true
|
||||
%>
|
||||
<%= role %>
|
||||
@@ -48,13 +51,13 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= f.submit t(:button_change), class: "button -primary -small" %>
|
||||
<%= f.submit t(member.new_record? ? :button_add : :button_change), class: "button -primary -small" %>
|
||||
<%= link_to t(:button_cancel),
|
||||
'#',
|
||||
"#",
|
||||
data: {
|
||||
action: 'members-form#toggleMembershipEdit',
|
||||
'members-form-toggling-class-param': row.toggle_item_class_name
|
||||
action: "members-form#toggleMembershipEdit",
|
||||
"members-form-toggling-class-param": row.toggle_item_class_name
|
||||
},
|
||||
class: 'button -small toggle-membership-button' %>
|
||||
class: "button -small toggle-membership-button" %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#++
|
||||
|
||||
module Members
|
||||
class RowComponent < ::RowComponent
|
||||
class RowComponent < ::RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
property :principal
|
||||
delegate :project, to: :table
|
||||
|
||||
@@ -75,17 +75,52 @@ module Members
|
||||
end
|
||||
|
||||
def shared
|
||||
count = member.shared_work_packages_count
|
||||
if count > 0
|
||||
link_to I18n.t(:label_x_work_packages, count:),
|
||||
helpers.project_work_packages_shared_with_path(principal, member.project),
|
||||
target: "_blank", rel: "noopener"
|
||||
return unless may_view_shared_work_packages?
|
||||
return if member.shared_work_package_ids.empty?
|
||||
|
||||
shared_work_packages_link
|
||||
end
|
||||
|
||||
def shared_work_packages_count = member.shared_work_package_ids.length
|
||||
|
||||
def shared_work_packages_link
|
||||
link_to I18n.t(:label_x_work_packages, count: shared_work_packages_count),
|
||||
shared_work_packages_url,
|
||||
target: "_blank",
|
||||
rel: "noopener"
|
||||
end
|
||||
|
||||
def shared_work_packages_url
|
||||
if member.other_shared_work_packages_count.zero?
|
||||
all_shared_work_packages_url
|
||||
else
|
||||
helpers.project_work_packages_with_ids_path(member.shared_work_package_ids, member.project)
|
||||
end
|
||||
end
|
||||
|
||||
delegate :all_shared_work_packages_count, to: :member
|
||||
|
||||
def all_shared_work_packages_link
|
||||
link_to I18n.t(:label_x_work_packages, count: all_shared_work_packages_count),
|
||||
all_shared_work_packages_url,
|
||||
target: "_blank",
|
||||
rel: "noopener"
|
||||
end
|
||||
|
||||
def all_shared_work_packages_url
|
||||
helpers.project_work_packages_shared_with_path(principal, member.project)
|
||||
end
|
||||
|
||||
def administration_settings_link
|
||||
link_to "administration settings",
|
||||
edit_user_path(model.principal, tab: :groups),
|
||||
target: "_blank",
|
||||
rel: "noopener"
|
||||
end
|
||||
|
||||
def roles_label
|
||||
project_roles = member.roles.select { |role| role.is_a?(ProjectRole) }.uniq.sort
|
||||
label = h project_roles.collect(&:name).join(', ')
|
||||
project_roles = member.roles.grep(ProjectRole).uniq.sort
|
||||
label = h project_roles.collect(&:name).join(", ")
|
||||
|
||||
if principal&.admin?
|
||||
label << tag(:br) if project_roles.any?
|
||||
@@ -97,7 +132,7 @@ module Members
|
||||
|
||||
def role_form
|
||||
render Members::RoleFormComponent.new(
|
||||
member,
|
||||
member.project_role? ? member : Member.new(project:, principal:),
|
||||
row: self,
|
||||
params: controller.params,
|
||||
roles: table.available_roles
|
||||
@@ -114,42 +149,105 @@ module Members
|
||||
helpers.translate_user_status(model.principal.status)
|
||||
end
|
||||
|
||||
def may_update?
|
||||
table.authorize_update
|
||||
end
|
||||
def shared_work_packages? = member.shared_work_package_ids.present?
|
||||
|
||||
def may_delete?
|
||||
table.authorize_update
|
||||
end
|
||||
def may_update? = table.authorize_update
|
||||
def may_delete? = table.authorize_delete
|
||||
def may_view_shared_work_packages? = table.authorize_work_package_shares_view
|
||||
def may_delete_shares? = table.authorize_work_package_shares_delete
|
||||
def may_manage_user? = table.authorize_manage_user
|
||||
|
||||
def can_update? = may_update? && !table.hide_roles?
|
||||
def can_delete? = may_delete? && member.project_role? && member.deletable?
|
||||
def can_delete_roles? = may_delete? && member.project_role? && member.some_roles_deletable?
|
||||
def can_view_shared_work_packages? = may_view_shared_work_packages? && shared_work_packages?
|
||||
|
||||
def button_links
|
||||
if !model.project_role?
|
||||
[share_warning]
|
||||
elsif may_update? && may_delete?
|
||||
[edit_link, delete_link].compact
|
||||
elsif may_delete?
|
||||
[delete_link].compact
|
||||
return [] if actions.empty?
|
||||
|
||||
if actions.one?
|
||||
actions.first => {label:, **button_options}
|
||||
|
||||
[render(Primer::Beta::IconButton.new(**button_options, size: :small, "aria-label": label))]
|
||||
else
|
||||
[]
|
||||
[
|
||||
render(Primer::Alpha::ActionMenu.new) do |menu|
|
||||
menu.with_show_button(scheme: :invisible, size: :small, icon: :"kebab-horizontal", "aria-label": t(:button_actions),
|
||||
tooltip_direction: :w)
|
||||
actions.each do |action_options|
|
||||
action_options => {scheme:, label:, icon:, **button_options}
|
||||
menu.with_item(scheme:, label:, content_arguments: button_options) do |item|
|
||||
item.with_leading_visual_icon(icon:)
|
||||
end
|
||||
end
|
||||
end
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def share_warning
|
||||
content_tag(:span,
|
||||
title: I18n.t('members.no_modify_on_shared')) do
|
||||
helpers.op_icon('icon icon-info1')
|
||||
def actions
|
||||
@actions ||= [].tap do |actions|
|
||||
actions << edit_action_options if can_update?
|
||||
actions << view_work_package_shares_action_options if can_view_shared_work_packages?
|
||||
actions << delete_action_options if may_delete? && member.project_role?
|
||||
actions << delete_work_package_shares_action_options if may_delete_shares? && shared_work_packages?
|
||||
end
|
||||
end
|
||||
|
||||
def edit_link
|
||||
link_to(
|
||||
helpers.op_icon('icon icon-edit'),
|
||||
'#',
|
||||
class: "toggle-membership-button #{toggle_item_class_name}",
|
||||
'data-action': 'members-form#toggleMembershipEdit',
|
||||
'data-members-form-toggling-class-param': toggle_item_class_name,
|
||||
title: t(:button_edit)
|
||||
)
|
||||
def edit_action_options
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :pencil,
|
||||
label: I18n.t(:button_manage_roles),
|
||||
data: {
|
||||
action: "members-form#toggleMembershipEdit",
|
||||
members_form_toggling_class_param: toggle_item_class_name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_action_options
|
||||
dialog = Members::DeleteMemberDialogComponent.new(member, row: self)
|
||||
|
||||
content_for :content_body do
|
||||
render(dialog)
|
||||
end
|
||||
|
||||
{
|
||||
scheme: :danger,
|
||||
icon: "op-person-remove",
|
||||
label: I18n.t(:button_remove_member),
|
||||
data: {
|
||||
show_dialog_id: dialog.id
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def view_work_package_shares_action_options
|
||||
{
|
||||
scheme: :default,
|
||||
tag: :a,
|
||||
icon: "op-view-list",
|
||||
label: I18n.t(:button_view_shared_work_packages),
|
||||
href: shared_work_packages_url
|
||||
}
|
||||
end
|
||||
|
||||
def delete_work_package_shares_action_options
|
||||
dialog = Members::DeleteWorkPackageSharesDialogComponent.new(member, row: self)
|
||||
|
||||
content_for :content_body do
|
||||
render(dialog)
|
||||
end
|
||||
|
||||
{
|
||||
scheme: :danger,
|
||||
icon: :trash,
|
||||
label: I18n.t(:button_revoke_work_package_shares),
|
||||
data: {
|
||||
show_dialog_id: dialog.id
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def roles_css_id
|
||||
@@ -160,32 +258,6 @@ module Members
|
||||
"member-#{member.id}--edit-toggle-item"
|
||||
end
|
||||
|
||||
def delete_link
|
||||
if model.deletable?
|
||||
link_to(
|
||||
helpers.op_icon('icon icon-delete'),
|
||||
{ controller: '/members', action: 'destroy', id: model, page: params[:page] },
|
||||
method: :delete,
|
||||
data: { confirm: delete_link_confirmation, disable_with: I18n.t(:label_loading) },
|
||||
title: delete_title
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_title
|
||||
if model.disposable?
|
||||
I18n.t(:title_remove_and_delete_user)
|
||||
else
|
||||
I18n.t(:button_remove)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_link_confirmation
|
||||
if !User.current.admin? && model.include?(User.current)
|
||||
t(:text_own_membership_delete_confirmation)
|
||||
end
|
||||
end
|
||||
|
||||
def column_css_class(column)
|
||||
if column == :mail
|
||||
"email"
|
||||
|
||||
@@ -29,8 +29,16 @@
|
||||
#++
|
||||
|
||||
module Members
|
||||
class TableComponent < ::TableComponent
|
||||
options :authorize_update, :available_roles, :is_filtered, :project
|
||||
class TableComponent < ::TableComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
options :authorize_update,
|
||||
:authorize_delete,
|
||||
:authorize_work_package_shares_view,
|
||||
:authorize_work_package_shares_delete,
|
||||
:authorize_manage_user,
|
||||
:available_roles,
|
||||
:is_filtered,
|
||||
:shared_role_name,
|
||||
:project
|
||||
columns :name, :mail, :roles, :groups, :shared, :status
|
||||
sortable_columns :name, :mail, :status
|
||||
|
||||
@@ -40,7 +48,7 @@ module Members
|
||||
|
||||
def apply_member_scopes(model)
|
||||
model
|
||||
.with_shared_work_packages_count(only_role_id:)
|
||||
.with_shared_work_packages_info(only_role_id: shared_role_id)
|
||||
# This additional select is necessary for removing "duplicate" memberships in the members table
|
||||
# In reality, we want to show distinct principals in the members page, but are filtering on the members
|
||||
# table which now has multiplpe entries per user if they are the recipient of multiple shares,
|
||||
@@ -52,24 +60,29 @@ module Members
|
||||
Member
|
||||
.where(
|
||||
id: model
|
||||
.reselect('DISTINCT ON (members.user_id) members.id')
|
||||
.reorder('members.user_id, members.entity_type NULLS FIRST')
|
||||
.reselect("DISTINCT ON (members.user_id) members.id")
|
||||
.reorder("members.user_id, members.entity_type NULLS FIRST")
|
||||
)
|
||||
end
|
||||
|
||||
def only_role_id
|
||||
case params[:shared_role_id]
|
||||
when 'all', nil
|
||||
nil
|
||||
else
|
||||
params[:shared_role_id]
|
||||
end
|
||||
def shared_role_id
|
||||
params[:shared_role_id] if params[:shared_role_id].present? && params[:shared_role_id] != "all"
|
||||
end
|
||||
|
||||
def initial_sort
|
||||
%i[name asc]
|
||||
end
|
||||
|
||||
def hide_roles? = params[:shared_role_id].present?
|
||||
|
||||
def columns
|
||||
if hide_roles?
|
||||
super - [:roles]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def headers
|
||||
columns.map do |name|
|
||||
[name.to_s, header_options(name)]
|
||||
@@ -80,7 +93,7 @@ module Members
|
||||
caption =
|
||||
case name
|
||||
when :shared
|
||||
I18n.t('members.columns.shared')
|
||||
I18n.t("members.columns.shared")
|
||||
else
|
||||
User.human_attribute_name(name)
|
||||
end
|
||||
@@ -88,6 +101,10 @@ module Members
|
||||
{ caption: }
|
||||
end
|
||||
|
||||
def container_class
|
||||
"generic-table--container_visible-overflow"
|
||||
end
|
||||
|
||||
##
|
||||
# Adjusts the order so that users are joined to support
|
||||
# sorting by their attributes
|
||||
@@ -103,7 +120,7 @@ module Members
|
||||
if is_filtered
|
||||
I18n.t :notice_no_principals_found
|
||||
else
|
||||
I18n.t :'members.index.no_results_title_text'
|
||||
I18n.t :"members.index.no_results_title_text"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,11 +44,15 @@ module OpPrimer
|
||||
render(OpPrimer::ComponentCollectionComponent.new(**), &)
|
||||
end
|
||||
|
||||
def border_box_container(**system_args, &)
|
||||
Primer::Beta::BorderBox.new(position: :relative, **system_args, &)
|
||||
end
|
||||
|
||||
def border_box_row(wrapper_arguments, &)
|
||||
if container
|
||||
container.with_row(**wrapper_arguments, &)
|
||||
else
|
||||
container = Primer::Beta::BorderBox.new
|
||||
container = border_box_container
|
||||
row = container.registered_slots[:rows][:renderable_function]
|
||||
.bind_call(container, **wrapper_arguments)
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
<turbo-frame id="<%=turbo_frame_id%>">
|
||||
<%= content %>
|
||||
</turbo-frame>
|
||||
<% if options[:lazy] == true %>
|
||||
<turbo-frame id="<%=turbo_frame_id%>" src="<%= options[:lazy_source] %>" loading="lazy">
|
||||
<%= content %>
|
||||
</turbo-frame>
|
||||
<% else %>
|
||||
<turbo-frame id="<%=turbo_frame_id%>">
|
||||
<%= content %>
|
||||
</turbo-frame>
|
||||
<% end %>
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
<div
|
||||
data-controller="attribute"
|
||||
data-application-target="dynamic"
|
||||
data-attribute-background-reference-id-value="<%= background_reference_id %>"
|
||||
class="op-long-text-attribute">
|
||||
|
||||
<%= render(
|
||||
Primer::Beta::Text.new(tag: :div,
|
||||
classes: ['op-long-text-attribute--text', PARAGRAPH_CSS_CLASS],
|
||||
color: text_color,
|
||||
style: "max-height: #{max_height};",
|
||||
data: {
|
||||
'attribute-target': "descriptionText"
|
||||
})) { short_text } %>
|
||||
})) { short_text }
|
||||
%>
|
||||
|
||||
<%= render(
|
||||
Primer::Beta::Text.new(tag: :div,
|
||||
display: display_expand_button_value,
|
||||
classes: 'op-long-text-attribute--text-hider',
|
||||
data: { 'attribute-target': 'textHider' }))
|
||||
%>
|
||||
|
||||
<%= render(
|
||||
Primer::Alpha::HiddenTextExpander.new(inline: false,
|
||||
"aria-label": I18n.t('label_attribute_expand_text', attribute: name),
|
||||
display: display_expand_button_value,
|
||||
data: {
|
||||
'attribute-target': 'expandButton',
|
||||
'test-selector': 'expand-button'
|
||||
},
|
||||
button_arguments: { 'data-show-dialog-id': id },
|
||||
classes: 'op-long-text-attribute--text-expander'
|
||||
))
|
||||
%>
|
||||
|
||||
<%= render(
|
||||
Primer::Alpha::Dialog.new(id: id,
|
||||
data: {
|
||||
'test-selector': 'attribute-dialog'
|
||||
},
|
||||
title: name,
|
||||
size: :large)) do |component|
|
||||
component.with_show_button(scheme: :link,
|
||||
display: display_expand_button_value,
|
||||
ml: 1,
|
||||
data: { 'attribute-target': 'expandButton' }) { I18n.t('js.label_expand') }
|
||||
component.with_body(mt: 2) { full_text }
|
||||
component.with_header(variant: :large)
|
||||
end
|
||||
|
||||
@@ -32,16 +32,22 @@ module OpenProject
|
||||
class AttributeComponent < Primer::Component
|
||||
attr_reader :id,
|
||||
:name,
|
||||
:description
|
||||
:description,
|
||||
:lines,
|
||||
:background_reference_id,
|
||||
:formatted
|
||||
|
||||
PARAGRAPH_CSS_CLASS = "op-uc-p".freeze
|
||||
|
||||
def initialize(id, name, description, **args)
|
||||
def initialize(id, name, description, lines: 1, background_reference_id: "content", formatted: false, **args)
|
||||
super
|
||||
@id = id
|
||||
@name = name
|
||||
@description = description
|
||||
@system_arguments = args
|
||||
@lines = lines
|
||||
@background_reference_id = background_reference_id
|
||||
@formatted = formatted
|
||||
end
|
||||
|
||||
def short_text
|
||||
@@ -53,7 +59,7 @@ module OpenProject
|
||||
end
|
||||
|
||||
def full_text
|
||||
@full_text ||= helpers.format_text(description)
|
||||
@full_text ||= formatted ? description : helpers.format_text(description)
|
||||
end
|
||||
|
||||
def display_expand_button_value
|
||||
@@ -64,8 +70,20 @@ module OpenProject
|
||||
:muted if multi_type?
|
||||
end
|
||||
|
||||
def max_height
|
||||
"#{lines * 1.6}em"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def first_paragraph_content
|
||||
return unless first_paragraph_ast
|
||||
|
||||
first_paragraph_ast
|
||||
.inner_html
|
||||
.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
def first_paragraph
|
||||
@first_paragraph ||= if body_children.any?
|
||||
body_children
|
||||
@@ -77,6 +95,13 @@ module OpenProject
|
||||
end
|
||||
end
|
||||
|
||||
def first_paragraph_ast
|
||||
@first_paragraph_ast ||= text_ast
|
||||
.xpath("html/body")
|
||||
.children
|
||||
.first
|
||||
end
|
||||
|
||||
def text_ast
|
||||
@text_ast ||= Nokogiri::HTML(full_text)
|
||||
end
|
||||
@@ -88,7 +113,10 @@ module OpenProject
|
||||
end
|
||||
|
||||
def multi_type?
|
||||
first_paragraph.include?("figure") || first_paragraph.include?("macro")
|
||||
@multi_type ||= (description.present? && first_paragraph_ast.nil?) ||
|
||||
%w[opce-macro-embedded-table figure macro].include?(first_paragraph_ast.name) ||
|
||||
first_paragraph_ast.css("figure, macro, .op-uc-toc--list, .opce-macro-embedded-table")&.any? ||
|
||||
(body_children.any? && first_paragraph.blank?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
.op-long-text-attribute
|
||||
display: flex
|
||||
align-items: center
|
||||
position: relative
|
||||
|
||||
&--text
|
||||
@include text-shortener
|
||||
overflow: hidden
|
||||
margin: 0
|
||||
flex-grow: 1
|
||||
&--text-hider
|
||||
position: absolute
|
||||
bottom: 0
|
||||
right: 0
|
||||
height: 1.5em
|
||||
width: 2em
|
||||
&--text-expander
|
||||
position: absolute
|
||||
bottom: 1px
|
||||
right: 0
|
||||
float: right
|
||||
|
||||
@@ -42,9 +42,11 @@ module OpenProject
|
||||
end
|
||||
|
||||
def call
|
||||
render(Primer::Beta::Text.new) do
|
||||
localized_parts.join(separator)
|
||||
end
|
||||
render(Primer::Beta::Text.new) { text }
|
||||
end
|
||||
|
||||
def text
|
||||
localized_parts.join(separator)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -4,36 +4,47 @@
|
||||
# Hack to give the draggable autcompleter (ng-select) bound to the dialog
|
||||
# enough height to display all options.
|
||||
# This is necessary as long as ng-select does not support popovers.
|
||||
style: "min-height: 430px")) do |d| %>
|
||||
style: "min-height: 480px")) do |d| %>
|
||||
<% d.with_header(variant: :large, mb: 3) %>
|
||||
|
||||
<%= render(Primer::Alpha::Dialog::Body.new) do %>
|
||||
<%= primer_form_with(
|
||||
url: projects_path,
|
||||
id: COLUMN_FORM_ID,
|
||||
id: QUERY_FORM_ID,
|
||||
method: :get,
|
||||
data: {
|
||||
controller: "params-from-query",
|
||||
'application-target': "dynamic",
|
||||
'params-from-query-allowed-value': '["filters", "query_id", "page", "per_page", "sortBy"]'
|
||||
}) do %>
|
||||
<%= helpers.angular_component_tag 'opce-draggable-autocompleter',
|
||||
inputs: {
|
||||
options: helpers.projects_columns_options,
|
||||
selected: selected_columns,
|
||||
protected: helpers.protected_projects_columns_options,
|
||||
name: COLUMN_HTML_NAME,
|
||||
id: 'columns-select',
|
||||
inputLabel: I18n.t(:'queries.configure_view.columns.input_label'),
|
||||
inputPlaceholder: I18n.t(:'queries.configure_view.columns.input_placeholder'),
|
||||
dragAreaLabel: I18n.t(:'queries.configure_view.columns.drag_area_label'),
|
||||
appendToComponent: true
|
||||
}%>
|
||||
'params-from-query-allowed-value': '["filters", "query_id", "page", "per_page"]'
|
||||
}) do |form| %>
|
||||
<%= render(Primer::Alpha::TabPanels.new(label: "label")) do |tab_panel| %>
|
||||
<% tab_panel.with_tab(selected: true, id: "tab-selects") do |tab| %>
|
||||
<% tab.with_text { I18n.t("label_columns") } %>
|
||||
<% tab.with_panel do %>
|
||||
<%= helpers.angular_component_tag 'opce-draggable-autocompleter',
|
||||
inputs: {
|
||||
options: helpers.projects_columns_options,
|
||||
selected: selected_columns,
|
||||
protected: helpers.protected_projects_columns_options,
|
||||
name: COLUMN_HTML_NAME,
|
||||
id: 'columns-select',
|
||||
inputLabel: I18n.t(:'queries.configure_view.columns.input_label'),
|
||||
inputPlaceholder: I18n.t(:'queries.configure_view.columns.input_placeholder'),
|
||||
dragAreaLabel: I18n.t(:'queries.configure_view.columns.drag_area_label'),
|
||||
appendToComponent: true
|
||||
}%>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% tab_panel.with_tab(id: "tab-selects") do |tab| %>
|
||||
<% tab.with_text { I18n.t("label_sort") }%>
|
||||
<% tab.with_panel do %>
|
||||
<%= render(Queries::SortByComponent.new(query: query, selectable_columns:)) %>
|
||||
<% end%>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= render(Primer::Alpha::Dialog::Footer.new) do %>
|
||||
<%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
|
||||
<%= render(Primer::ButtonComponent.new(scheme: :primary, type: :submit, form: COLUMN_FORM_ID)) { I18n.t(:button_apply) } %>
|
||||
<%= render(Primer::ButtonComponent.new(scheme: :primary, type: :submit, form: QUERY_FORM_ID)) { I18n.t(:button_apply) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -30,11 +30,17 @@
|
||||
|
||||
class Projects::ConfigureViewModalComponent < ApplicationComponent
|
||||
MODAL_ID = "op-project-list-configure-dialog"
|
||||
COLUMN_FORM_ID = "op-project-list-configure-columns-form"
|
||||
QUERY_FORM_ID = "op-project-list-configure-query-form"
|
||||
COLUMN_HTML_NAME = "columns"
|
||||
|
||||
options :query
|
||||
|
||||
def selectable_columns
|
||||
@selectable_columns ||= [
|
||||
{ id: :lft, name: I18n.t(:label_project_hierarchy) }
|
||||
] + helpers.projects_columns_options
|
||||
end
|
||||
|
||||
def selected_columns
|
||||
@selected_columns ||= query
|
||||
.selects
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<p class="information-section">
|
||||
<%= helpers.op_icon('icon-info1') %>
|
||||
<%= t(:label_projects_disk_usage_information,
|
||||
count: Project.count,
|
||||
used_disk_space: number_to_human_size(Project.total_projects_size, precision: 2)) %>
|
||||
</p>
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
class Projects::StorageInformationComponent < ApplicationComponent
|
||||
class Projects::DiskUsageInformationComponent < ApplicationComponent
|
||||
options :current_user
|
||||
|
||||
def render?
|
||||
@@ -1,37 +1,42 @@
|
||||
<%= render(Primer::OpenProject::PageHeader.new) do |header| %>
|
||||
<% if show_state? %>
|
||||
<% header.with_title(data: { 'test-selector': 'project-query-name'}) { page_title } %>
|
||||
<% if show_state? %>
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title(data: { 'test-selector': 'project-query-name'}) { page_title }
|
||||
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
|
||||
|
||||
<% header.with_actions do %>
|
||||
<% if query_saveable? %>
|
||||
<%= render(
|
||||
Primer::Beta::Text.new(tag: :span,
|
||||
mr: BUTTON_MARGIN_RIGHT,
|
||||
color: :muted)) do
|
||||
t('lists.can_be_saved_as')
|
||||
if can_save?
|
||||
header_save_action(
|
||||
header:,
|
||||
message: t("lists.can_be_saved"),
|
||||
label: t("button_save"),
|
||||
href: projects_query_path(query),
|
||||
method: :patch
|
||||
)
|
||||
elsif can_save_as?
|
||||
header_save_action(
|
||||
header:,
|
||||
message: t("lists.can_be_saved_as"),
|
||||
label: t("button_save_as"),
|
||||
href: new_projects_query_path
|
||||
)
|
||||
end
|
||||
|
||||
header.with_action_menu(menu_arguments: {
|
||||
anchor_align: :end
|
||||
},
|
||||
button_arguments: {
|
||||
icon: "op-kebab-vertical",
|
||||
"aria-label": t(:label_more),
|
||||
data: { "test-selector": "project-more-dropdown-menu" }
|
||||
}) do |menu|
|
||||
if can_rename?
|
||||
menu.with_item(
|
||||
label: t('button_rename'),
|
||||
href: rename_projects_query_path(query),
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :pencil)
|
||||
end
|
||||
%>
|
||||
|
||||
<%= render(
|
||||
Primer::Beta::Button.new(scheme: :invisible,
|
||||
size: :medium,
|
||||
mr: BUTTON_MARGIN_RIGHT,
|
||||
tag: :a,
|
||||
href: new_projects_query_path,
|
||||
data: {
|
||||
controller: "params-from-query",
|
||||
'application-target': "dynamic",
|
||||
'params-from-query-allowed-value': '["filters", "columns"]'
|
||||
},
|
||||
classes: 'Button--invisibleOP')) do |button|
|
||||
button.with_leading_visual_icon(icon: :'op-save')
|
||||
|
||||
t('button_save_as')
|
||||
end %>
|
||||
<% end %>
|
||||
|
||||
<%= render(Primer::Alpha::ActionMenu.new) do |menu|
|
||||
menu.with_show_button(icon: 'op-kebab-vertical', 'aria-label': t(:label_more), data: { 'test-selector': 'project-more-dropdown-menu' })
|
||||
end
|
||||
|
||||
if gantt_portfolio_project_ids.any?
|
||||
menu.with_item(
|
||||
@@ -53,20 +58,12 @@
|
||||
item.with_leading_visual_icon(icon: 'tasklist')
|
||||
end
|
||||
|
||||
if query_saveable?
|
||||
menu.with_item(
|
||||
label: t('button_save_as'),
|
||||
href: new_projects_query_path,
|
||||
content_arguments: {
|
||||
data: {
|
||||
controller: "params-from-query",
|
||||
'application-target': "dynamic",
|
||||
'params-from-query-allowed-value': '["filters", "columns"]'
|
||||
}
|
||||
}
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :'op-save')
|
||||
end
|
||||
if can_save?
|
||||
menu_save_item(menu:, label: t('button_save'), href: projects_query_path(query), method: :patch)
|
||||
end
|
||||
|
||||
if may_save_as?
|
||||
menu_save_item(menu:, label: t('button_save_as'), href: new_projects_query_path)
|
||||
end
|
||||
|
||||
menu.with_item(
|
||||
@@ -84,6 +81,29 @@
|
||||
end
|
||||
|
||||
if query.persisted?
|
||||
# TODO: Remove section when the sharing modal is implemented (https://community.openproject.org/projects/openproject/work_packages/55163)
|
||||
if can_publish?
|
||||
if query.public?
|
||||
menu.with_item(
|
||||
label: t(:button_unpublish),
|
||||
scheme: :danger,
|
||||
href: unpublish_projects_query_path(query),
|
||||
content_arguments: { data: { method: :post } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: 'eye-closed')
|
||||
end
|
||||
else
|
||||
menu.with_item(
|
||||
label: t(:button_publish),
|
||||
scheme: :default,
|
||||
href: publish_projects_query_path(query),
|
||||
content_arguments: { data: { method: :post } }
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: 'eye')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
menu.with_item(
|
||||
label: t(:button_delete),
|
||||
scheme: :danger,
|
||||
@@ -93,28 +113,28 @@
|
||||
end
|
||||
end
|
||||
end
|
||||
%>
|
||||
<% end %>
|
||||
|
||||
<% else %>
|
||||
<% header.with_title(mt: 2, mb: 2, data: { 'test-selector': 'project-query-name'}) do
|
||||
end
|
||||
%>
|
||||
<%= render(Projects::ConfigureViewModalComponent.new(query:)) %>
|
||||
<%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %>
|
||||
<%= render(Projects::ExportListModalComponent.new(query:)) %>
|
||||
<% else %>
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title(data: { 'test-selector': 'project-query-name'}) do
|
||||
primer_form_with(model: query,
|
||||
url: projects_queries_path,
|
||||
url: @query.new_record? ? projects_queries_path : projects_query_path(@query),
|
||||
scope: 'query',
|
||||
data: {
|
||||
controller: "params-from-query",
|
||||
'application-target': "dynamic",
|
||||
'params-from-query-allowed-value': '["filters", "columns"]'
|
||||
'params-from-query-allowed-value': '["filters", "columns", "query_id", "sortBy"]'
|
||||
},
|
||||
id: 'project-save-form') do |f|
|
||||
render(Queries::Projects::Create.new(f))
|
||||
render(Queries::Projects::Form.new(f, query:))
|
||||
end
|
||||
end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if show_state? %>
|
||||
<%= render(Projects::ConfigureViewModalComponent.new(query:)) %>
|
||||
<%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %>
|
||||
<%= render(Projects::ExportListModalComponent.new(query:)) %>
|
||||
end
|
||||
header.with_breadcrumbs(breadcrumb_items)
|
||||
end
|
||||
%>
|
||||
<% end %>
|
||||
|
||||
@@ -37,11 +37,7 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
|
||||
:state,
|
||||
:params
|
||||
|
||||
BUTTON_MARGIN_RIGHT = 2
|
||||
|
||||
STATE_DEFAULT = :show
|
||||
STATE_EDIT = :edit
|
||||
STATE_OPTIONS = [STATE_DEFAULT, STATE_EDIT].freeze
|
||||
STATE_OPTIONS = %i[show edit rename].freeze
|
||||
|
||||
def initialize(current_user:, query:, params:, state: :show)
|
||||
super
|
||||
@@ -70,11 +66,108 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
|
||||
query.name || t(:label_project_plural)
|
||||
end
|
||||
|
||||
def query_saveable?
|
||||
current_user.logged? && query.name.blank?
|
||||
def may_save_as? = current_user.logged?
|
||||
|
||||
def can_save_as? = may_save_as? && query.changed?
|
||||
|
||||
def can_save?
|
||||
return false unless current_user.logged?
|
||||
return false unless query.persisted?
|
||||
return false unless query.changed?
|
||||
|
||||
if query.public?
|
||||
current_user.allowed_globally?(:manage_public_project_queries)
|
||||
else
|
||||
query.user == current_user
|
||||
end
|
||||
end
|
||||
|
||||
def can_rename?
|
||||
return false unless current_user.logged?
|
||||
return false unless query.persisted?
|
||||
return false if query.changed?
|
||||
|
||||
if query.public?
|
||||
current_user.allowed_globally?(:manage_public_project_queries)
|
||||
else
|
||||
query.user == current_user
|
||||
end
|
||||
end
|
||||
|
||||
def can_publish?
|
||||
OpenProject::FeatureDecisions.project_list_sharing_active? &&
|
||||
current_user.allowed_globally?(:manage_public_project_queries) &&
|
||||
query.persisted?
|
||||
end
|
||||
|
||||
def show_state?
|
||||
state == :show
|
||||
end
|
||||
|
||||
def breadcrumb_items
|
||||
[
|
||||
{ href: projects_path, text: t(:label_project_plural) },
|
||||
current_breadcrumb_element
|
||||
]
|
||||
end
|
||||
|
||||
def current_breadcrumb_element
|
||||
return page_title if query.name.blank?
|
||||
|
||||
if current_section && current_section.header.present?
|
||||
I18n.t("menus.breadcrumb.nested_element", section_header: current_section.header, title: query.name).html_safe
|
||||
else
|
||||
page_title
|
||||
end
|
||||
end
|
||||
|
||||
def current_section
|
||||
return @current_section if defined?(@current_section)
|
||||
|
||||
projects_menu = Menus::Projects.new(controller_path:, params:, current_user:)
|
||||
|
||||
@current_section = projects_menu.first_level_menu_items.find { |section| section.children.any?(&:selected) }
|
||||
end
|
||||
|
||||
def header_save_action(header:, message:, label:, href:, method: nil)
|
||||
header.with_action_text { message }
|
||||
|
||||
header.with_action_link(
|
||||
mobile_icon: nil, # Do not show on mobile as it is already part of the menu
|
||||
mobile_label: nil,
|
||||
href:,
|
||||
data: {
|
||||
method:,
|
||||
controller: "params-from-query",
|
||||
"application-target": "dynamic",
|
||||
"params-from-query-allowed-value": '["filters", "columns", "sortBy", "query_id"]'
|
||||
}.compact
|
||||
) do
|
||||
render(
|
||||
Primer::Beta::Octicon.new(
|
||||
icon: "op-save",
|
||||
align_self: :center,
|
||||
"aria-label": label,
|
||||
mr: 1
|
||||
)
|
||||
) + content_tag(:span, label)
|
||||
end
|
||||
end
|
||||
|
||||
def menu_save_item(menu:, label:, href:, method: nil)
|
||||
menu.with_item(
|
||||
label:,
|
||||
href:,
|
||||
content_arguments: {
|
||||
data: {
|
||||
method:,
|
||||
controller: "params-from-query",
|
||||
"application-target": "dynamic",
|
||||
"params-from-query-allowed-value": '["filters", "columns", "sortBy", "query_id"]'
|
||||
}.compact
|
||||
}
|
||||
) do |item|
|
||||
item.with_leading_visual_icon(icon: :"op-save")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,7 +47,8 @@ class Projects::ProjectsFiltersComponent < FiltersComponent
|
||||
Queries::Projects::Filters::CreatedAtFilter,
|
||||
Queries::Projects::Filters::LatestActivityAtFilter,
|
||||
Queries::Projects::Filters::NameAndIdentifierFilter,
|
||||
Queries::Projects::Filters::TypeFilter
|
||||
Queries::Projects::Filters::TypeFilter,
|
||||
Queries::Projects::Filters::FavoredFilter
|
||||
]
|
||||
allowlist << Queries::Filters::Shared::CustomFields::Base if EnterpriseToken.allows_to?(:custom_fields_in_projects_list)
|
||||
|
||||
|
||||
@@ -37,19 +37,8 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
</td>
|
||||
<% end %>
|
||||
<td class="buttons">
|
||||
<% if more_menu_items.any? %>
|
||||
<ul class="project-actions">
|
||||
<li aria-haspopup="true" title="<%= I18n.t(:label_open_menu) %>" class="drop-down">
|
||||
<a class="icon icon-show-more-horizontal context-menu--icon" title="<%= t(:label_open_menu) %>" href></a>
|
||||
<ul style="display:none;" class="menu-drop-down-container">
|
||||
<% more_menu_items.each do |item| %>
|
||||
<li>
|
||||
<%= link_to(*item) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<% button_links.each do |link| %>
|
||||
<%= link %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
</tr>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
#++
|
||||
module Projects
|
||||
class RowComponent < ::RowComponent
|
||||
delegate :favored_project_ids, to: :table
|
||||
|
||||
def project
|
||||
model.first
|
||||
end
|
||||
@@ -42,6 +44,27 @@ module Projects
|
||||
""
|
||||
end
|
||||
|
||||
def favored
|
||||
render(Primer::Beta::IconButton.new(
|
||||
icon: currently_favored? ? "star-fill" : "star",
|
||||
scheme: :invisible,
|
||||
mobile_icon: currently_favored? ? "star-fill" : "star",
|
||||
size: :medium,
|
||||
tag: :a,
|
||||
tooltip_direction: :e,
|
||||
href: helpers.build_favorite_path(project, format: :html),
|
||||
data: { method: currently_favored? ? :delete : :post },
|
||||
classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite",
|
||||
label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
|
||||
aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
|
||||
test_selector: "project-list-favorite-button"
|
||||
))
|
||||
end
|
||||
|
||||
def currently_favored?
|
||||
@currently_favored ||= favored_project_ids.include?(project.id)
|
||||
end
|
||||
|
||||
def column_value(column)
|
||||
if custom_field_column?(column)
|
||||
custom_field_column(column)
|
||||
@@ -57,7 +80,12 @@ module Projects
|
||||
custom_value = project.formatted_custom_value_for(cf)
|
||||
|
||||
if cf.field_format == "text" && custom_value.present?
|
||||
render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-cf-#{cf.id}", cf.name, custom_value.html_safe) # rubocop:disable Rails/OutputSafety
|
||||
render OpenProject::Common::AttributeComponent.new(
|
||||
"dialog-#{project.id}-cf-#{cf.id}",
|
||||
cf.name,
|
||||
custom_value,
|
||||
formatted: true
|
||||
)
|
||||
elsif custom_value.is_a?(Array)
|
||||
safe_join(Array(custom_value).compact_blank, ", ")
|
||||
else
|
||||
@@ -133,13 +161,17 @@ module Projects
|
||||
end
|
||||
|
||||
def row_css_class
|
||||
classes = %w[basics context-menu--reveal]
|
||||
classes = %w[basics context-menu--reveal op-project-row-component]
|
||||
classes << project_css_classes
|
||||
classes << row_css_level_classes
|
||||
|
||||
classes.join(" ")
|
||||
end
|
||||
|
||||
def row_css_id
|
||||
"project-#{project.id}"
|
||||
end
|
||||
|
||||
def row_css_level_classes
|
||||
if level > 0
|
||||
"idnt idnt-#{level}"
|
||||
@@ -165,7 +197,7 @@ module Projects
|
||||
def additional_css_class(column)
|
||||
if column.attribute == :name
|
||||
"project--hierarchy #{project.archived? ? 'archived' : ''}"
|
||||
elsif [:status_explanation, :description].include?(column.attribute)
|
||||
elsif %i[status_explanation description].include?(column.attribute)
|
||||
"project-long-text-container"
|
||||
elsif custom_field_column?(column)
|
||||
cf = column.custom_field
|
||||
@@ -174,82 +206,153 @@ module Projects
|
||||
end
|
||||
end
|
||||
|
||||
def button_links
|
||||
if more_menu_items.empty?
|
||||
[]
|
||||
else
|
||||
[action_menu]
|
||||
end
|
||||
end
|
||||
|
||||
def action_menu
|
||||
render(Primer::Alpha::ActionMenu.new(test_selector: "project-list-row--action-menu")) do |menu|
|
||||
menu.with_show_button(scheme: :invisible,
|
||||
size: :small,
|
||||
icon: :"kebab-horizontal",
|
||||
"aria-label": t(:label_open_menu),
|
||||
tooltip_direction: :w)
|
||||
more_menu_items.each do |action_options|
|
||||
action_options => { scheme:, label:, icon:, **button_options }
|
||||
menu.with_item(scheme:,
|
||||
label:,
|
||||
test_selector: "project-list-row--action-menu-item",
|
||||
content_arguments: button_options) do |item|
|
||||
item.with_leading_visual_icon(icon:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_items
|
||||
@more_menu_items ||= [more_menu_subproject_item,
|
||||
more_menu_settings_item,
|
||||
more_menu_activity_item,
|
||||
more_menu_favorite_item,
|
||||
more_menu_unfavorite_item,
|
||||
more_menu_archive_item,
|
||||
more_menu_unarchive_item,
|
||||
more_menu_copy_item,
|
||||
more_menu_delete_item].compact
|
||||
end
|
||||
|
||||
def more_menu_favorite_item
|
||||
return if currently_favored?
|
||||
|
||||
{
|
||||
scheme: :default,
|
||||
icon: "star",
|
||||
href: helpers.build_favorite_path(project, format: :html),
|
||||
data: { method: :post },
|
||||
label: I18n.t(:button_favorite),
|
||||
aria: { label: I18n.t(:button_favorite) }
|
||||
}
|
||||
end
|
||||
|
||||
def more_menu_unfavorite_item
|
||||
return unless currently_favored?
|
||||
|
||||
{
|
||||
scheme: :default,
|
||||
icon: "star-fill",
|
||||
size: :medium,
|
||||
href: helpers.build_favorite_path(project, format: :html),
|
||||
data: { method: :delete },
|
||||
classes: "op-primer--star-icon",
|
||||
label: I18n.t(:button_unfavorite),
|
||||
aria: { label: I18n.t(:button_unfavorite) }
|
||||
}
|
||||
end
|
||||
|
||||
def more_menu_subproject_item
|
||||
if User.current.allowed_in_project?(:add_subprojects, project)
|
||||
[t(:label_subproject_new),
|
||||
new_project_path(parent_id: project.id),
|
||||
{ class: "icon-context icon-add",
|
||||
title: t(:label_subproject_new) }]
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :plus,
|
||||
label: I18n.t(:label_subproject_new),
|
||||
href: new_project_path(parent_id: project.id)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_settings_item
|
||||
if User.current.allowed_in_project?({ controller: "/projects/settings/general", action: "show", project_id: project.id },
|
||||
project)
|
||||
[t(:label_project_settings),
|
||||
project_settings_general_path(project),
|
||||
{ class: "icon-context icon-settings",
|
||||
title: t(:label_project_settings) }]
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :gear,
|
||||
label: I18n.t(:label_project_settings),
|
||||
href: project_settings_general_path(project)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_activity_item
|
||||
if User.current.allowed_in_project?(:view_project_activity, project)
|
||||
[
|
||||
t(:label_project_activity),
|
||||
project_activity_index_path(project, event_types: ["project_attributes"]),
|
||||
{ class: "icon-context icon-checkmark",
|
||||
title: t(:label_project_activity) }
|
||||
]
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :check,
|
||||
label: I18n.t(:label_project_activity),
|
||||
href: project_activity_index_path(project, event_types: ["project_attributes"])
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_archive_item
|
||||
if User.current.allowed_in_project?(:archive_project, project) && project.active?
|
||||
[t(:button_archive),
|
||||
project_archive_path(project, status: params[:status]),
|
||||
{ data: { confirm: t("project.archive.are_you_sure", name: project.name) },
|
||||
method: :post,
|
||||
class: "icon-context icon-locked",
|
||||
title: t(:button_archive) }]
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :lock,
|
||||
label: I18n.t(:button_archive),
|
||||
href: project_archive_path(project, status: params[:status]),
|
||||
data: {
|
||||
confirm: t("project.archive.are_you_sure", name: project.name),
|
||||
method: :post
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_unarchive_item
|
||||
if User.current.admin? && project.archived? && (project.parent.nil? || project.parent.active?)
|
||||
[t(:button_unarchive),
|
||||
project_archive_path(project, status: params[:status]),
|
||||
{ method: :delete,
|
||||
class: "icon-context icon-unlocked",
|
||||
title: t(:button_unarchive) }]
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :unlock,
|
||||
label: I18n.t(:button_unarchive),
|
||||
href: project_archive_path(project, status: params[:status]),
|
||||
data: { method: :delete }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_copy_item
|
||||
if User.current.allowed_in_project?(:copy_projects, project) && !project.archived?
|
||||
[t(:button_copy),
|
||||
copy_project_path(project),
|
||||
{ class: "icon-context icon-copy",
|
||||
title: t(:button_copy) }]
|
||||
{
|
||||
scheme: :default,
|
||||
icon: :copy,
|
||||
label: I18n.t(:button_copy),
|
||||
href: copy_project_path(project)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def more_menu_delete_item
|
||||
if User.current.admin
|
||||
[t(:button_delete),
|
||||
confirm_destroy_project_path(project),
|
||||
{ class: "icon-context icon-delete",
|
||||
title: t(:button_delete) }]
|
||||
{
|
||||
scheme: :danger,
|
||||
icon: :trash,
|
||||
label: I18n.t(:button_delete),
|
||||
href: confirm_destroy_project_path(project)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -258,7 +361,7 @@ module Projects
|
||||
end
|
||||
|
||||
def custom_field_column?(column)
|
||||
column.is_a?(Queries::Projects::Selects::CustomField)
|
||||
column.is_a?(::Queries::Projects::Selects::CustomField)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.op-project-row-component
|
||||
&--favorite
|
||||
opacity: 0
|
||||
|
||||
&:hover &--favorite
|
||||
opacity: 1
|
||||
+4
-4
@@ -1,15 +1,15 @@
|
||||
<%=
|
||||
component_wrapper do
|
||||
flex_layout(align_items: :center, justify_content: :space_between, classes: 'op-project-custom-field', data: {
|
||||
qa_selector: "project-custom-field-#{@project_custom_field.id}"
|
||||
test_selector: "project-custom-field-#{@project_custom_field.id}"
|
||||
}) do |custom_field_container|
|
||||
custom_field_container.with_column(flex_layout: true) do |title_container|
|
||||
title_container.with_column(pt: 1, mr: 2) do
|
||||
render(Primer::Beta::Text.new) do
|
||||
render(Primer::Beta::Text.new) do
|
||||
@project_custom_field.name
|
||||
end
|
||||
end
|
||||
title_container.with_column(pt: 1, mr: 2, data: { qa_selector: "custom-field-type" } ) do
|
||||
title_container.with_column(pt: 1, mr: 2, data: { test_selector: "custom-field-type" } ) do
|
||||
render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) do
|
||||
@project_custom_field.field_format.capitalize
|
||||
end
|
||||
@@ -37,7 +37,7 @@
|
||||
),
|
||||
csrf_token: form_authenticity_token,
|
||||
data: { 'turbo-method': :put, 'turbo-stream': true,
|
||||
qa_selector: "toggle-project-custom-field-mapping-#{@project_custom_field.id}" },
|
||||
test_selector: "toggle-project-custom-field-mapping-#{@project_custom_field.id}" },
|
||||
checked: active_in_project?,
|
||||
enabled: !@project_custom_field.required?, # required fields cannot be disabled
|
||||
size: :small,
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<%= render Primer::OpenProject::PageHeader.new do |header| %>
|
||||
<%= header.with_title(variant: :default) { t('projects.settings.project_custom_fields.header.title') } %>
|
||||
<%= header.with_description { t('projects.settings.project_custom_fields.header.description',
|
||||
overview_url: project_path(@project),
|
||||
admin_settings_url: admin_settings_project_custom_fields_path
|
||||
).html_safe } %>
|
||||
<%= header.with_breadcrumbs(breadcrumb_items) %>
|
||||
<% end %>
|
||||
+13
-24
@@ -2,7 +2,7 @@
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2023 the OpenProject GmbH
|
||||
# Copyright (C) 2010-2023 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
@@ -27,32 +27,21 @@
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
#
|
||||
|
||||
module Meetings
|
||||
class AddButtonComponent < ::AddButtonComponent
|
||||
def render?
|
||||
if current_project
|
||||
User.current.allowed_in_project?(:create_meetings, current_project)
|
||||
else
|
||||
User.current.allowed_in_any_project?(:create_meetings)
|
||||
end
|
||||
module Projects::Settings::ProjectCustomFieldSections
|
||||
# rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
class IndexPageHeaderComponent < ApplicationComponent
|
||||
include ApplicationHelper
|
||||
|
||||
def initialize(project: nil)
|
||||
super
|
||||
@project = project
|
||||
end
|
||||
|
||||
def dynamic_path
|
||||
polymorphic_path([:new, current_project, :meeting])
|
||||
end
|
||||
|
||||
def id
|
||||
"add-meeting-button"
|
||||
end
|
||||
|
||||
def accessibility_label_text
|
||||
I18n.t(:label_meeting_new)
|
||||
end
|
||||
|
||||
def label_text
|
||||
I18n.t(:label_meeting)
|
||||
def breadcrumb_items
|
||||
[{ href: project_overview_path(@project.id), text: @project.name },
|
||||
{ href: project_settings_general_path(@project.id), text: I18n.t("label_project_settings") },
|
||||
t("settings.project_attributes.heading")]
|
||||
end
|
||||
end
|
||||
end
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
<%=
|
||||
component_wrapper do
|
||||
render(Primer::Beta::BorderBox.new(mt: 3, classes: 'op-project-custom-field-section', data: {
|
||||
qa_selector: "project-custom-field-section-#{@project_custom_field_section.id}"
|
||||
render(border_box_container(mt: 3, classes: 'op-project-custom-field-section', data: {
|
||||
test_selector: "project-custom-field-section-#{@project_custom_field_section.id}"
|
||||
})) do |component|
|
||||
component.with_header(font_weight: :bold, py: 2) do
|
||||
flex_layout(justify_content: :space_between, align_items: :center) do |section_header_container|
|
||||
@@ -26,7 +26,7 @@
|
||||
font_weight: :bold,
|
||||
color: :subtle,
|
||||
'aria-label': t('projects.settings.project_custom_fields.actions.label_enable_all'),
|
||||
data: { 'turbo-method': :put, 'turbo-stream': true, qa_selector: "enable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
|
||||
data: { 'turbo-method': :put, 'turbo-stream': true, test_selector: "enable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
|
||||
)) do |button|
|
||||
button.with_leading_visual_icon(icon: 'check-circle', color: :subtle)
|
||||
t('projects.settings.project_custom_fields.actions.label_enable_all')
|
||||
@@ -45,7 +45,7 @@
|
||||
font_weight: :bold,
|
||||
color: :subtle,
|
||||
'aria-label': t('projects.settings.project_custom_fields.actions.label_disable_all'),
|
||||
data: { 'turbo-method': :put, 'turbo-stream': true, qa_selector: "disable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
|
||||
data: { 'turbo-method': :put, 'turbo-stream': true, test_selector: "disable-all-project-custom-field-mappings-#{@project_custom_field_section.id}" }
|
||||
)) do |button|
|
||||
button.with_leading_visual_icon(icon: 'x-circle', color: :subtle)
|
||||
t('projects.settings.project_custom_fields.actions.label_disable_all')
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<p class="information-section">
|
||||
<%= helpers.op_icon('icon-info1') %>
|
||||
<%= t(:label_projects_storage_information,
|
||||
count: Project.count,
|
||||
storage: number_to_human_size(Project.total_projects_size, precision: 2)) %>
|
||||
</p>
|
||||
@@ -27,78 +27,85 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<div class="generic-table--flex-container">
|
||||
<div class="generic-table--container">
|
||||
<div class="generic-table--results-container">
|
||||
<table class="generic-table" <%= table_id ? "id=\"#{table_id}\"".html_safe : '' %>>
|
||||
<colgroup>
|
||||
<% columns.each do |column| %>
|
||||
<col <%= "opHighlightCol" unless column.attribute == :hierarchy %> >
|
||||
<% end %>
|
||||
<col opHighlightCol>
|
||||
</colgroup>
|
||||
<thead class="-sticky">
|
||||
<tr>
|
||||
<% columns.each do |column| %>
|
||||
<% if column.attribute == :hierarchy %>
|
||||
<th id="project-table--hierarchy-header">
|
||||
<div class="generic-table--sort-header-outer generic-table--sort-header-outer_no-highlighting">
|
||||
<div class="generic-table--sort-header"
|
||||
data-controller="params-from-query"
|
||||
data-application-target="dynamic"
|
||||
data-params-from-query-all-anchors-value="true"
|
||||
data-params-from-query-allowed-value='["query_id", "per_page", "filters", "columns"]'>
|
||||
<%= content_tag :a,
|
||||
helpers.op_icon("icon-hierarchy"),
|
||||
href: href_only_when_not_sort_lft,
|
||||
class: "spot-link #{deactivate_class_on_lft_sort}",
|
||||
title: t(:label_sort_by, value: %("#{t(:label_project_hierarchy)}")) %>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<% elsif sortable_column?(column) %>
|
||||
<%= build_sort_header column.attribute,
|
||||
order_options(column) %>
|
||||
<% else %>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<%= column.caption %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<div class="project-list-page--table">
|
||||
<div class="generic-table--flex-container">
|
||||
<div class="generic-table--container <%= container_class %>">
|
||||
<div class="generic-table--results-container">
|
||||
<table class="generic-table" <%= table_id ? "id=\"#{table_id}\"".html_safe : '' %>>
|
||||
<colgroup>
|
||||
<% columns.each do |column| %>
|
||||
<col <%= "opHighlightCol" unless column.attribute == :hierarchy %> >
|
||||
<% end %>
|
||||
<% end %>
|
||||
<th class="-right">
|
||||
<div class="generic-table--header-outer">
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if rows.empty? %>
|
||||
<tr class="generic-table--empty-row">
|
||||
<td colspan="<%= headers.length + 1 %>"><%= empty_row_message %></td>
|
||||
<col>
|
||||
</colgroup>
|
||||
<thead class="-sticky">
|
||||
<tr>
|
||||
<% columns.each do |column| %>
|
||||
<% if column.attribute == :hierarchy %>
|
||||
<th id="project-table--hierarchy-header">
|
||||
<div class="generic-table--sort-header-outer generic-table--sort-header-outer_no-highlighting">
|
||||
<div class="generic-table--sort-header"
|
||||
data-controller="params-from-query"
|
||||
data-application-target="dynamic"
|
||||
data-params-from-query-all-anchors-value="true"
|
||||
data-params-from-query-allowed-value='["query_id", "per_page", "filters", "columns"]'>
|
||||
<%= content_tag :a,
|
||||
helpers.op_icon("icon-hierarchy"),
|
||||
href: href_only_when_not_sort_lft,
|
||||
class: "spot-link #{deactivate_class_on_lft_sort}",
|
||||
title: t(:label_sort_by, value: %("#{t(:label_project_hierarchy)}")) %>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<% elsif sortable_column?(column) %>
|
||||
<%= build_sort_header column.attribute,
|
||||
order_options(column) %>
|
||||
<% else %>
|
||||
<th>
|
||||
<div class="generic-table--sort-header-outer">
|
||||
<div class="generic-table--sort-header">
|
||||
<span>
|
||||
<% if column.attribute == :favored %>
|
||||
<%= render(Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, ml: 2, "aria-label": I18n.t(:label_favorite))) %>
|
||||
<% else %>
|
||||
<%= column.caption %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<th>
|
||||
<div class="generic-table--empty-header">
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if rows.empty? %>
|
||||
<tr class="generic-table--empty-row">
|
||||
<td colspan="<%= columns.length + 1 %>"><%= empty_row_message %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<%= render_collection rows %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% if inline_create_link && show_inline_create %>
|
||||
<div class="wp-inline-create-button">
|
||||
<%= inline_create_link %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render_collection rows %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% if inline_create_link && show_inline_create %>
|
||||
<div class="wp-inline-create-button">
|
||||
<%= inline_create_link %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if paginated? %>
|
||||
<div data-controller="params-from-query"
|
||||
data-application-target="dynamic"
|
||||
data-params-from-query-all-anchors-value="true"
|
||||
data-params-from-query-allowed-value='["query_id", "columns"]'>
|
||||
<%= helpers.pagination_links_full model, { blocked_url_params: [:query_id, :columns] } %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if paginated? %>
|
||||
<div data-controller="params-from-query"
|
||||
data-application-target="dynamic"
|
||||
data-params-from-query-all-anchors-value="true"
|
||||
data-params-from-query-allowed-value='["query_id", "columns"]'>
|
||||
<%= helpers.pagination_links_full model, { blocked_url_params: [:query_id, :columns] } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -51,6 +51,10 @@ module Projects
|
||||
"project-table"
|
||||
end
|
||||
|
||||
def container_class
|
||||
"generic-table--container_visible-overflow"
|
||||
end
|
||||
|
||||
##
|
||||
# The project sort by is handled differently
|
||||
def build_sort_header(column, options)
|
||||
@@ -61,7 +65,7 @@ module Projects
|
||||
# but the [project, level] array from the helper
|
||||
def rows
|
||||
@rows ||= begin
|
||||
projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a } # rubocop:disable Lint/ToEnumArguments
|
||||
projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a }
|
||||
instance_exec(model, &projects_enumerator)
|
||||
end
|
||||
end
|
||||
@@ -109,10 +113,10 @@ module Projects
|
||||
|
||||
def columns
|
||||
@columns ||= begin
|
||||
columns = query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) }
|
||||
columns = query.selects.reject { |select| select.is_a?(::Queries::Selects::NotExistingSelect) }
|
||||
|
||||
index = columns.index { |column| column.attribute == :name }
|
||||
columns.insert(index, Queries::Projects::Selects::Default.new(:hierarchy)) if index
|
||||
columns.insert(index, ::Queries::Projects::Selects::Default.new(:hierarchy)) if index
|
||||
|
||||
columns
|
||||
end
|
||||
@@ -149,6 +153,10 @@ module Projects
|
||||
end
|
||||
end
|
||||
|
||||
def favored_project_ids
|
||||
@favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id)
|
||||
end
|
||||
|
||||
def sorted_by_lft?
|
||||
query.orders.first&.attribute == :lft
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<%= render(Primer::Beta::Heading.new(tag: :h5)) { I18n.t('queries.configure_view.sort_by.automatic.heading') } %>
|
||||
<%= render(Primer::Beta::Text.new(font_size: :small, color: :subtle)) { I18n.t('queries.configure_view.sort_by.automatic.description', plural: queried_model_name.plural) } %>
|
||||
<div data-controller="sort-by-config" data-application-target="dynamic">
|
||||
<%= hidden_field_tag :sortBy, current_orders, data: { "sort-by-config-target" => "sortByField" } %>
|
||||
<%= render(Primer::OpenProject::FlexLayout.new(data: { "sort-by-config-target" => "inputRowContainer" })) do |layout| %>
|
||||
<% order_limit.times do |i| %>
|
||||
<% layout.with_row(mt: 3, data: { "sort-by-config-target" => "inputRow" }) do %>
|
||||
<%= render(Queries::SortByFieldComponent.new(order: query.orders[i], available_orders:, index: i)) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2010-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
class Queries::SortByComponent < ApplicationComponent
|
||||
options :query
|
||||
options :selectable_columns
|
||||
|
||||
def current_orders
|
||||
JSON.dump(query.orders.map { |order| [order.attribute, order.direction] })
|
||||
end
|
||||
|
||||
def order_limit
|
||||
3
|
||||
end
|
||||
|
||||
def queried_model_name
|
||||
query.class.model.model_name
|
||||
end
|
||||
|
||||
def available_orders
|
||||
@available_orders ||= begin
|
||||
all_order_keys = ::Queries::Register.orders[query.class]&.map(&:key)
|
||||
|
||||
# Keys from the order can be symbols, strings or regexes
|
||||
selectable_columns.select do |column_option|
|
||||
all_order_keys.any? { |order_key| order_key === column_option[:id] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= render(Primer::OpenProject::FlexLayout.new(test_selector: 'sort-by-field')) do |flex| %>
|
||||
<% flex.with_column(flex: 1) do %>
|
||||
<%#- We are just using the classes of the primer component here, because when using the primer component, we cannot detach the input element from the form %>
|
||||
<%#- The form="none" adds the input to a nonexistant form (as we do not have one with the ID="none" and thus the fields to not get appended to the query string %>
|
||||
<%= select_tag 'sort_field', select_options, prompt: "-", form: "none", class: "FormControl-select FormControl-medium FormControl--fullWidth", data: { action: "change->sort-by-config#fieldChanged"} %>
|
||||
<% end %>
|
||||
<% flex.with_column do %>
|
||||
<%= render(Primer::Alpha::SegmentedControl.new("aria-label": "Sort order", hide_labels: true, ml: 3)) do |sort_buttons| %>
|
||||
<%#- The segmented control actions need to be included here as well as they do the visual styling of the currently selected option, just setting our action would remove their action %>
|
||||
<% sort_buttons.with_item(icon: "sort-asc", label: "sort ascending", selected: order_asc?, data: { direction: 'asc', action: "click:segmented-control#select click->sort-by-config#fieldChanged"}) %>
|
||||
<% sort_buttons.with_item(icon: "sort-desc", label: "sort descending", selected: order_desc?, data: { direction: 'desc', action: "click:segmented-control#select click->sort-by-config#fieldChanged"}) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2010-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
class Queries::SortByFieldComponent < ApplicationComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
options :order,
|
||||
:available_orders,
|
||||
:index
|
||||
|
||||
def select_options
|
||||
options_for_select(
|
||||
available_orders.map { |order| [order[:name], order[:id]] },
|
||||
order&.attribute
|
||||
)
|
||||
end
|
||||
|
||||
def active?
|
||||
order.present? && available_orders.any? { |o| order.attribute.to_sym == o[:id] }
|
||||
end
|
||||
|
||||
def order_asc?
|
||||
active? && order&.direction == :asc
|
||||
end
|
||||
|
||||
def order_desc?
|
||||
active? && order&.direction == :desc
|
||||
end
|
||||
end
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<%=
|
||||
component_wrapper(class: "op-project-custom-field-container", data: { qa_selector: "project-custom-field-container-#{@project_custom_field.id}" }) do
|
||||
component_wrapper(class: "op-project-custom-field-container", data: { test_selector: "project-custom-field-container-#{@project_custom_field.id}" }) do
|
||||
flex_layout(justify_content: :space_between, align_items: :center) do |main_container|
|
||||
main_container.with_column(flex_layout: true, align_items: :center) do |content_container|
|
||||
content_container.with_column(mr: 2) do
|
||||
@@ -36,7 +36,7 @@
|
||||
end
|
||||
end
|
||||
main_container.with_column do
|
||||
render(Primer::Alpha::ActionMenu.new(data: { qa_selector: "project-custom-field-action-menu" })) do |menu|
|
||||
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "project-custom-field-action-menu" })) do |menu|
|
||||
menu.with_show_button(icon: "kebab-horizontal", 'aria-label': t("settings.project_attributes.label_project_custom_field_actions"), scheme: :invisible)
|
||||
edit_action_item(menu)
|
||||
move_actions(menu)
|
||||
|
||||
@@ -45,7 +45,7 @@ module Settings
|
||||
def edit_action_item(menu)
|
||||
menu.with_item(label: t("label_edit"),
|
||||
href: edit_admin_settings_project_custom_field_path(@project_custom_field),
|
||||
data: { turbo: "false", qa_selector: "project-custom-field-edit" }) do |item|
|
||||
data: { turbo: "false", test_selector: "project-custom-field-edit" }) do |item|
|
||||
item.with_leading_visual_icon(icon: :pencil)
|
||||
end
|
||||
end
|
||||
@@ -68,7 +68,7 @@ module Settings
|
||||
menu.with_item(label: label_text,
|
||||
href: move_admin_settings_project_custom_field_path(@project_custom_field, move_to:),
|
||||
form_arguments: {
|
||||
method: :put, data: { 'turbo-stream': true, qa_selector: "project-custom-field-move-#{move_to}" }
|
||||
method: :put, data: { "turbo-stream": true, test_selector: "project-custom-field-move-#{move_to}" }
|
||||
}) do |item|
|
||||
item.with_leading_visual_icon(icon:)
|
||||
end
|
||||
@@ -79,8 +79,8 @@ module Settings
|
||||
scheme: :danger,
|
||||
href: admin_settings_project_custom_field_path(@project_custom_field),
|
||||
form_arguments: {
|
||||
method: :delete, data: { confirm: t("text_are_you_sure"), 'turbo-stream': true,
|
||||
qa_selector: "project-custom-field-delete" }
|
||||
method: :delete, data: { confirm: t("text_are_you_sure_with_project_custom_fields"),
|
||||
"turbo-stream": true, test_selector: "project-custom-field-delete" }
|
||||
}) do |item|
|
||||
item.with_leading_visual_icon(icon: :trash)
|
||||
end
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
component_wrapper do
|
||||
primer_form_with(**form_config) do |f|
|
||||
component_collection do |collection|
|
||||
collection.with_component(Primer::Alpha::Dialog::Body.new) do
|
||||
collection.with_component(Primer::BaseComponent.new(tag: :div)) do
|
||||
flex_layout(my: 3) do |modal_body|
|
||||
modal_body.with_row do
|
||||
render(ProjectCustomFieldSections::NameForm.new(f))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%=
|
||||
component_wrapper(class: "op-project-custom-field-section-container", data: { qa_selector: "project-custom-field-section-container-#{@project_custom_field_section.id}" }) do
|
||||
render(Primer::Beta::BorderBox.new(mt: 3, data: drag_and_drop_target_config)) do |component|
|
||||
component_wrapper(class: "op-project-custom-field-section-container", data: { test_selector: "project-custom-field-section-container-#{@project_custom_field_section.id}" }) do
|
||||
render(border_box_container(mt: 3, data: drag_and_drop_target_config)) do |component|
|
||||
component.with_header(font_weight: :bold) do
|
||||
flex_layout(justify_content: :space_between, align_items: :center) do |section_header_container|
|
||||
section_header_container.with_column(flex_layout: true, align_items: :center) do |content_container|
|
||||
@@ -15,7 +15,7 @@
|
||||
end
|
||||
section_header_container.with_column(flex_layout: true, justify_content: :flex_end) do |actions_container|
|
||||
actions_container.with_column do
|
||||
render(Primer::Alpha::ActionMenu.new(data: { qa_selector: "project-custom-field-section-action-menu" })) do |menu|
|
||||
render(Primer::Alpha::ActionMenu.new(data: { test_selector: "project-custom-field-section-action-menu" })) do |menu|
|
||||
menu.with_show_button(icon: "kebab-horizontal", 'aria-label': t("settings.project_attributes.label_section_actions"), scheme: :invisible)
|
||||
edit_action_item(menu)
|
||||
move_actions(menu)
|
||||
@@ -50,7 +50,7 @@
|
||||
type: "ProjectCustomField", custom_field_section_id: @project_custom_field_section.id
|
||||
),
|
||||
scheme: :secondary,
|
||||
data: { turbo: "false", qa_selector: "new-project-custom-field-button" }
|
||||
data: { turbo: "false", test_selector: "new-project-custom-field-button" }
|
||||
)) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t('settings.project_attributes.label_new_attribute')
|
||||
|
||||
@@ -49,18 +49,18 @@ module Settings
|
||||
|
||||
def drag_and_drop_target_config
|
||||
{
|
||||
'is-drag-and-drop-target': true,
|
||||
'target-container-accessor': '.Box > ul', # the accessor of the container that contains the drag and drop items
|
||||
'target-id': @project_custom_field_section.id, # the id of the target
|
||||
'target-allowed-drag-type': 'custom-field' # the type of dragged items which are allowed to be dropped in this target
|
||||
"is-drag-and-drop-target": true,
|
||||
"target-container-accessor": ".Box > ul", # the accessor of the container that contains the drag and drop items
|
||||
"target-id": @project_custom_field_section.id, # the id of the target
|
||||
"target-allowed-drag-type": "custom-field" # the type of dragged items which are allowed to be dropped in this target
|
||||
}
|
||||
end
|
||||
|
||||
def draggable_item_config(project_custom_field)
|
||||
{
|
||||
'draggable-id': project_custom_field.id,
|
||||
'draggable-type': 'custom-field',
|
||||
'drop-url': drop_admin_settings_project_custom_field_path(project_custom_field)
|
||||
"draggable-id": project_custom_field.id,
|
||||
"draggable-type": "custom-field",
|
||||
"drop-url": drop_admin_settings_project_custom_field_path(project_custom_field)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -82,7 +82,8 @@ module Settings
|
||||
menu.with_item(label: label_text,
|
||||
href: move_admin_settings_project_custom_field_section_path(@project_custom_field_section, move_to:),
|
||||
form_arguments: {
|
||||
method: :put, data: { 'turbo-stream': true, qa_selector: "project-custom-field-section-move-#{move_to}" }
|
||||
method: :put, data: { "turbo-stream": true,
|
||||
test_selector: "project-custom-field-section-move-#{move_to}" }
|
||||
}) do |item|
|
||||
item.with_leading_visual_icon(icon:)
|
||||
end
|
||||
@@ -99,8 +100,8 @@ module Settings
|
||||
menu.with_item(label: t("settings.project_attributes.label_edit_section"),
|
||||
tag: :button,
|
||||
content_arguments: {
|
||||
'data-show-dialog-id': "project-custom-field-section-dialog#{@project_custom_field_section.id}",
|
||||
'data-qa-selector': "project-custom-field-section-edit"
|
||||
"data-show-dialog-id": "project-custom-field-section-dialog#{@project_custom_field_section.id}",
|
||||
"data-test-selector": "project-custom-field-section-edit"
|
||||
},
|
||||
value: "") do |item|
|
||||
item.with_leading_visual_icon(icon: :pencil)
|
||||
@@ -112,8 +113,8 @@ module Settings
|
||||
scheme: :danger,
|
||||
href: admin_settings_project_custom_field_section_path(@project_custom_field_section),
|
||||
form_arguments: {
|
||||
method: :delete, data: { confirm: t("text_are_you_sure"), 'turbo-stream': true,
|
||||
qa_selector: "project-custom-field-section-delete" }
|
||||
method: :delete, data: { confirm: t("text_are_you_sure"), "turbo-stream": true,
|
||||
test_selector: "project-custom-field-section-delete" }
|
||||
}) do |item|
|
||||
item.with_leading_visual_icon(icon: :trash)
|
||||
end
|
||||
|
||||
@@ -28,11 +28,8 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
++#%>
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title(variant: :medium) { @custom_field.name }
|
||||
header.with_description { t('settings.project_attributes.edit.description') }
|
||||
header.with_parent_link(href: admin_settings_project_custom_fields_path, 'aria-label': I18n.t("button_back")) do
|
||||
t('settings.project_attributes.heading')
|
||||
end
|
||||
header.with_back_button(href: admin_settings_project_custom_fields_path, 'aria-label': t('button_back'))
|
||||
header.with_title { @custom_field.name }
|
||||
header.with_description { t("settings.project_attributes.edit.description") }
|
||||
header.with_breadcrumbs(breadcrumbs_items)
|
||||
end
|
||||
%>
|
||||
%>
|
||||
|
||||
@@ -34,6 +34,14 @@ module Settings
|
||||
|
||||
@custom_field = custom_field
|
||||
end
|
||||
|
||||
def breadcrumbs_items
|
||||
[{ href: admin_index_path, text: t("label_administration") },
|
||||
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
|
||||
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
|
||||
@custom_field.name
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
<% button_block = lambda do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t('settings.project_attributes.label_new_section')
|
||||
end %>
|
||||
|
||||
<%=
|
||||
component_wrapper do
|
||||
component_wrapper do
|
||||
render Primer::OpenProject::PageHeader.new do |header|
|
||||
header.with_title(variant: :default) { t('settings.project_attributes.heading') }
|
||||
header.with_description { t('settings.project_attributes.heading_description') }
|
||||
header.with_actions do
|
||||
flex_layout(justify_content: :space_between, align_items: :center) do |action_buttons_container|
|
||||
action_buttons_container.with_column(mr: 2) do
|
||||
render(Primer::Beta::Button.new(
|
||||
tag: :a,
|
||||
href: new_admin_settings_project_custom_field_path(type: "ProjectCustomField"),
|
||||
scheme: :primary,
|
||||
data: { turbo: "false", qa_selector: "new-project-custom-field-button" }
|
||||
)) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t('settings.project_attributes.label_new_attribute')
|
||||
end
|
||||
end
|
||||
action_buttons_container.with_column do
|
||||
render(Primer::Alpha::Dialog.new(
|
||||
id: "project-custom-field-section-dialog", title: t('settings.project_attributes.label_new_section'),
|
||||
size: :medium_portrait
|
||||
)) do |dialog|
|
||||
dialog.with_show_button('aria-label': t('settings.project_attributes.label_new_section'), scheme: :default) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t('settings.project_attributes.label_new_section')
|
||||
end
|
||||
render(Settings::ProjectCustomFieldSections::DialogBodyFormComponent.new())
|
||||
end
|
||||
end
|
||||
header.with_title(variant: :default) { t("settings.project_attributes.heading") }
|
||||
header.with_description { t("settings.project_attributes.heading_description") }
|
||||
header.with_breadcrumbs(breadcrumbs_items)
|
||||
header.with_action_button(tag: :a,
|
||||
href: new_admin_settings_project_custom_field_path(type: "ProjectCustomField"),
|
||||
scheme: :primary,
|
||||
data: { turbo: "false", test_selector: "new-project-custom-field-button" },
|
||||
mobile_icon: :plus,
|
||||
mobile_label: t("settings.project_attributes.label_new_attribute")) do |button|
|
||||
button.with_leading_visual_icon(icon: :plus)
|
||||
t("settings.project_attributes.label_new_attribute")
|
||||
end
|
||||
|
||||
header.with_action_dialog(mobile_icon: :plus,
|
||||
mobile_label: t('settings.project_attributes.label_new_section'),
|
||||
dialog_arguments: {id: "project-custom-field-section-dialog", title: t('settings.project_attributes.label_new_section', size: :medium_portrait), size: :medium_portrait},
|
||||
button_arguments: {'aria-label': t('settings.project_attributes.label_new_section'), button_block: button_block}) do |dialog|
|
||||
dialog.with_body do
|
||||
render(Settings::ProjectCustomFieldSections::DialogBodyFormComponent.new)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,12 @@ module Settings
|
||||
include ApplicationHelper
|
||||
include OpPrimer::ComponentHelpers
|
||||
include OpTurbo::Streamable
|
||||
|
||||
def breadcrumbs_items
|
||||
[{ href: admin_index_path, text: t("label_administration") },
|
||||
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
|
||||
t("settings.project_attributes.heading")]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,11 +29,8 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
<%=
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title(variant: :medium) { t('settings.project_attributes.new.heading') }
|
||||
header.with_title { t('settings.project_attributes.new.heading') }
|
||||
header.with_description { t('settings.project_attributes.new.description') }
|
||||
header.with_parent_link(href: admin_settings_project_custom_fields_path, 'aria-label': I18n.t("button_back")) do
|
||||
t('settings.project_attributes.heading')
|
||||
end
|
||||
header.with_back_button(href: admin_settings_project_custom_fields_path, 'aria-label': t('button_back'))
|
||||
header.with_breadcrumbs(breadcrumb_items)
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
module Settings
|
||||
module ProjectCustomFields
|
||||
class NewFormHeaderComponent < ApplicationComponent
|
||||
def breadcrumb_items
|
||||
[{ href: admin_index_path, text: t("label_administration") },
|
||||
{ href: admin_settings_project_custom_fields_path, text: t("label_project_plural") },
|
||||
{ href: admin_settings_project_custom_fields_path, text: t("settings.project_attributes.heading") },
|
||||
t("settings.project_attributes.new.heading")]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+10
-17
@@ -27,23 +27,16 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<% html_title t(:label_administration), t("project_module_storages") %>
|
||||
|
||||
<% if ::Storages::Storage.any? %>
|
||||
<%= toolbar title: t("storages.page_titles.project_settings.index") do %>
|
||||
|
||||
<li class="toolbar-item">
|
||||
<%= link_to new_project_settings_project_storage_path,
|
||||
{ class: 'button -primary',
|
||||
aria: { label: t(:'storages.label_new_storage') },
|
||||
title: t(:'storages.label_new_storage') } do %>
|
||||
<%= op_icon('button--icon icon-add') %>
|
||||
<span class="button--text"><%= ::Storages::Storage.model_name.human %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
<%= component_wrapper(tag: "tr", class: row_css_class, data: { turbo: true }) do %>
|
||||
<% columns.each do |column| %>
|
||||
<td class="<%= column_css_class(column) %>">
|
||||
<%= column_value(column) %>
|
||||
</td>
|
||||
<% end %>
|
||||
|
||||
<%= render(::Storages::ProjectStorages::TableComponent.new(rows: @project_storages)) %>
|
||||
<% else %>
|
||||
<%= render partial: '/storages/project_settings/toast_no_storage_set_up' %>
|
||||
<td class="buttons">
|
||||
<% button_links.each do |link| %>
|
||||
<%= link %>
|
||||
<% end %>
|
||||
</td>
|
||||
<% end %>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Settings
|
||||
module ProjectCustomFields
|
||||
module ProjectCustomFieldMapping
|
||||
class RowComponent < Projects::RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
include OpTurbo::Streamable
|
||||
|
||||
def wrapper_uniq_by
|
||||
"project-#{project.id}"
|
||||
end
|
||||
|
||||
def more_menu_items
|
||||
@more_menu_items ||= [more_menu_detach_project].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def more_menu_detach_project
|
||||
if User.current.admin
|
||||
{
|
||||
scheme: :danger,
|
||||
icon: :trash,
|
||||
label: I18n.t(:button_delete),
|
||||
href: unlink_admin_settings_project_custom_field_path(
|
||||
id: @table.params[:custom_field].id,
|
||||
project_custom_field_project_mapping: { project_id: model.first.id }
|
||||
),
|
||||
data: { turbo_method: :delete }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%= component_wrapper(tag: "div", data: { turbo: true }) do %>
|
||||
<%= render_parent %>
|
||||
<% end %>
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2023 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Settings
|
||||
module ProjectCustomFields
|
||||
module ProjectCustomFieldMapping
|
||||
class TableComponent < Projects::TableComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
include OpTurbo::Streamable
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -59,15 +59,12 @@ module Statuses
|
||||
[
|
||||
[:name, { caption: Status.human_attribute_name(:name) }],
|
||||
[:color, { caption: Status.human_attribute_name(:color) }],
|
||||
[:done_ratio, { caption: WorkPackage.human_attribute_name(:done_ratio) }],
|
||||
[:default?, { caption: Status.human_attribute_name(:is_default) }],
|
||||
[:closed?, { caption: Status.human_attribute_name(:is_closed) }],
|
||||
[:readonly?, { caption: Status.human_attribute_name(:is_readonly) }],
|
||||
[:sort, { caption: I18n.t(:label_sort) }]
|
||||
].tap do |default|
|
||||
if WorkPackage.use_status_for_done_ratio?
|
||||
default.insert 2, [:done_ratio, { caption: WorkPackage.human_attribute_name(:done_ratio) }]
|
||||
end
|
||||
end
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<div class="generic-table--container" data-test-selector="<%= test_selector %>">
|
||||
<div class="generic-table--container <%= container_class %>" data-test-selector="<%= test_selector %>">
|
||||
<div class="generic-table--results-container">
|
||||
<table class="generic-table">
|
||||
<colgroup>
|
||||
|
||||
@@ -157,6 +157,10 @@ class TableComponent < ApplicationComponent
|
||||
self.class.row_class
|
||||
end
|
||||
|
||||
def container_class
|
||||
nil
|
||||
end
|
||||
|
||||
def columns
|
||||
self.class.columns
|
||||
end
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module WorkPackages
|
||||
module Progress
|
||||
# rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
class BaseModalComponent < ApplicationComponent
|
||||
# rubocop:enable OpenProject/AddPreviewForViewComponent
|
||||
|
||||
FIELD_MAP = {
|
||||
"estimatedTime" => :estimated_hours,
|
||||
"work_package[estimated_hours]" => :estimated_hours,
|
||||
"remainingTime" => :remaining_hours,
|
||||
"work_package[remaining_hours]" => :remaining_hours,
|
||||
"work_package[status_id]" => :status_id,
|
||||
"statusId" => :status_id
|
||||
}.freeze
|
||||
|
||||
include ApplicationHelper
|
||||
include Turbo::FramesHelper
|
||||
include OpPrimer::ComponentHelpers
|
||||
include OpenProject::StaticRouting::UrlHelpers
|
||||
|
||||
attr_reader :work_package, :mode, :focused_field, :touched_field_map
|
||||
|
||||
def initialize(work_package, focused_field: nil, touched_field_map: {})
|
||||
super()
|
||||
|
||||
@work_package = work_package
|
||||
@focused_field = map_field(focused_field)
|
||||
@touched_field_map = touched_field_map
|
||||
end
|
||||
|
||||
def submit_path
|
||||
if work_package.new_record?
|
||||
url_for(controller: "work_packages/progress",
|
||||
action: "create")
|
||||
else
|
||||
url_for(controller: "work_packages/progress",
|
||||
action: "update",
|
||||
work_package_id: work_package.id)
|
||||
end
|
||||
end
|
||||
|
||||
def learn_more_href
|
||||
OpenProject::Static::Links.links[:progress_tracking_docs][:href]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def map_field(field)
|
||||
# Scenarios when a field is not provided occur after a
|
||||
# form submission since the last focused element
|
||||
# was the submit button. In this case, don't focus on
|
||||
# an element by default.
|
||||
return nil if field.nil?
|
||||
|
||||
field = FIELD_MAP[field.to_s]
|
||||
|
||||
return field if field.present?
|
||||
|
||||
raise ArgumentError, "The selected field is not one of #{FIELD_MAP.keys.join(', ')}."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
.progress-form
|
||||
.input--readonly
|
||||
background-color: unset !important
|
||||
border: none !important
|
||||
box-shadow: none !important
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<%= turbo_frame_tag "work_package_progress_modal" do %>
|
||||
<%= primer_form_with(
|
||||
model: work_package,
|
||||
url: submit_path,
|
||||
class: "progress-form",
|
||||
id: "progress-form",
|
||||
html: { autocomplete: "off" },
|
||||
data: { "application-target": "dynamic",
|
||||
"work-packages--progress--preview-progress-target": "form",
|
||||
controller: "work-packages--progress--focus-field " \
|
||||
"work-packages--progress--preview-progress " \
|
||||
"work-packages--progress--touched-field-marker" }
|
||||
) do |f| %>
|
||||
<%= flex_layout do |modal_body| %>
|
||||
<% modal_body.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do |_fields| %>
|
||||
<%= render(WorkPackages::ProgressForm.new(f, work_package:, mode:, focused_field:, touched_field_map:)) %>
|
||||
<% end %>
|
||||
|
||||
<% modal_body.with_row(mt: 3) do |_tooltip| %>
|
||||
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { t("work_package.progress.label_note") } %>
|
||||
<%= render(Primer::Beta::Text.new) { t("work_package.progress.modal.status_based_help_text") } %>
|
||||
<%= render(Primer::Beta::Link.new(href: learn_more_href)) { t(:label_learn_more) } %>
|
||||
<% end %>
|
||||
|
||||
<% modal_body.with_row(mt: 3) do |_actions_row| %>
|
||||
<%= flex_layout(justify_content: :flex_end) do |action_buttons| %>
|
||||
<%= action_buttons.with_column do %>
|
||||
<%= render(Primer::Beta::Button.new(scheme: :primary,
|
||||
type: :submit)) { t(:button_save) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2010-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module WorkPackages
|
||||
module Progress
|
||||
module StatusBased
|
||||
class ModalBodyComponent < BaseModalComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
def initialize(work_package, focused_field: nil, touched_field_map: {})
|
||||
super
|
||||
|
||||
@mode = :status_based
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
<%= turbo_frame_tag "work_package_progress_modal" do %>
|
||||
<%= primer_form_with(
|
||||
model: work_package,
|
||||
url: submit_path,
|
||||
class: "progress-form",
|
||||
id: "progress-form",
|
||||
html: { autocomplete: "off" },
|
||||
data: { "application-target": "dynamic",
|
||||
"work-packages--progress--preview-progress-target": "form",
|
||||
controller: "work-packages--progress--focus-field " \
|
||||
"work-packages--progress--preview-progress " \
|
||||
"work-packages--progress--touched-field-marker" }
|
||||
) do |f| %>
|
||||
<%= flex_layout do |modal_body| %>
|
||||
<% modal_body.with_row(classes: "FormControl-horizontalGroup--sm-vertical") do |_fields| %>
|
||||
<%= render(WorkPackages::ProgressForm.new(f, work_package:, mode:, focused_field:, touched_field_map:)) %>
|
||||
<% end %>
|
||||
|
||||
<% if should_display_migration_warning? %>
|
||||
<% modal_body.with_row(mt: 3) do |_migration_warning| %>
|
||||
<%= render(Primer::Alpha::Banner.new) { t("work_package.progress.modal.migration_warning_text") } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% modal_body.with_row(mt: 3) do |_tooltip| %>
|
||||
<%= render(Primer::Beta::Text.new(font_weight: :semibold)) { t("work_package.progress.label_note") } %>
|
||||
<%= render(Primer::Beta::Text.new) { t("work_package.progress.modal.work_based_help_text") } %>
|
||||
<%= render(Primer::Beta::Link.new(href: learn_more_href)) { t(:label_learn_more) } %>
|
||||
<% end %>
|
||||
|
||||
<% modal_body.with_row(mt: 3) do |_actions_row| %>
|
||||
<%= flex_layout(justify_content: :flex_end) do |action_buttons| %>
|
||||
<%= action_buttons.with_column do %>
|
||||
<%= render(Primer::Beta::Button.new(scheme: :primary,
|
||||
type: :submit)) { t(:button_save) } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2010-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module WorkPackages
|
||||
module Progress
|
||||
module WorkBased
|
||||
# rubocop:disable OpenProject/AddPreviewForViewComponent
|
||||
class ModalBodyComponent < BaseModalComponent
|
||||
def initialize(work_package, focused_field: nil, touched_field_map: {})
|
||||
super
|
||||
|
||||
@mode = :work_based
|
||||
end
|
||||
|
||||
def should_display_migration_warning?
|
||||
work_package.done_ratio.present? && work_package.estimated_hours.nil? && work_package.remaining_hours.nil?
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable OpenProject/AddPreviewForViewComponent
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@
|
||||
data: { 'test-selector': 'op-share-wp-active-list',
|
||||
controller: 'work-packages--share--bulk-selection',
|
||||
application_target: 'dynamic' }) do
|
||||
render(Primer::Beta::BorderBox.new(list_id: insert_target_modifier_id)) do |border_box|
|
||||
render(border_box_container(list_id: insert_target_modifier_id)) do |border_box|
|
||||
border_box.with_header(color: :muted, data: { 'test-selector': 'op-share-wp-header' }) do
|
||||
grid_layout('op-share-wp-modal-body--header', tag: :div, align_items: :center) do |header_grid|
|
||||
header_grid.with_area(:counter, tag: :div) do
|
||||
|
||||
@@ -72,7 +72,7 @@ module WorkPackages
|
||||
end
|
||||
|
||||
def permission_name(value)
|
||||
options.select { |option| option[:value] == value }
|
||||
options.find { |option| option[:value] == value }[:label]
|
||||
end
|
||||
|
||||
def form_inputs(role_id)
|
||||
|
||||
@@ -59,8 +59,10 @@ class DeleteContract < ModelContract
|
||||
user.admin? && user.active?
|
||||
when Proc
|
||||
instance_exec(&permission)
|
||||
when Symbol
|
||||
model.project && user.allowed_in_project?(permission, model.project)
|
||||
else
|
||||
!model.project || user.allowed_in_project?(permission, model.project)
|
||||
raise ArgumentError, "#{self.class} used without delete_permission. Set a Proc, or project-based permission symbol"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ module Members
|
||||
private
|
||||
|
||||
def member_is_deletable
|
||||
errors.add(:base, :not_deletable) unless model.deletable?
|
||||
errors.add(:base, :not_deletable) unless model.some_roles_deletable?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module ProjectCustomFieldProjectMappings
|
||||
class DeleteContract < ::DeleteContract
|
||||
delete_permission :select_project_custom_fields
|
||||
end
|
||||
end
|
||||
@@ -35,6 +35,7 @@ module Projects
|
||||
attribute :identifier
|
||||
attribute :description
|
||||
attribute :public
|
||||
attribute :settings
|
||||
attribute :active do
|
||||
validate_active_present
|
||||
validate_changing_active
|
||||
|
||||
@@ -41,17 +41,36 @@ module Queries::Projects::ProjectQueries
|
||||
presence: true,
|
||||
length: { maximum: 255 }
|
||||
|
||||
validate :user_is_current_user_and_logged_in
|
||||
validate :name_select_included
|
||||
validate :existing_selects
|
||||
validate :user_is_logged_in
|
||||
validate :allowed_to_modify_private_query
|
||||
validate :allowed_to_modify_public_query
|
||||
|
||||
protected
|
||||
|
||||
def user_is_current_user_and_logged_in
|
||||
unless user.logged? && user == model.user
|
||||
def user_is_logged_in
|
||||
unless user.logged?
|
||||
errors.add :base, :error_unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_to_modify_private_query
|
||||
return if model.public?
|
||||
|
||||
if model.user != user
|
||||
errors.add :base, :can_only_be_modified_by_owner
|
||||
end
|
||||
end
|
||||
|
||||
def allowed_to_modify_public_query
|
||||
return unless model.public?
|
||||
|
||||
unless user.allowed_globally?(:manage_public_project_queries)
|
||||
errors.add :base, :need_permission_to_modify_public_query
|
||||
end
|
||||
end
|
||||
|
||||
def name_select_included
|
||||
if model.selects.none? { |s| s.attribute == :name }
|
||||
errors.add :selects, :name_not_included
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Queries::Projects::ProjectQueries
|
||||
class PublishContract < BaseContract
|
||||
attribute :public
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2010-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
module Queries::Projects::ProjectQueries
|
||||
class UpdateContract < BaseContract; end
|
||||
end
|
||||
@@ -29,7 +29,7 @@
|
||||
module Relations
|
||||
class BaseContract < ::ModelContract
|
||||
attribute :relation_type
|
||||
attribute :delay
|
||||
attribute :lag
|
||||
attribute :description
|
||||
attribute :from
|
||||
attribute :to
|
||||
|
||||
@@ -35,7 +35,7 @@ module WorkPackageMembers
|
||||
private
|
||||
|
||||
def member_is_deletable
|
||||
errors.add(:base, :not_deletable) unless model.deletable?
|
||||
errors.add(:base, :not_deletable) unless model.some_roles_deletable?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,17 +53,20 @@ module WorkPackages
|
||||
attribute :project_id
|
||||
|
||||
attribute :done_ratio,
|
||||
writable: ->(*) do
|
||||
Setting.work_package_done_ratio == 'field'
|
||||
end
|
||||
writable: false
|
||||
attribute :derived_done_ratio,
|
||||
writable: false
|
||||
|
||||
attribute :estimated_hours
|
||||
attribute :estimated_hours do
|
||||
validate_work_is_set_when_remaining_work_is_set
|
||||
end
|
||||
attribute :derived_estimated_hours,
|
||||
writable: false
|
||||
|
||||
attribute :remaining_hours
|
||||
attribute :remaining_hours do
|
||||
validate_remaining_work_is_lower_than_work
|
||||
validate_remaining_work_is_set_when_work_is_set
|
||||
end
|
||||
attribute :derived_remaining_hours,
|
||||
writable: false
|
||||
|
||||
@@ -74,7 +77,7 @@ module WorkPackages
|
||||
next unless model.project
|
||||
|
||||
validate_people_visible :assigned_to,
|
||||
'assigned_to_id',
|
||||
"assigned_to_id",
|
||||
assignable_assignees
|
||||
end
|
||||
|
||||
@@ -82,7 +85,7 @@ module WorkPackages
|
||||
next unless model.project
|
||||
|
||||
validate_people_visible :responsible,
|
||||
'responsible_id',
|
||||
"responsible_id",
|
||||
assignable_responsibles
|
||||
end
|
||||
|
||||
@@ -223,7 +226,7 @@ module WorkPackages
|
||||
|
||||
def validate_after_soonest_start(date_attribute)
|
||||
if !model.schedule_manually? && before_soonest_start?(date_attribute)
|
||||
message = I18n.t('activerecord.errors.models.work_package.attributes.start_date.violates_relationships',
|
||||
message = I18n.t("activerecord.errors.models.work_package.attributes.start_date.violates_relationships",
|
||||
soonest_start: model.soonest_start)
|
||||
|
||||
errors.add date_attribute, message, error_symbol: :violates_relationships
|
||||
@@ -320,6 +323,42 @@ module WorkPackages
|
||||
end
|
||||
end
|
||||
|
||||
def validate_remaining_work_is_lower_than_work
|
||||
if work_set? && remaining_work_set? && remaining_work_exceeds_work?
|
||||
if model.changed.include?("estimated_hours")
|
||||
errors.add(:estimated_hours, :cant_be_inferior_to_remaining_work)
|
||||
end
|
||||
|
||||
if model.changed.include?("remaining_hours")
|
||||
errors.add(:remaining_hours, :cant_exceed_work)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_remaining_work_is_set_when_work_is_set
|
||||
if work_set? && !remaining_work_set?
|
||||
errors.add(:remaining_hours, :must_be_set_when_work_is_set)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_work_is_set_when_remaining_work_is_set
|
||||
if remaining_work_set? && !work_set?
|
||||
errors.add(:estimated_hours, :must_be_set_when_remaining_work_is_set)
|
||||
end
|
||||
end
|
||||
|
||||
def work_set?
|
||||
model.estimated_hours.present?
|
||||
end
|
||||
|
||||
def remaining_work_set?
|
||||
model.remaining_hours.present?
|
||||
end
|
||||
|
||||
def remaining_work_exceeds_work?
|
||||
model.remaining_hours > model.estimated_hours
|
||||
end
|
||||
|
||||
def validate_no_reopen_on_closed_version
|
||||
if model.version_id && model.reopened? && model.version.closed?
|
||||
errors.add :base, I18n.t(:error_can_not_reopen_work_package_on_closed_version)
|
||||
@@ -333,7 +372,7 @@ module WorkPackages
|
||||
|
||||
unless principal_visible?(id, list)
|
||||
errors.add attribute,
|
||||
I18n.t('api_v3.errors.validation.invalid_user_assigned_to_work_package',
|
||||
I18n.t("api_v3.errors.validation.invalid_user_assigned_to_work_package",
|
||||
property: I18n.t("attributes.#{attribute}"))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) 2012-2024 the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "work_packages/create_contract"
|
||||
|
||||
module WorkPackages
|
||||
class CopyContract < CreateContract
|
||||
# As % Complete can be set while Work and Remaining work are not, copying is
|
||||
# a scenario where this field must be writable
|
||||
attribute :done_ratio,
|
||||
writable: true
|
||||
end
|
||||
end
|
||||
@@ -26,14 +26,14 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "work_packages/create_contract"
|
||||
require "work_packages/copy_contract"
|
||||
|
||||
# Can be used to copy all of a project's work packages. As the
|
||||
# work packages can be old, some of the validations that would
|
||||
# apply to newly created work packages need not apply there, e.g
|
||||
# on copying, it is ok for work packages to have closed versions
|
||||
module WorkPackages
|
||||
class CopyProjectContract < CreateContract
|
||||
class CopyProjectContract < CopyContract
|
||||
include WorkPackages::SkipAuthorizationChecks
|
||||
|
||||
# let the contract be used in error messages
|
||||
|
||||
@@ -35,7 +35,7 @@ module Admin
|
||||
|
||||
before_action :find_attachment, only: %i[destroy]
|
||||
|
||||
menu_item :attachment_quarantine
|
||||
menu_item :attachments
|
||||
|
||||
def index; end
|
||||
|
||||
@@ -51,12 +51,10 @@ module Admin
|
||||
redirect_to action: :index
|
||||
end
|
||||
|
||||
def default_breadcrumb
|
||||
t("antivirus_scan.quarantined_attachments.title")
|
||||
end
|
||||
def default_breadcrumb; end
|
||||
|
||||
def show_local_breadcrumb
|
||||
true
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user