Merge remote-tracking branch 'opf/dev' into HEAD

# Conflicts:
#	modules/backlogs/config/locales/crowdin/cs.yml
#	modules/backlogs/config/locales/crowdin/es.yml
#	modules/backlogs/config/locales/crowdin/it.yml
#	modules/backlogs/config/locales/crowdin/ko.yml
#	modules/backlogs/config/locales/crowdin/pl.yml
#	modules/backlogs/config/locales/crowdin/pt-BR.yml
#	modules/backlogs/config/locales/crowdin/ro.yml
#	modules/backlogs/config/locales/crowdin/zh-CN.yml
#	modules/backlogs/spec/features/inbox_column_spec.rb
#	modules/backlogs/spec/support/pages/backlog.rb
#	modules/resource_management/config/locales/crowdin/es.yml
#	modules/resource_management/config/locales/crowdin/it.yml
#	modules/resource_management/config/locales/crowdin/ko.yml
#	modules/resource_management/config/locales/crowdin/pl.yml
#	modules/resource_management/config/locales/crowdin/zh-CN.yml
#	modules/wikis/config/locales/crowdin/es.yml
#	modules/wikis/config/locales/crowdin/it.yml
#	modules/wikis/config/locales/crowdin/ko.yml
#	modules/wikis/config/locales/crowdin/pl.yml
#	modules/wikis/config/locales/crowdin/pt-BR.yml
#	modules/wikis/config/locales/crowdin/zh-CN.yml
This commit is contained in:
Alexander Brandon Coles
2026-06-04 09:44:51 +02:00
1516 changed files with 41527 additions and 54452 deletions
+48 -2
View File
@@ -13,16 +13,40 @@ updates:
groups:
angular:
patterns:
- '@angular*'
- '@angular/*'
- '@angular-devkit/*'
- '@angular-builders/*'
angular-eslint:
patterns:
- 'angular-eslint'
- '@angular-eslint/*'
blocknote:
patterns:
- '@blocknote/*'
fullcalendar:
patterns:
- '@fullcalendar/*'
eslint:
patterns:
- 'eslint'
- 'eslint-plugin-*'
- '@eslint/*'
- '@stylistic/eslint-plugin'
- 'globals'
html-eslint:
patterns:
- '@html-eslint/*'
hotwired:
patterns:
- '@hotwired/turbo'
- '@hotwired/turbo-rails'
jquery:
patterns:
- 'jquery'
- 'jquery-migrate'
mantine:
patterns:
- '@mantine/*'
ng-select:
patterns:
- '@ng-select/*'
@@ -35,8 +59,30 @@ updates:
- 'react-dom'
- '@types/react'
- '@types/react-dom'
testing-library:
patterns:
- '@testing-library/*'
typescript-eslint:
patterns:
- 'typescript-eslint'
- '@typescript-eslint/*'
uirouter:
patterns:
- '@uirouter/*'
vitest:
patterns:
- 'vitest'
- '@vitest/*'
ignore:
- dependency-name: "@angular*"
- dependency-name: "@angular/*"
update-types: ["version-update:semver-major"]
- dependency-name: "@angular-builders/*"
update-types: ["version-update:semver-major"]
- dependency-name: "@angular-devkit/*"
update-types: ["version-update:semver-major"]
- dependency-name: "angular-eslint"
update-types: ["version-update:semver-major"]
- dependency-name: "@angular-eslint/*"
update-types: ["version-update:semver-major"]
- dependency-name: "@openproject/octicons"
- dependency-name: "@openproject/primer-view-components"
+2 -2
View File
@@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
- name: Setup Brakeman
run: |
@@ -46,6 +46,6 @@ jobs:
--output output.sarif.json
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with:
sarif_file: output.sarif.json
+2 -2
View File
@@ -36,7 +36,7 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with:
config-file: ./.github/codeql/config.yml
languages: ${{ matrix.language }}
@@ -44,6 +44,6 @@ jobs:
queries: security-extended,security-and-quality
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
with:
category: "/language:${{matrix.language}}"
@@ -10,40 +10,28 @@ jobs:
if: github.repository == 'opf/openproject'
runs-on: ubuntu-latest
outputs:
previous_release_branch: ${{ steps.find_previous_release.outputs.branch }}
latest_release_branch: ${{ steps.find_latest_release.outputs.branch }}
previous_release_branch: ${{ steps.find_release_branches.outputs.previous_branch }}
latest_release_branch: ${{ steps.find_release_branches.outputs.latest_branch }}
steps:
- id: find_previous_release
- id: find_release_branches
env:
GITHUB_TOKEN: ${{ secrets.OPENPROJECTCI_GH_CORE_PAT }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$GITHUB_REPOSITORY/branches?protected=true | \
jq -r '.[].name' | grep '^release/' | sort --version-sort | tail -2 | head -1
LATEST_BRANCHES=$(gh api --paginate \
"repos/$GITHUB_REPOSITORY/branches?protected=true&per_page=100" \
--jq '.[].name' | grep '^release/' | sort --version-sort | tail -2
)
if [ "$BRANCH" = "" ]; then
echo "Invalid release branch found: $BRANCH"
LATEST_BRANCH=$(echo "$LATEST_BRANCHES" | tail -1)
PREVIOUS_BRANCH=$(echo "$LATEST_BRANCHES" | head -1)
if [ -z "$LATEST_BRANCH" ] || [ -z "$PREVIOUS_BRANCH" ]; then
echo "Invalid release branches found: latest=$LATEST_BRANCH, previous=$PREVIOUS_BRANCH"
exit 1
fi
echo "Found previous release branch: $BRANCH"
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
- id: find_latest_release
env:
GITHUB_TOKEN: ${{ secrets.OPENPROJECTCI_GH_CORE_PAT }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$GITHUB_REPOSITORY/branches?protected=true | \
jq -r '.[].name' | grep '^release/' | sort --version-sort | tail -1
)
if [ "$BRANCH" = "" ]; then
echo "Invalid release branch found: $BRANCH"
exit 1
fi
echo "Found current release branch: $BRANCH"
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
echo "Found previous release branch: $PREVIOUS_BRANCH"
echo "previous_branch=${PREVIOUS_BRANCH}" >> $GITHUB_OUTPUT
echo "Found latest release branch: $LATEST_BRANCH"
echo "latest_branch=${LATEST_BRANCH}" >> $GITHUB_OUTPUT
merge-or-create-pr:
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref_name == needs.setup.outputs.previous_release_branch)
env:
@@ -20,9 +20,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.OPENPROJECTCI_GH_CORE_PAT }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$GITHUB_REPOSITORY/branches?protected=true | \
jq -r '.[].name' | grep '^release/' | sort --version-sort | tail -1
BRANCH=$(gh api --paginate \
"repos/$GITHUB_REPOSITORY/branches?protected=true&per_page=100" --jq '.[].name' | \
grep '^release/' | sort --version-sort | tail -1
)
if [ "$BRANCH" = "" ]; then
echo "Invalid release branch found: $BRANCH"
+4 -4
View File
@@ -17,9 +17,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
BRANCH=$(curl -H "Authorization: token $GITHUB_TOKEN" \
https://api.github.com/repos/$GITHUB_REPOSITORY/branches?protected=true | \
jq -r '.[].name' | grep '^release/' | sort --version-sort | tail -1
BRANCH=$(gh api --paginate \
"repos/$GITHUB_REPOSITORY/branches?protected=true&per_page=100" --jq '.[].name' | \
grep '^release/' | sort --version-sort | tail -1
)
if [ "$BRANCH" = "" ]; then
echo "Invalid release branch found: $BRANCH"
@@ -46,7 +46,7 @@ jobs:
with:
ref: ${{ matrix.branch }}
fetch-depth: 1
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true
- name: "Set crowdin branch name"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
ruby-version: .ruby-version
bundler-cache: true
+3 -3
View File
@@ -106,7 +106,7 @@ jobs:
STEPS_EXTRACT_VERSION_OUTPUTS_DOCKER_TAGS: ${{ steps.extract_version.outputs.docker_tags }}
STEPS_EXTRACT_VERSION_OUTPUTS_REGISTRY_IMAGE: ${{ steps.extract_version.outputs.registry_image }}
- name: Cache NPM
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: |
frontend/node_modules
@@ -114,12 +114,12 @@ jobs:
key: nodejs-x64-${{ hashFiles('**/package-lock.json') }}
restore-keys: nodejs-x64-
- name: Cache angular
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: frontend/.angular
key: angular-${{ github.ref }}
restore-keys: angular-
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true
- run: bundle exec ./script/docs/check_links
@@ -39,7 +39,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true
- run: script/docs/check_readme_yaml_header_syntax
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send mail
uses: dawidd6/action-send-mail@d38f3f7cd391cdebfe0d38efc3998b935e951c4f # v16
uses: dawidd6/action-send-mail@42942bc2f8fba4e611b459a018967a6a7c78c68c # v17
with:
subject: ${{ inputs.subject }}
body: ${{ inputs.body }}
+2 -2
View File
@@ -161,7 +161,7 @@ jobs:
- name: Deploy EDGE
if: github.ref == 'refs/heads/dev' && github.repository == 'opf/openproject'
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26
with:
workflow: edge-deploy-shards.yml
repo: opf/saas-deploy
@@ -172,7 +172,7 @@ jobs:
- name: Deploy STAGE
# make sure to always use the latest release branch here
if: github.ref == 'refs/heads/release/17.5' && github.repository == 'opf/openproject'
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26
with:
workflow: stage-deploy-shards.yml
repo: opf/saas-deploy
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
- name: Setup i18n-tasks
run: |
+224
View File
@@ -0,0 +1,224 @@
name: npm audit fix
on:
workflow_dispatch:
schedule:
- cron: "23 4 * * 1"
env:
BASE_BRANCH: dev
BRANCH_PREFIX: npm-audit-fix
permissions: {}
concurrency:
group: npm-audit-fix
cancel-in-progress: false
jobs:
audit-fix:
name: npm audit fix
runs-on: ubuntu-latest
timeout-minutes: 15
if: github.repository == 'opf/openproject' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
permissions:
contents: write # for git push
pull-requests: write # for creating pull requests
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ env.BASE_BRANCH }}
token: ${{ secrets.OPENPROJECTCI_GH_CORE_PAT }}
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '22.21'
package-manager-cache: false
- name: Required git config
run: |
git config user.name "OpenProject Actions CI"
git config user.email "operations+ci@openproject.com"
- name: Run npm audit fix
id: audit_fix
run: |
set -euo pipefail
mkdir -p reports
print_audit_report() {
local report="$1"
local label="$2"
node - "$report" "$label" <<'NODE'
const fs = require('fs');
const reportPath = process.argv[2];
const label = process.argv[3];
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const vulnerabilities = Object.entries(report.vulnerabilities || {});
const counts = report.metadata?.vulnerabilities || {};
console.log(`${label}: ${vulnerabilities.length} vulnerable package(s)`);
console.log(`severity counts: ${JSON.stringify(counts)}`);
for (const [name, details] of vulnerabilities) {
const via = (details.via || [])
.map((finding) => typeof finding === 'string' ? finding : finding.title)
.filter(Boolean)
.join('; ');
console.log([
`- ${name}`,
`severity: ${details.severity}`,
`range: ${details.range || 'n/a'}`,
`fixAvailable: ${JSON.stringify(details.fixAvailable)}`,
via ? `via: ${via}` : null
].filter(Boolean).join(' | '));
}
NODE
}
run_for_package() {
local name="$1"
local directory="$2"
local before_report="$GITHUB_WORKSPACE/reports/npm-audit-${name}-before.json"
local after_report="$GITHUB_WORKSPACE/reports/npm-audit-${name}-after.json"
echo "::group::npm audit before fix (${name})"
set +e
npm --prefix "$directory" audit --audit-level=high --json > "$before_report"
audit_before_exit=$?
set -e
print_audit_report "$before_report" "${name} before fix"
echo "npm audit before fix exited with ${audit_before_exit}"
echo "::endgroup::"
echo "::group::npm audit fix (${name})"
set +e
npm --prefix "$directory" audit fix --package-lock-only --ignore-scripts --audit-level=high
audit_fix_exit=$?
set -e
echo "npm audit fix exited with ${audit_fix_exit}"
echo "::endgroup::"
echo "::group::npm audit after fix (${name})"
set +e
npm --prefix "$directory" audit --audit-level=high --json > "$after_report"
audit_after_exit=$?
set -e
print_audit_report "$after_report" "${name} after fix"
echo "npm audit after fix exited with ${audit_after_exit}"
echo "::endgroup::"
}
run_for_package root .
run_for_package frontend frontend
if git diff --quiet -- package.json package-lock.json frontend/package.json frontend/package-lock.json; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "npm audit fix produced no package or lockfile changes."
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "npm audit fix produced package or lockfile changes."
fi
- name: Show diff
if: steps.audit_fix.outputs.has_changes == 'true'
run: git diff -- package.json package-lock.json frontend/package.json frontend/package-lock.json
- name: Create pull request
if: steps.audit_fix.outputs.has_changes == 'true'
env:
GITHUB_TOKEN: ${{ secrets.OPENPROJECTCI_GH_CORE_PAT }}
run: |
set -euo pipefail
pr_numbers=$(gh pr list \
--base "$BASE_BRANCH" \
--state open \
--limit 100 \
--json number,headRefName \
--jq '.[] | select(.headRefName | startswith("'"$BRANCH_PREFIX"'-")) | .number')
for pr_number in $pr_numbers; do
gh pr close "$pr_number" --delete-branch
done
pr_body_file=$(mktemp)
{
echo 'Created by GitHub action.'
echo
echo '## Impacted packages'
} > "$pr_body_file"
node <<'NODE' >> "$pr_body_file"
const fs = require('fs');
const reports = [
['root', 'reports/npm-audit-root-before.json'],
['frontend', 'reports/npm-audit-frontend-before.json']
];
let foundPackages = false;
for (const [label, reportPath] of reports) {
if (!fs.existsSync(reportPath)) {
continue;
}
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const vulnerabilities = Object.entries(report.vulnerabilities || {})
.sort(([left], [right]) => left.localeCompare(right));
if (vulnerabilities.length === 0) {
continue;
}
foundPackages = true;
console.log('');
console.log(`### ${label}`);
for (const [name, details] of vulnerabilities) {
console.log(`- \`${name}\` (${details.severity})`);
}
}
if (!foundPackages) {
console.log('');
console.log('No vulnerable packages were reported before the fix.');
}
NODE
{
for pr_number in $pr_numbers; do
if [ "$pr_number" = "$(echo "$pr_numbers" | head -1)" ]; then
echo
echo '## Replaced PRs'
echo
fi
echo "Replaces #$pr_number"
done
} >> "$pr_body_file"
temp_branch="$BRANCH_PREFIX-$(date "+%Y%m%d%H%M%S")"
git switch -c "$temp_branch"
git add package.json package-lock.json frontend/package.json frontend/package-lock.json
git commit -m "Run npm audit fix"
gh auth setup-git
git push origin "$temp_branch"
gh pr create \
--base "$BASE_BRANCH" \
--head "$temp_branch" \
--title "Run npm audit fix" \
--body-file "$pr_body_file"
echo "Created a PR with npm audit fixes (${temp_branch}) against ${BASE_BRANCH}"
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
- uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
- name: Run Rubocop
uses: reviewdog/action-rubocop@b6d5e953a5fc0bf3ab65254e77730ea2174d6d6d # v2
with:
+1 -1
View File
@@ -98,7 +98,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libpq-dev
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
with:
bundler-cache: true
+5 -5
View File
@@ -38,7 +38,7 @@ jobs:
persist-credentials: false
- name: Cache DOCKER
id: cache_docker
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: cache/docker
# Note: no restore keys since whenever the files below change, we want to rebuild the full image from scratch
@@ -47,28 +47,28 @@ jobs:
if: steps.cache_docker.outputs.cache-hit == 'true'
run: docker load -i cache/docker/image.tar
- name: Cache GEM
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: cache/bundle
key: gem-trixie-${{ hashFiles('.ruby-version') }}-${{ hashFiles('Gemfile.lock') }}
restore-keys: |
gem-trixie-${{ hashFiles('.ruby-version') }}-
- name: Cache NPM
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: cache/node
key: node-${{ hashFiles('package.json', 'frontend/package-lock.json') }}
restore-keys: |
node-
- name: Cache ANGULAR
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: cache/angular
key: angular-${{ hashFiles('package.json', 'frontend/package-lock.json') }}
restore-keys: |
angular-
- name: Cache TEST RUNTIME
uses: runs-on/cache@bd330c5a5f6cbb837823ee25864f3c71a211c2e3 # v5
uses: runs-on/cache@88d90644011a3a9957fd141a106f5a94f9794203 # v5
with:
path: cache/runtime-logs
key: runtime-logs-${{ github.head_ref || github.ref }}-${{ github.sha }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1
- name: Verify linked version matches core version
id: version-check
+34
View File
@@ -0,0 +1,34 @@
name: yamllint
on:
pull_request:
paths:
- '.yamllint.yml'
- 'config/locales/en.yml'
- 'config/locales/js-en.yml'
- 'modules/*/config/locales/en.yml'
- 'modules/*/config/locales/js-en.yml'
permissions: {}
jobs:
rubocop:
name: yamllint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run Yamllint
uses: reviewdog/action-yamllint@f01d8a48fd8d89f89895499fca2cff09f9e9e8c0 # v1.21.0
with:
github_token: ${{ secrets.github_token }}
fail_level: error
yamllint_flags: >
.yamllint.yml
config/locales/en.yml
config/locales/js-en.yml
modules/*/config/locales/en.yml
modules/*/config/locales/js-en.yml
+1 -1
View File
@@ -27,4 +27,4 @@ jobs:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
+8
View File
@@ -0,0 +1,8 @@
---
extends: default
rules:
comments:
require-starting-space: false
key-ordering: {}
line-length: disable
+9 -8
View File
@@ -162,7 +162,7 @@ gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues
# prawn implicitly depends on matrix gem no longer in ruby core with 3.1
gem "matrix", "~> 0.4.3"
gem "mcp", "~> 0.14.0"
gem "mcp", "~> 0.16.0"
gem "meta-tags", "~> 2.23.0"
@@ -200,7 +200,7 @@ gem "rack-timeout", "~> 0.7.0", require: "rack/timeout/base"
gem "nokogiri", "~> 1.19.2"
gem "carrierwave", "~> 2.2.6"
gem "carrierwave", "~> 2.2.7"
gem "carrierwave_direct", "~> 3.0.0"
gem "fog-aws"
gem "ssrf_filter", "~> 1.3"
@@ -238,10 +238,10 @@ gem "yabeda-rails"
# opentelemetry
gem "opentelemetry-exporter-otlp", "~> 0.34.0", require: false
gem "opentelemetry-instrumentation-all", "~> 0.93.0", require: false
gem "opentelemetry-instrumentation-all", "~> 0.94.0", require: false
gem "opentelemetry-sdk", "~> 1.10", require: false
gem "view_component", "~> 4.10.0"
gem "view_component", "~> 4.11.0"
# Lookbook
gem "lookbook", "2.3.14"
@@ -265,11 +265,10 @@ group :test do
gem "rack-test", "~> 2.2.0"
gem "shoulda-context", "~> 2.0"
gem "parallel_tests", "~> 5.7"
# Test prof provides factories from code
# and other niceties
gem "test-prof", "~> 1.6.0"
gem "turbo_tests", github: "opf/turbo_tests", ref: "2_2_5_with_patches"
gem "turbo_tests", github: "opf/turbo_tests", ref: "with-patches"
gem "rack_session_access"
gem "rspec", "~> 3.13.2"
@@ -296,7 +295,7 @@ group :test do
gem "rails-controller-testing", "~> 1.0.2"
gem "capybara", "~> 3.40.0"
gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", tag: "v0.15.0"
gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", tag: "v0.16.0"
gem "capybara-screenshot", "~> 1.0.17"
gem "cuprite", "~> 0.17.0"
gem "rspec-wait"
@@ -318,6 +317,8 @@ group :test do
gem "equivalent-xml", "~> 0.6"
gem "json_spec", "~> 1.1.4"
gem "shoulda-matchers", "~> 7.0", require: nil
gem "parallel_tests", "~> 4.0"
end
group :ldap do
@@ -366,7 +367,7 @@ group :development, :test do
gem "rubocop-factory_bot", require: false
gem "rubocop-openproject", require: false
gem "rubocop-performance", require: false
gem "rubocop-rails", "~> 2.35.1"
gem "rubocop-rails", "~> 2.35.2"
gem "rubocop-rspec", require: false
gem "rubocop-rspec_rails", require: false
+102 -102
View File
@@ -1,7 +1,7 @@
GIT
remote: https://github.com/citizensadvice/capybara_accessible_selectors
revision: 5b9ce7840d04270e99f4f0cb03989e05437326a6
tag: v0.15.0
revision: 568699fc71b6648e7186a4ac77bba072447c131e
tag: v0.16.0
specs:
capybara_accessible_selectors (0.15.0)
capybara (~> 3.36)
@@ -54,11 +54,11 @@ GIT
GIT
remote: https://github.com/opf/turbo_tests.git
revision: 4208e7372a7987c972dda634eb4f29ad9f1448e1
ref: 2_2_5_with_patches
revision: c1c4707f536a5642a168650d273d714dfb62d842
ref: with-patches
specs:
turbo_tests (2.2.5)
parallel_tests (>= 3.3.0, < 6)
turbo_tests (2.2.0)
parallel_tests (>= 3.3.0, < 5)
rspec (>= 3.10)
GIT
@@ -217,7 +217,7 @@ PATH
remote: modules/two_factor_authentication
specs:
openproject-two_factor_authentication (1.0.0)
aws-sdk-sns (>= 1.101, < 1.114)
aws-sdk-sns (~> 1.116)
messagebird-rest (>= 1.4.2, < 5.1.0)
rotp (~> 6.1)
webauthn (~> 3.0)
@@ -361,8 +361,8 @@ GEM
awesome_nested_set (3.9.0)
activerecord (>= 4.0.0, < 8.2)
aws-eventstream (1.4.0)
aws-partitions (1.1249.0)
aws-sdk-core (3.247.0)
aws-partitions (1.1255.0)
aws-sdk-core (3.250.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -370,15 +370,15 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.125.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sdk-kms (1.128.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.222.0)
aws-sdk-core (~> 3, >= 3.247.0)
aws-sdk-s3 (1.224.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-sns (1.113.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-sns (1.116.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
@@ -425,7 +425,7 @@ GEM
capybara-screenshot (1.0.27)
capybara (>= 1.0, < 4)
launchy
carrierwave (2.2.6)
carrierwave (2.2.7)
activemodel (>= 5.0.0)
activesupport (>= 5.0.0)
addressable (~> 2.6)
@@ -479,7 +479,7 @@ GEM
capybara (~> 3.0)
ferrum (~> 0.17.0)
daemons (1.4.1)
dalli (5.0.4)
dalli (5.0.5)
logger
date (3.5.1)
date_validator (0.12.0)
@@ -497,7 +497,7 @@ GEM
disposable (0.6.3)
declarative (>= 0.0.9, < 1.0.0)
representable (>= 3.1.1, < 4)
doorkeeper (5.9.0)
doorkeeper (5.9.1)
railties (>= 5)
dotenv (3.2.0)
dotenv-rails (3.2.0)
@@ -587,7 +587,7 @@ GEM
logger
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
faraday-net_http (3.4.3)
net-http (~> 0.5)
ferrum (0.17.2)
addressable (~> 2.5)
@@ -625,13 +625,13 @@ GEM
friendly_id (5.7.0)
activerecord (>= 4.0.0)
front_matter_parser (1.0.1)
fugit (1.12.1)
fugit (1.12.2)
et-orbi (~> 1.4)
raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
glob (0.4.0)
glob (0.5.0)
globalid (1.3.0)
activesupport (>= 6.1)
good_job (4.18.2)
@@ -649,34 +649,34 @@ GEM
mini_mime (~> 1.1)
representable (~> 3.0)
retriable (~> 3.1)
google-apis-gmail_v1 (0.49.0)
google-apis-gmail_v1 (0.51.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-env (2.3.1)
base64 (~> 0.2)
faraday (>= 1.0, < 3.a)
google-logging-utils (0.2.0)
google-protobuf (4.34.1)
google-protobuf (4.35.0)
bigdecimal
rake (~> 13.3)
google-protobuf (4.34.1-aarch64-linux-gnu)
google-protobuf (4.35.0-aarch64-linux-gnu)
bigdecimal
rake (~> 13.3)
google-protobuf (4.34.1-aarch64-linux-musl)
google-protobuf (4.35.0-aarch64-linux-musl)
bigdecimal
rake (~> 13.3)
google-protobuf (4.34.1-arm64-darwin)
google-protobuf (4.35.0-arm64-darwin)
bigdecimal
rake (~> 13.3)
google-protobuf (4.34.1-x86_64-darwin)
google-protobuf (4.35.0-x86_64-darwin)
bigdecimal
rake (~> 13.3)
google-protobuf (4.34.1-x86_64-linux-gnu)
google-protobuf (4.35.0-x86_64-linux-gnu)
bigdecimal
rake (~> 13.3)
google-protobuf (4.34.1-x86_64-linux-musl)
google-protobuf (4.35.0-x86_64-linux-musl)
bigdecimal
rake (~> 13.3)
googleapis-common-protos-types (1.22.0)
googleapis-common-protos-types (1.23.0)
google-protobuf (~> 4.26)
googleauth (1.16.2)
faraday (>= 1.0, < 3.a)
@@ -755,8 +755,8 @@ GEM
jmespath (1.6.2)
job-iteration (1.14.0)
activejob (>= 7.1)
json (2.19.5)
json-jwt (1.17.0)
json (2.19.7)
json-jwt (1.17.1)
activesupport (>= 4.2)
aes_key_wrap
base64
@@ -829,7 +829,7 @@ GEM
marcel (1.0.4)
markly (0.16.0)
matrix (0.4.3)
mcp (0.14.0)
mcp (0.16.0)
json-schema (>= 4.1)
messagebird-rest (5.0.0)
jwt (< 4)
@@ -846,7 +846,7 @@ GEM
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
msgpack (1.8.1)
multi_json (1.20.1)
mustermann (4.0.0)
mustermann-grape (1.1.0)
@@ -885,7 +885,7 @@ GEM
oj (3.17.1)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
okcomputer (1.19.1)
okcomputer (1.19.2)
benchmark
omniauth-saml (1.10.6)
omniauth (~> 1.3, >= 1.3.2)
@@ -954,7 +954,7 @@ GEM
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_support (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-all (0.93.0)
opentelemetry-instrumentation-all (0.94.0)
opentelemetry-instrumentation-active_model_serializers (~> 0.25.0)
opentelemetry-instrumentation-anthropic (~> 0.5.0)
opentelemetry-instrumentation-aws_lambda (~> 0.7.0)
@@ -991,14 +991,14 @@ GEM
opentelemetry-instrumentation-ruby_kafka (~> 0.25.0)
opentelemetry-instrumentation-sidekiq (~> 0.29.0)
opentelemetry-instrumentation-sinatra (~> 0.30.0)
opentelemetry-instrumentation-trilogy (~> 0.68.0)
opentelemetry-instrumentation-trilogy (~> 0.69.0)
opentelemetry-instrumentation-anthropic (0.5.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-aws_lambda (0.7.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-aws_sdk (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-base (0.26.0)
opentelemetry-instrumentation-base (0.26.1)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
@@ -1020,9 +1020,9 @@ GEM
opentelemetry-instrumentation-rack (~> 0.29)
opentelemetry-instrumentation-graphql (0.32.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-grpc (0.5.0)
opentelemetry-instrumentation-grpc (0.5.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-gruf (0.6.0)
opentelemetry-instrumentation-gruf (0.6.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-http (0.30.0)
opentelemetry-instrumentation-base (~> 0.25)
@@ -1051,7 +1051,7 @@ GEM
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-racecar (0.7.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.31.0)
opentelemetry-instrumentation-rack (0.31.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rails (0.42.0)
opentelemetry-instrumentation-action_mailer (~> 0.7)
@@ -1078,7 +1078,7 @@ GEM
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-sinatra (0.30.0)
opentelemetry-instrumentation-rack (~> 0.29)
opentelemetry-instrumentation-trilogy (0.68.0)
opentelemetry-instrumentation-trilogy (0.69.0)
opentelemetry-helpers-mysql
opentelemetry-helpers-sql
opentelemetry-helpers-sql-processor
@@ -1092,14 +1092,14 @@ GEM
opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.37.1)
opentelemetry-semantic_conventions (1.38.0)
opentelemetry-api (~> 1.0)
optimist (3.2.1)
os (1.1.4)
ostruct (0.6.3)
ox (2.14.26)
bigdecimal (>= 3.0)
pagy (43.5.4)
pagy (43.5.5)
json
uri
yaml
@@ -1107,7 +1107,7 @@ GEM
activerecord (>= 7.1)
request_store (~> 1.4)
parallel (2.1.0)
parallel_tests (5.7.0)
parallel_tests (4.10.1)
parallel
parser (3.3.11.1)
ast (~> 2.4.1)
@@ -1197,7 +1197,7 @@ GEM
eventmachine_httpserver
http_parser.rb (~> 0.8.0)
multi_json
puma (8.0.1)
puma (8.0.2)
nio4r (~> 2.0)
puma-plugin-statsd (2.8.0)
puma (>= 5.0, < 9)
@@ -1335,7 +1335,7 @@ GEM
rspec-support (3.13.7)
rspec-wait (1.0.2)
rspec (>= 3.4)
rubocop (1.86.2)
rubocop (1.87.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -1355,13 +1355,13 @@ GEM
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-openproject (0.4.0)
rubocop-openproject (0.5.0)
rubocop
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
rubocop-rails (2.35.1)
rubocop-rails (2.35.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -1425,7 +1425,7 @@ GEM
bigdecimal
logger
ruby-ole
spring (4.5.0)
spring (4.6.0)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-rubocop (0.4.0)
@@ -1493,7 +1493,7 @@ GEM
public_suffix
vcr (6.4.0)
vernier (1.10.1)
view_component (4.10.0)
view_component (4.11.0)
actionview (>= 7.1.0)
activesupport (>= 7.1.0)
concurrent-ruby (~> 1)
@@ -1554,8 +1554,8 @@ GEM
railties
yabeda (~> 0.8)
yaml (0.4.0)
yard (0.9.43)
zeitwerk (2.7.5)
yard (0.9.44)
zeitwerk (2.8.2)
PLATFORMS
aarch64-linux
@@ -1594,7 +1594,7 @@ DEPENDENCIES
capybara (~> 3.40.0)
capybara-screenshot (~> 1.0.17)
capybara_accessible_selectors!
carrierwave (~> 2.2.6)
carrierwave (~> 2.2.7)
carrierwave_direct (~> 3.0.0)
climate_control
closure_tree (~> 9.7.0)
@@ -1656,7 +1656,7 @@ DEPENDENCIES
mail (= 2.9.0)
markly (~> 0.15)
matrix (~> 0.4.3)
mcp (~> 0.14.0)
mcp (~> 0.16.0)
md_to_pdf!
meta-tags (~> 2.23.0)
mini_magick (~> 5.3.0)
@@ -1700,13 +1700,13 @@ DEPENDENCIES
openproject-wikis!
openproject-xls_export!
opentelemetry-exporter-otlp (~> 0.34.0)
opentelemetry-instrumentation-all (~> 0.93.0)
opentelemetry-instrumentation-all (~> 0.94.0)
opentelemetry-sdk (~> 1.10)
overviews!
ox
pagy
paper_trail (~> 17.0.0)
parallel_tests (~> 5.7)
parallel_tests (~> 4.0)
pdf-inspector (~> 1.2)
pg (~> 1.6.2)
plaintext (~> 0.3.7)
@@ -1746,7 +1746,7 @@ DEPENDENCIES
rubocop-factory_bot
rubocop-openproject
rubocop-performance
rubocop-rails (~> 2.35.1)
rubocop-rails (~> 2.35.2)
rubocop-rspec
rubocop-rspec_rails
ruby-duration (~> 3.2.0)
@@ -1784,7 +1784,7 @@ DEPENDENCIES
validate_url
vcr
vernier
view_component (~> 4.10.0)
view_component (~> 4.11.0)
warden (~> 1.2)
warden-basic_auth (~> 0.2.1)
webmock (~> 3.26)
@@ -1830,11 +1830,11 @@ CHECKSUMS
auto_strip_attributes (2.6.0) sha256=a7e2e0cf744de2bcd947fd68014220702bcc88c81274c1cd9ce6f7316aae39b0
awesome_nested_set (3.9.0) sha256=3ce99e816550f97f4de118e621630070aacf24928b920fe4a68846578a8daaed
aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b
aws-partitions (1.1249.0) sha256=f0bed070aba353e6ffe439953d6c354b3f51d16770386d2b198e71d34612f2e9
aws-sdk-core (3.247.0) sha256=789864594ce8cef05ee3d81fa8ed506099280bda6ea12a7612b8b7c5e5e62851
aws-sdk-kms (1.125.0) sha256=23f81bc0838ae6ec2e8de3eae88af521d0e29d3a59b6c9dbb4b21343ba476bc8
aws-sdk-s3 (1.222.0) sha256=bae4b06fccf0b81b8d77e7abfc56e5e2146590d43c4ab58db0cee3ff5bbfb7f1
aws-sdk-sns (1.113.0) sha256=15fe37d010e86f4c28b4c2f2133c463ce5c14189ec3673a1f43c30dfee511b0f
aws-partitions (1.1255.0) sha256=490ffc1f032d3eac771cf4a94ab7a3c7999c5edbe6fb7660904e702e291c93b5
aws-sdk-core (3.250.0) sha256=8df71164c7e5f2ff411782a3dc3a1ab5c0a73170284409ead111f385e9992963
aws-sdk-kms (1.128.0) sha256=20ce3957f80c7b40b3115a7e902af52b15a6eef42fe6b2e19db72f8e8c9e5658
aws-sdk-s3 (1.224.0) sha256=7d443c4ae9eda795b60e081d41f49b498e2483f1fc204f3239176ec922ed7879
aws-sdk-sns (1.116.0) sha256=5536532b8f9d54c92a44d8f38fae84333e5eea9b53fdf30bee18cba99e29a9c2
aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00
axe-core-api (4.11.3) sha256=f5f6e802743644a50e2d8ef24c22aefbb6df49dd169024ff0144b47f37e652ba
axe-core-rspec (4.11.3) sha256=246c8d443517354e9a9962a10a8cc456bcef4c617516c0924b051a9af9d7da99
@@ -1853,7 +1853,7 @@ CHECKSUMS
capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef
capybara-screenshot (1.0.27) sha256=afa1896cc23df77be1774e8d3b3ce3953bf060aeaa04ff87607b5daf689174f2
capybara_accessible_selectors (0.15.0)
carrierwave (2.2.6) sha256=cd9b6108fc7544e97e7fbcc561bd319a09f23c96816fdd0df8f2f45ffdc0dac3
carrierwave (2.2.7) sha256=1ede8f502f420e8a34ca1ccbbc9dc92ea159e94583e1194b4ea720a5ccc2d855
carrierwave_direct (3.0.0) sha256=da4105ec7beea2687817b95ad95181be3d657248ec1cb5a0e5c35a45b176fc2f
cbor (0.5.10.2) sha256=df5104f7a62c881123e6505441b1e276208be1771540c2cc3b1de8a210a7c52c
cgi (0.5.1) sha256=e93fcafc69b8a934fe1e6146121fa35430efa8b4a4047c4893764067036f18e9
@@ -1884,7 +1884,7 @@ CHECKSUMS
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
cuprite (0.17) sha256=b140d5dc70d08b97ad54bcf45cd95d0bd430e291e9dffe76fff851fddd57c12b
daemons (1.4.1) sha256=8fc76d76faec669feb5e455d72f35bd4c46dc6735e28c420afb822fac1fa9a1d
dalli (5.0.4) sha256=3b258f100b8e3acd128a095fbd090c5a1324a6f5238fd1f86a2547473e6d49fc
dalli (5.0.5) sha256=5f51502200999278806654c846054856d6d0b5607cc1d14d06c617042f71e6f9
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
date_validator (0.12.0) sha256=68c9834da240347b9c17441c553a183572508617ebfbe8c020020f3192ce3058
deckar01-task_list (2.3.4) sha256=66abdc7e009ea759732bb53867e1ea42de550e2aa03ac30a015cbf42a04c1667
@@ -1893,7 +1893,7 @@ CHECKSUMS
descendants_tracker (0.0.4) sha256=e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
disposable (0.6.3) sha256=7f2a3fb251bff6cd83f25b164043d4ec3531209b51b066ed476a9df9c2d384cc
doorkeeper (5.9.0) sha256=edad053b3dcb6bf51e3181fc423333e7fba28863beb2122379643ce6463b6336
doorkeeper (5.9.1) sha256=f74a35af9da40e59cac7cfd72c6d927421ef171b6d6d9ca0babf66e3fa8b9c90
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
dotenv-rails (3.2.0) sha256=657e25554ba622ffc95d8c4f1670286510f47f2edda9f68293c3f661b303beab
drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
@@ -1925,7 +1925,7 @@ CHECKSUMS
factory_bot_rails (6.5.1) sha256=d3cc4851eae4dea8a665ec4a4516895045e710554d2b5ac9e68b94d351bc6d68
faraday (2.14.2) sha256=73ccb9994a9e8648f010e32eca2ae82e41c57860aa10932cda29418b9e0223ad
faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
faraday-net_http (3.4.3) sha256=9db13becec9312f345a769eeeecf9049c9287d54c0ae053d7235228993a4eec1
ferrum (0.17.2) sha256=2c2540a850b211a46f4d81de21bfd62048f507e4c327d1807225c3823c17e6ee
ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df
ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39
@@ -1943,23 +1943,23 @@ CHECKSUMS
formatador (1.2.3) sha256=19fa898133c2c26cdbb5d09f6998c1e137ad9427a046663e55adfe18b950d894
friendly_id (5.7.0) sha256=b8994416ebaceebc7c60bc556e84ef32a3a663e2e582eb04c429216cc98de4c4
front_matter_parser (1.0.1) sha256=bae298bda01db95788a4d6452f1670a3d198c6716c8d3727db9a95533deb7b7b
fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68
fugit (1.12.2) sha256=643f2bf28db263bd400cbf8e0dd8b76b2c9b94bdb130e12d2394de04d9c20e5e
fuubar (2.5.1) sha256=b272a7804b282661c7fab583a3764f92543cb482c365ae39c685cd218fdd4880
glob (0.4.0) sha256=893dc9e2d24abe13dda907ce0cda576f680ff382f2a6cf9e543f98ecbe29238c
glob (0.5.0) sha256=6397ae620b2f71b00424ec6c880c92d5ddcf6c44e6035c0b610a59efe16418fd
globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
good_job (4.18.2) sha256=7e557a15865fc7b7ad4ab71644cf1d4189a2a1869d3b381e5e88741c540beca6
google-apis-core (1.0.2) sha256=ba4579aaadc902d6cc7bc8db88f566ab00f5e31ea87ab41e9f9a032c470f2629
google-apis-gmail_v1 (0.49.0) sha256=f5b5e597c8018d9e4b8df37fb06a8df429bd2de5f0fc94fb21a4d32b139f6ff1
google-apis-gmail_v1 (0.51.0) sha256=cec89406ee645e71697a0eb0237e972df213d4dde86a772b05979175c48a6d11
google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7
google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b
google-protobuf (4.34.1) sha256=347181542b8d659c60f028fa3791c9cccce651a91ad27782dbc5c5e374796cdc
google-protobuf (4.34.1-aarch64-linux-gnu) sha256=f9c07607dc139c895f2792a7740fcd01cd94d4d7b0e0a939045b50d7999f0b1d
google-protobuf (4.34.1-aarch64-linux-musl) sha256=db58e5a4a492b43c6614486aea31b7fb86955b175d1d48f28ebf388f058d78a9
google-protobuf (4.34.1-arm64-darwin) sha256=2745061f973119e6e7f3c81a0c77025d291a3caa6585a2cd24a25bbc7bedb267
google-protobuf (4.34.1-x86_64-darwin) sha256=4dc498376e218871613589c4d872400d42ad9ae0c700bdb2606fe1c77a593075
google-protobuf (4.34.1-x86_64-linux-gnu) sha256=87088c9fd8e47b5b40ca498fc1195add6149e941ff7e81c532a5b0b8876d4cc9
google-protobuf (4.34.1-x86_64-linux-musl) sha256=8c0e91436fbe504ffc64f0bd621f2e69adbcce8ed2c58439d7a21117069cfdd7
googleapis-common-protos-types (1.22.0) sha256=f97492b77bd6da0018c860d5004f512fe7cd165554d7019a8f4df6a56fbfc4c7
google-protobuf (4.35.0) sha256=95346162c792ed78c9a28cbf2d937a53f706de6df36a27471582f63f03c30c0d
google-protobuf (4.35.0-aarch64-linux-gnu) sha256=2cabd61b420918aec1564f9f7414e455bf922d20c5f8139d62783df5558a7aea
google-protobuf (4.35.0-aarch64-linux-musl) sha256=f6b11f3420a4564f68e8233c95eac6924f6a727dfc2f81a56af2e09add551501
google-protobuf (4.35.0-arm64-darwin) sha256=66ab26d3fc82b8950702e53ab16c198e3c0ea3f2a38aaaf1f32152da45593ac5
google-protobuf (4.35.0-x86_64-darwin) sha256=05eb5c8bc9899135befff496fc0a3642e7ff3d0943f043841dcc456f5654fea0
google-protobuf (4.35.0-x86_64-linux-gnu) sha256=999226f3b00cd9fddb1b26851d16060212fa1d90c406aaad47e574682b716059
google-protobuf (4.35.0-x86_64-linux-musl) sha256=be0218520d77b2aee898b363514b03819f6f63f9c041ae0d0d79b4ce5247bffd
googleapis-common-protos-types (1.23.0) sha256=992e740a523794d9fc5f29a504465d8fc737aaa16c930fe7228e3346860faf0a
googleauth (1.16.2) sha256=15009502e2e38af71948cda918f230e27d327f6882a1e47967a5a4664930a638
grape (3.2.1) sha256=448072f55904e5a4dca2e3781f0a373942514be65402cafb6177f5bc73db1b94
grape_logging (3.0.0) sha256=7b62d984ce96df15d120508668debe307e6a59ac1c511f1d9b5f3b4bea793e13
@@ -1991,8 +1991,8 @@ CHECKSUMS
iso8601 (0.13.0) sha256=298c2b15b7be5fa95a1372813d36a2257656cd8e906dfbc1f5cb409851425aa2
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
job-iteration (1.14.0) sha256=f154f978109acc838c0359ecde2fdd4dccc3382f95a22e03a58ac561a3615224
json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59
json-jwt (1.17.0) sha256=6ff99026b4c54281a9431179f76ceb81faa14772d710ef6169785199caadc4cc
json (2.19.7) sha256=fe432c8639f6efff69f9d73b518a3705d9581ab93156f981ea72806e1e5bcc3e
json-jwt (1.17.1) sha256=5e1ced0f7b206b4c567efee19e6503c1426a819749132926cda579ec013d1f46
json-schema (6.2.0) sha256=e8bff46ed845a22c1ab2bd0d7eccf831c01fe23bb3920caa4c74db4306813666
json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
json_spec (1.1.5) sha256=7a77b97a92c787e2aa3fbc4a1239afc3342c781151dc98cfb81461b3b7cad10f
@@ -2013,7 +2013,7 @@ CHECKSUMS
marcel (1.0.4) sha256=0d5649feb64b8f19f3d3468b96c680bae9746335d02194270287868a661516a4
markly (0.16.0) sha256=6f70d79e385b1efc9e171f74c81628826259039fe6c778e03c3924c71dac5511
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
mcp (0.14.0) sha256=9e3ca2e6b5e568739e8c07090982829896f2e4d884ffbb668d06f0fe758489e1
mcp (0.16.0) sha256=d8ff1a78826945f952eccf803fae1c086e54e91dac7b876038467f5fb1198617
md_to_pdf (0.2.6)
messagebird-rest (5.0.0) sha256=da4cc1efba3d5e4aa021fad07426c2cb6b326ce5670da5104bb8f6056a39d59c
meta-tags (2.23.0) sha256=ffe78b5bee398de4ff5ac3316f5a786049538a651643b8476def06c3acc762c1
@@ -2023,7 +2023,7 @@ CHECKSUMS
mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732
msgpack (1.8.1) sha256=3fef787cd3965fd119c08a22724a56a93ca25008c3421fc15039f603a8b7c86c
multi_json (1.20.1) sha256=2f3934e805cc45ef91b551a1f89d0e9191abd06a5e04a2ef09a6a036c452ca6d
mustermann (4.0.0) sha256=91f67411bb208d1d93c41e6128cb3b0f8ddd9ec7c45966f1007e1c43c08040d7
mustermann-grape (1.1.0) sha256=8d258a986004c8f01ce4c023c0b037c168a9ed889cf5778068ad54398fa458c5
@@ -2044,7 +2044,7 @@ CHECKSUMS
nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f
oj (3.17.1) sha256=b00687f10bf68a32bfb633b87624174faf0989a5c96aff2f3f96f992717ce782
okcomputer (1.19.1) sha256=7df770e768434816d228407f0786563827cbf34cb379933578829720cb4f1e77
okcomputer (1.19.2) sha256=d069aedf1e31b8ebe7e1fdf9e327dee158ea49b9fbdebebc2f1bed4690cb7a6d
omniauth (1.9.2)
omniauth-openid-connect (0.5.0)
omniauth-openid_connect-providers (0.2.0)
@@ -2096,11 +2096,11 @@ CHECKSUMS
opentelemetry-instrumentation-active_record (0.13.0) sha256=239fceaae5a42e82dd9dd87bc63b1888bea32058dac0779b5ea36110fcb3a299
opentelemetry-instrumentation-active_storage (0.5.0) sha256=39920d405fd111cd98c01a90a24b19413f4cb5bc8de2a24d7882f785e8f02c19
opentelemetry-instrumentation-active_support (0.12.0) sha256=29a2cbdcb3aad4a42f4c9e829dab11167b71ed8a5205ad54587fe4d59d8ee704
opentelemetry-instrumentation-all (0.93.0) sha256=e1f918add0d5ec48502cb2fbb49c122457fc2bd2cb54ee85e2db872308bfcb24
opentelemetry-instrumentation-all (0.94.0) sha256=f1d1bb66638c57a4a38bf8f1a26eaa80094a35c26a839ab990b88f238809936f
opentelemetry-instrumentation-anthropic (0.5.0) sha256=a6b1e1f324d35323d4714a7f204c9d34c46da07529877a725829420a4a44bb13
opentelemetry-instrumentation-aws_lambda (0.7.0) sha256=50e5a32c454f2d38ecb53cc94e77dc646b33c47294cc6e6363e7c226097fa132
opentelemetry-instrumentation-aws_sdk (0.12.0) sha256=e2f48bf471cefe4d4bd9cfdafabffce65790b73381040e82d337933ac8bfb366
opentelemetry-instrumentation-base (0.26.0) sha256=fdec8bff9a8de04d113bd4e8d490b17414c92d6c79dd457dfa079c97ba922be0
opentelemetry-instrumentation-base (0.26.1) sha256=49e8d35d565e1a13b4a6b794a4cca4ed8c590d9b33bda5244249bd98c8673775
opentelemetry-instrumentation-bunny (0.25.0) sha256=a8b20b7b4cdbfcfd64036b41c160061b164da2938aa7a7621849ee9f7ecb81b8
opentelemetry-instrumentation-concurrent_ruby (0.25.0) sha256=722912e7078e3025a84a25d0b6085702417598c58187c19a762234702cbf7b2d
opentelemetry-instrumentation-dalli (0.30.0) sha256=5e2fc0ef1f7eb684c6a987789ad0bad22ea9350376e134cd7c803e5eb02776ee
@@ -2110,8 +2110,8 @@ CHECKSUMS
opentelemetry-instrumentation-faraday (0.33.0) sha256=f4320bece35997b8ce2ba520eaf52499b89a0c048fce9ce0a10c1ae5d783f801
opentelemetry-instrumentation-grape (0.7.0) sha256=1b7dddd8e2baad62de6cd20fc924089fb5b8953e23ba41b83b4116ad5bcc03bf
opentelemetry-instrumentation-graphql (0.32.0) sha256=c3af73b42ac5ac873476f2c4c4cf46de2fb81bb74612cea61214a9911708afaa
opentelemetry-instrumentation-grpc (0.5.0) sha256=09bd9ccbedea4668e80546a24dbc1b148fd3775658228050f7378fc0e03b863d
opentelemetry-instrumentation-gruf (0.6.0) sha256=b2a2455b2c622962fc27a943572ee0c660297206bc5eb51b61095d5eb37eae0c
opentelemetry-instrumentation-grpc (0.5.1) sha256=e7a40d1b394745bf638aba27f2c207b439c9e7fd1196f713c731e91697d19bc1
opentelemetry-instrumentation-gruf (0.6.1) sha256=875407e0063f874a01768ed567b13913ab8d03115bb9ff76f179149abc597dd5
opentelemetry-instrumentation-http (0.30.0) sha256=36d2639eab81d386b25e99e0d91fe31888a255159a1213b9648e8359751055c8
opentelemetry-instrumentation-http_client (0.29.0) sha256=92363f0aa7a4286cb02e551c483078a2d5323e9f4ca2b706f2066834f7793d3d
opentelemetry-instrumentation-httpx (0.8.0) sha256=694b6e3eb6df04f1534d7713b5bd67ab5c2e2f2d5438f2c972542b1617378fec
@@ -2123,7 +2123,7 @@ CHECKSUMS
opentelemetry-instrumentation-pg (0.36.0) sha256=f7346f8c4377c053ca2720ac112eacef884840e3f97621ec6f553f3cb23baec7
opentelemetry-instrumentation-que (0.13.0) sha256=95e04b8c17e89eaa446f5dff7975cf72c2e74f7e1e84717be47facf510613112
opentelemetry-instrumentation-racecar (0.7.0) sha256=93a991917687aa0a6e785ccd8d0de5746af110122552b2bf1e3abe12ab04584a
opentelemetry-instrumentation-rack (0.31.0) sha256=bae9f424a2bb1dde2aa5e2c7e02dbbb80c972e7f4964ab820431229f276e0bfc
opentelemetry-instrumentation-rack (0.31.1) sha256=29a9c145ce6d3fa2938f30334209787c29b581a51fdb472e2098a1d69e15536a
opentelemetry-instrumentation-rails (0.42.0) sha256=5ea3808373ca73ee9fa4ecf337471bad1c28e98ea64460b89ab225f5b6eaf8b3
opentelemetry-instrumentation-rake (0.6.0) sha256=71746e4e172560f8ccf1d3c91354196f5aa0fd9b0c477d6ff17a451cef901822
opentelemetry-instrumentation-rdkafka (0.10.0) sha256=ad1a4aa78c0ab43c1f130d961f54e1d6ac20c674b0180b87d4364770b81ec209
@@ -2133,19 +2133,19 @@ CHECKSUMS
opentelemetry-instrumentation-ruby_kafka (0.25.0) sha256=33ceccd5cef4f648e652fa45896d0b014da1a71b3a80f17064829ee5aa84e285
opentelemetry-instrumentation-sidekiq (0.29.0) sha256=b1d2a0cb9041a5e14239fe7c94d99e3dd07f870e2759460ab63592d7cdd8aadc
opentelemetry-instrumentation-sinatra (0.30.0) sha256=b67301153420f43264a0c68cdb3ca5bd77467cf5054e57b83a2bf891aaaa0361
opentelemetry-instrumentation-trilogy (0.68.0) sha256=24b31efdf21a08644ad26038b574f4b0876195b1502f3f64b1065eff3fe0f588
opentelemetry-instrumentation-trilogy (0.69.0) sha256=0676dd720eeab284abfa52f273967442156fcac7084a1e1411373cf14ec026ad
opentelemetry-registry (0.6.0) sha256=5d3ed32ab9eee0fbdb30d4f0d0bb61ad11a4040b267b475ae815b80a8498a728
opentelemetry-sdk (1.12.0) sha256=a224abe0c59023d41cb7ac1c634d9d28843907efcd045ed1ae320796c48b864b
opentelemetry-semantic_conventions (1.37.1) sha256=74e87b31ab88c38410c5447d3bc5e692dcc0a73ee2114910c0142fc4546531c1
opentelemetry-semantic_conventions (1.38.0) sha256=f0c2275a9c921ad7b5e70b658e269bfc6d46e3471d0636e1529fd7f34ae9a663
optimist (3.2.1) sha256=8cf8a0fd69f3aa24ab48885d3a666717c27bc3d9edd6e976e18b9d771e72e34e
os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
overviews (1.0.0)
ox (2.14.26) sha256=d4a9cb49fb55751631d469623cfb2e7dad7cea89119c93749c1e31a3fb2f4896
pagy (43.5.4) sha256=2bdf3fa6b1e0cac5bbafe5d077fb24eb971f72f3194f8c6863a0f3867261ce59
pagy (43.5.5) sha256=86790336da3a1966e3503c226989577f1f56e3353dad10353445e8c988412548
paper_trail (17.0.0) sha256=1c2842061d3874ca7015908e821e2aa14f9b982af2acb2a7974713bf79021c85
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
parallel_tests (5.7.0) sha256=3f1762c46ca2c223b8af8ef877217f9d76974e191bfa934f2580b58bcf1d005c
parallel_tests (4.10.1) sha256=df05458c691462b210f7a41fc2651d4e4e8a881e8190e6d1e122c92c07735d70
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
pdf-core (0.9.0) sha256=4f368b2f12b57ec979872d4bf4bd1a67e8648e0c81ab89801431d2fc89f4e0bb
pdf-inspector (1.3.0) sha256=fc107579d6f29b636e2da3d6743479b2624d9e390bf2d84beef8fd4ebe1a05bd
@@ -2177,7 +2177,7 @@ CHECKSUMS
psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
puffing-billy (4.0.4) sha256=87015b0c41e0722b2171a0c5aa8130fd3f58aa1c016a1dc6dc569b2028aa846f
puma (8.0.1) sha256=7b94e50c07655718c1fb8ae41a11fc06c7d61293208b3aa608ff71a46d3ad37c
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
puma-plugin-statsd (2.8.0) sha256=e515445f93232b6b3571a23b832f93a776d4ce0fc8a5edee798013b82f3488f3
raabro (1.4.0) sha256=d4fa9ff5172391edb92b242eed8be802d1934b1464061ae5e70d80962c5da882
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
@@ -2230,13 +2230,13 @@ CHECKSUMS
rspec-retry (0.6.2) sha256=6101ba23a38809811ae3484acde4ab481c54d846ac66d5037ccb40131a60d858
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
rspec-wait (1.0.2) sha256=865f921239325d3d26fc10ded4bdd485d8b58bcaaad1a28dd85ed15266b5a912
rubocop (1.86.2) sha256=bb2e97f635eda42c448f2588f4a6ff78f221b8bdfdf65b1e9b07fbd57521b45d
rubocop (1.87.0) sha256=b9d9ddf55116a513f8ef2c7ae660662d8b49301f118d3f0df61865b33a5c188d
rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
rubocop-capybara (2.23.0) sha256=f9ea1ba3a7561ee8e88cf76fc378ce517ce5327155f305ee7b5c2500e5aee357
rubocop-factory_bot (2.28.0) sha256=4b17fc02124444173317e131759d195b0d762844a71a29fe8139c1105d92f0cb
rubocop-openproject (0.4.0) sha256=ce56d9e591f9be5a4d98125b10a73564b0557a5e408f97918f9630fb15ae66ae
rubocop-openproject (0.5.0) sha256=564d4a0d240bec8543c96d1cb38facba9afd3fbe7e91ff28d254d3ebf329c323
rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834
rubocop-rails (2.35.1) sha256=4ad270f5fb968ce86ac19f53c4b97354e4f91bc5334177478238674f51568377
rubocop-rails (2.35.3) sha256=6edd45410866912b9b2e90ae3aeafd31d576df2bb2a9c9408f1667a50c32c7de
rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100
ruby-duration (3.2.3) sha256=eb3d13b1df85067a015a8fb2ed8f1eec842a3b721e47c9b6fd74d2f356069784
@@ -2262,7 +2262,7 @@ CHECKSUMS
simpleidn (0.2.3) sha256=08ce96f03fa1605286be22651ba0fc9c0b2d6272c9b27a260bc88be05b0d2c29
smart_properties (1.17.0) sha256=f9323f8122e932341756ddec8e0ac9ec6e238408a7661508be99439ca6d6384b
spreadsheet (1.3.5) sha256=cd83ea66803d9cae4ac258dfe16cd8c2b85da33eec18a6d7b48fd4a45840ab7d
spring (4.5.0) sha256=9124b954f6d079cb9aa610d7069cbada7c146c9368599557806a9ff7067b2e1b
spring (4.6.0) sha256=e9e41f9a56fa5f111df7c85110a7d606ca731dc116c906ba395fd74c9fd474e3
spring-commands-rspec (1.0.4) sha256=6202e54fa4767452e3641461a83347645af478bf45dddcca9737b43af0dd1a2c
spring-commands-rubocop (0.4.0) sha256=3e677a2c8a27ae8a986f04bfb69e66d5d55b017541e8be93bf0dc48a7f5690c1
sprockets (3.7.5) sha256=72c20f256548f8a37fe7db41d96be86c3262fddaf4ebe9d69ec8317394fed383
@@ -2291,7 +2291,7 @@ CHECKSUMS
ttfunk (1.7.0) sha256=2370ba484b1891c70bdcafd3448cfd82a32dd794802d81d720a64c15d3ef2a96
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
turbo_power (0.7.0) sha256=ad95d147e0fa761d0023ad9ca00528c7b7ddf6bba8ca2e23755d5b21b290d967
turbo_tests (2.2.5)
turbo_tests (2.2.0)
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
tzinfo-data (1.2026.2) sha256=7db0d3d3d53b8d7601fc183fccc8c6d056a3004e14eb59ea995bf6aec4ae10bc
uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc
@@ -2303,7 +2303,7 @@ CHECKSUMS
validate_url (1.0.15) sha256=72fe164c0713d63a9970bd6700bea948babbfbdcec392f2342b6704042f57451
vcr (6.4.0) sha256=077ac92cc16efc5904eb90492a18153b5e6ca5398046d8a249a7c96a9ea24ae6
vernier (1.10.1) sha256=f2ab2e163b75ac3ef4e3173262d27d27da6219cf04b1b4a7171e4798114275ce
view_component (4.10.0) sha256=9e86960ee7ea4da3d1d07f65b1a66e4a9fb656be5434f1a649eb6fce6ac9baf9
view_component (4.11.0) sha256=92df960575b8fa5e984522532df225ec333015a07a8105b3b1c200c655065517
virtus (2.0.0) sha256=8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
warden-basic_auth (0.2.1) sha256=bfc752e0109c0182c3e69e930284c5e1e81e7b4a354aeb2b5914ead1391f3c6e
@@ -2323,8 +2323,8 @@ CHECKSUMS
yabeda-puma-plugin (0.9.0) sha256=b78673ecc7ee30bc50691ddc41b7022c1c1801843900d5101418f4a14b550bc8
yabeda-rails (0.11.0) sha256=afa2581bd44c8f419cb3f2bbf9f6fb40f817c30476f7caf5d1c55c48d69a5b29
yaml (0.4.0) sha256=240e69d1e6ce3584d6085978719a0faa6218ae426e034d8f9b02fb54d3471942
yard (0.9.43) sha256=cf8733a8f0485df2a162927e9b5f182215a61f6d22de096b8f402c726a1c5821
zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
yard (0.9.44) sha256=eb087e9b631ccd887b049f303d489963945452d5e2a7eb49a5a74a7cf6887f28
zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12
RUBY VERSION
ruby 4.0.2
+1 -2
View File
@@ -1,6 +1,4 @@
@import "enterprise_edition/banner_component"
@import "filter/filters_component"
@import "open_project/common/border_box_list_component"
@import "op_primer/border_box_table_component"
@import "op_primer/full_page_prompt_component"
@import "op_primer/form_helpers"
@@ -9,6 +7,7 @@
@import "open_project/common/attribute_help_text_component"
@import "open_project/common/attribute_help_text_caption_component"
@import "open_project/common/attribute_label_component"
@import "open_project/common/border_box_list_component"
@import "open_project/common/inplace_edit_fields/index"
@import "open_project/common/submenu_component"
@import "open_project/common/main_menu_toggle_component"
@@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= component_wrapper(tag: "tr", class: row_css_class, data: { turbo: true }) do %>
<%= component_wrapper(tag: "tr", class: row_css_class) do %>
<% columns.each do |column| %>
<td class="<%= column_css_class(column) %>">
<%= column_value(column) %>
@@ -0,0 +1,39 @@
<%=
render(
Primer::OpenProject::DangerDialog.new(
id: DIALOG_ID,
title: t("working_days.dialog.title"),
confirm_button_text: t("working_days.dialog.confirm_button"),
form_arguments:,
size: :medium_portrait
)
) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { t("working_days.dialog.heading") }
message.with_description_content(t("working_days.dialog.description"))
end
dialog.with_additional_details do
concat hidden_settings_fields
concat(
tag.div do
if removed_non_working_days.any?
concat tag.p(t("working_days.dialog.removed_title"))
concat(
render(
Primer::BaseComponent.new(
tag: :ul,
mb: 3
)
) do
safe_join(removed_non_working_days.map { |non_working_day| tag.li(non_working_day) })
end
)
end
concat tag.p(t("working_days.dialog.warning"))
end
)
end
end
%>
@@ -0,0 +1,78 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Admin
module Settings
module WorkingDays
class ConfirmDialogComponent < ApplicationComponent
include OpTurbo::Streamable
DIALOG_ID = "working-days-change-dialog"
attr_reader :form_values, :removed_non_working_days
def initialize(form_values: {}, removed_non_working_days: [])
super
@form_values = form_values
@removed_non_working_days = removed_non_working_days
end
private
def form_arguments
{
action: helpers.admin_settings_working_days_and_hours_path,
method: :patch,
data: { turbo: false }
}
end
def hidden_settings_fields
hidden_field_tags_for("settings", form_values)
end
def hidden_field_tags_for(name, value)
case value
when Hash
safe_join(
value.flat_map do |key, nested_value|
hidden_field_tags_for("#{name}[#{key}]", nested_value)
end
)
when Array
safe_join(value.map { |array_value| hidden_field_tag("#{name}[]", array_value) })
else
hidden_field_tag(name, value)
end
end
end
end
end
end
+18 -122
View File
@@ -26,134 +26,30 @@
<% else %>
<%= helpers.turbo_frame_tag TURBO_FRAME_ID do %>
<%= form_tag(
{},
<%= primer_form_with(
url: {},
method: :get,
data: {
action: "submit->filter--filters-form#sendForm:prevent"
}
) do %>
<fieldset class="advanced-filters--container">
<%=
render(
Primer::Beta::IconButton.new(
icon: :x,
scheme: :invisible,
classes: "advanced-filters--close",
tooltip_direction: :se,
aria: { label: t("js.close_form_title") },
data: { action: "filter--filters-form#toggleDisplayFilters" }
)
)
%>
<legend><%= t(:label_filter_plural) %></legend>
<ul class="advanced-filters--filters">
<% 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" %>"
data-filter-name="<%= filter.name %>"
data-filter-type="<%= filter.type %>"
data-filter--filters-form-target="filter">
<label class="advanced-filters--filter-name" for="<%= filter.name %>">
<%= filter.human_name %>
</label>
<% selected_operator = filter.operator || filter.default_operator.symbol %>
<%= content_tag :div, class: "advanced-filters--filter-operator", style: filter_boolean ? "display:none" : "" do %>
<%= select_tag :operator,
options_from_collection_for_select(
filter.available_operators,
:symbol,
:human_name,
selected_operator
),
class: "advanced-filters--select",
data: {
action: "change->filter--filters-form#setValueVisibility",
"filter--filters-form-filter-name-param": filter.name,
"filter--filters-form-target": "operator",
"filter-name": filter.name
} %>
<% end %>
<% if autocomplete_filter %>
<%= render partial: "filters/autocomplete",
locals: { value_visibility: value_hidden_class(selected_operator),
filter: filter,
autocomplete_options: additional_options[:autocomplete_options] } %>
<% elsif filter_boolean %>
<%= render partial: "filters/boolean",
locals: { value_visibility: value_hidden_class(selected_operator),
filter: filter } %>
<% elsif %i(list list_optional list_all).include? filter.type %>
<%= render partial: "filters/list/input_options",
locals: { value_visibility: value_hidden_class(selected_operator),
filter: filter } %>
<% elsif [:datetime_past, :date].include? filter.type %>
<%= render partial: "filters/date/input_options",
locals: { value_visibility: value_hidden_class(selected_operator),
filter: filter,
selected_operator: selected_operator } %>
<% else %>
<%# All other simple types %>
<%= render partial: "filters/text",
locals: { value_visibility: value_hidden_class(selected_operator),
filter: filter } %>
<% end %>
<div class="advanced-filters--remove-filter">
<a href=""
class="filter_rem"
data-action="click->filter--filters-form#removeFilter"
data-filter--filters-form-filter-name-param="<%= filter.name %>">
<%= helpers.op_icon("icon-close advanced-filters--remove-filter-icon", title: I18n.t("js.button_delete")) %>
</a>
</div>
</li>
<% end %>
<li class="advanced-filters--spacer <%= query.filters.blank? ? "hidden" : "" %>"
data-filter--filters-form-target="spacer"></li>
<li class="advanced-filters--add-filter">
<!-- Add filters -->
<label for="add_filter_select" aria-hidden="true" class="advanced-filters--add-filter-label ng-binding">
<%= helpers.op_icon("icon-add icon4") %>
<%= t(:label_filter_add) %>:
</label>
<label for="add_filter_select" class="sr-only ng-binding">
<%= t(:label_filter_add) %>
<%= t("js.filter.description.text_open_filter") %>
<%= t("js.filter.description.text_close_filter") %>
</label>
<div class="advanced-filters--add-filter-value">
<%= select_tag "add_filter_select",
options_from_collection_for_select(
allowed_filters,
:name,
:human_name,
disabled: query.filters.map(&:name)
),
prompt: t(:actionview_instancetag_blank_option),
class: "advanced-filters--select",
focus: "false",
"aria-invalid": "false",
data: {
"filter--filters-form-target": "addFilterSelect",
action: "change->filter--filters-form#addFilter:prevent"
} %>
</div>
</li>
) do |f| %>
<%= render(Primer::Beta::BorderBox.new) do |box| %>
<% box.with_body do %>
<%= render(filter_form(f)) %>
<% unless turbo_requests? %>
<li class="advanced-filters--controls">
<%= submit_tag t("button_apply"), class: "button -small -primary", name: nil %>
</li>
<div class="advanced-filters--actions">
<%= render(
Primer::Beta::Button.new(
scheme: :secondary,
type: :button,
data: { action: "filter--filters-form#toggleDisplayFilters" }
)
) { t("js.close_form_title") } %>
<%= render(Primer::Beta::Button.new(type: :submit, scheme: :primary)) { t("button_apply") } %>
</div>
<% end %>
</ul>
</fieldset>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
+9 -114
View File
@@ -29,7 +29,6 @@
# ++
module Filter
class FilterComponent < ApplicationComponent
OPERATORS_WITHOUT_VALUES = %w[* !* t w].freeze
TURBO_FRAME_ID = "filter_component"
options :query
@@ -38,29 +37,14 @@ module Filter
options lazy_loaded_path: false
options initially_expanded: false
# 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.each do |allowed_filter|
active_filter = query.find_active_filter(allowed_filter.name)
filter = active_filter || allowed_filter
yield filter, active_filter.present?, additional_filter_attributes(filter)
end
def filter_form(form)
Filters::FilterFormComponent.new(builder: form, query:, allowed_filters:)
end
def allowed_filters
query.available_advanced_filters
end
def value_hidden_class(selected_operator)
operator_without_value?(selected_operator) ? "hidden" : ""
end
def operator_without_value?(operator)
OPERATORS_WITHOUT_VALUES.include?(operator)
end
def lazy_loaded? = !!lazy_loaded_path
def initially_expanded? = initially_expanded
@@ -75,105 +59,16 @@ module Filter
end
def filter_classes
"op-filters-form op-filters-form_top-margin #{'-expanded' if initially_expanded?}"
[
"op-filters-form",
"op-filters-form_top-margin",
("-expanded" if initially_expanded?),
("op-filters-form--with-footer" unless turbo_requests?)
].compact.join(" ")
end
def lazy_turbo_frame_src
public_send(lazy_loaded_path, **params.permit(:filters, :columns, :sortBy, :id, :query_id))
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::Required,
Queries::Filters::Shared::ProjectFilter::Optional
{ autocomplete_options: project_autocomplete_options }
when Queries::Filters::Shared::CustomFields::User
{ autocomplete_options: user_autocomplete_options }
when Queries::Filters::Shared::CustomFields::ListOptional
{ autocomplete_options: custom_field_list_autocomplete_options(filter) }
when Queries::Filters::Shared::CustomFields::Hierarchy
{ autocomplete_options: custom_field_hierarchy_autocomplete_options(filter) }
when Queries::Projects::Filters::ProjectStatusFilter,
Queries::Projects::Filters::TypeFilter
{ autocomplete_options: list_autocomplete_options(filter) }
else
{}
end
end
def custom_field_list_autocomplete_options(filter)
all_items = if filter.custom_field.version?
filter.allowed_values.map { |name, id, project_name| { name:, id:, project_name: } }
else
filter.allowed_values.map { |name, id| { name:, id: } }
end
selected = filter.values
options = { items: all_items }
options[:groupBy] = "project_name" if filter.custom_field.version?
autocomplete_options.merge(options).merge(model: all_items.select { |item| selected.include?(item[:id]) })
end
def custom_field_hierarchy_autocomplete_options(filter)
items = filter.allowed_values.map do |name, id|
path = name.split(" / ")
{ name: path.last, id:, depth: path.length - 1 }
end
selected = filter.values
autocomplete_options.merge({ items: }).merge(model: items.select { |item| selected.include?(item[:id]) })
end
def list_autocomplete_options(filter)
all_items = filter.allowed_values.map { |name, id| { name:, id: } }
selected = filter.values
autocomplete_options.merge(
items: all_items,
model: all_items.select { |item| selected.include?(item[:id]) }
)
end
def autocomplete_options
{
component: "opce-autocompleter",
bindValue: "id",
bindLabel: "name",
hideSelected: true
}
end
def project_autocomplete_options
{
component: "opce-project-autocompleter",
resource: "projects",
filters: [
{ name: "active", operator: "=", values: ["t"] }
]
}
end
def user_autocomplete_options
{
component: "opce-user-autocompleter",
hideSelected: true,
defaultData: false,
placeholder: I18n.t(:label_user_search),
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
filters: [
{ name: "status", operator: "!", values: [Principal.statuses["locked"].to_s] }
],
searchKey: "any_name_attribute",
focusDirectly: false
}
public_send(lazy_loaded_path, **params.permit(:filters, :columns, :sortBy, :id, :query_id, :project_id))
end
end
end
@@ -1,47 +0,0 @@
.op-filters
&-form
display: none
&.-expanded
display: block
.advanced-filters--filter-value
// visibility based on operator type
&.hidden
visibility: hidden
height: 55px
// visibility for list value selectors
.multi-select
display: none
.single-select
display: block
&.multi-value
.multi-select
display: block
.single-select
display: none
// visibility for datetime_past value selectors
&.between-dates
>.on-date,
>.days
display: none
&.on-date
>.between-dates,
>.days
display: none
&.days
>.on-date,
>.between-dates
display: none
// Special input field styles
&.between-dates
input[type="text"]
display: inline-block
max-width: 6rem
.advanced-filters--controls
margin-top: 1rem
&_top-margin
margin-top: 1rem
@@ -0,0 +1,11 @@
<%=
render(
Primer::ConditionalWrapper.new(
condition: @wrap_with_controller,
**@wrapper_arguments
)
) do
%>
<%= hidden_filters_input if @hidden_input_name %>
<%= render(form_list) %>
<% end %>
@@ -0,0 +1,172 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
# Renders the list of filter input fields (one row per available filter plus an
# "add filter" select) for a given query as part of a Primer form.
#
# The set of inputs depends on the query's available and active filters and is
# built dynamically at render time. The component receives the builder of the
# surrounding `primer_form_with` so that the emitted field names match what the
# controller expects (top-level `operator_<filter>` and `<filter>_value` fields).
#
# Embed it in any primer form:
#
# <%= primer_form_with(url: ...) do |f| %>
# <%= f.text_field(name: :title) %>
# <%= render(Filters::FilterFormComponent.new(builder: f, query: @query)) %>
# <% end %>
#
# Customise the set of advertised filters by passing `allowed_filters:` (used
# by `Filter::FilterComponent` subclasses that restrict or reorder the list).
#
# By default the component does *not* attach the `filter--filters-form` Stimulus
# controller, because in the standard layout (e.g. `Projects::IndexSubHeaderComponent`)
# the controller has to sit on a common ancestor of the advanced filter form
# *and* the inline quick filter input so that `sendForm()` can collect values
# from both. For standalone embeds with no co-located quick filter, pass
# `wrap_with_controller: true` and the component will emit its own controller
# wrapper.
#
# Pass `hidden_input_name:` (e.g. `"filters"`) to also emit a hidden input
# bound to the Stimulus controller's `filtersInput` target. The controller
# keeps the field value in sync with the serialized filter selections so
# that a normal form submit carries the canonical filter string in
# `params[:<hidden_input_name>]` — no `sendForm` redirect needed.
#
# `output_format:` selects how the filter selection is serialized into the
# hidden field (and into the URL when `sendForm` redirects). Supported values:
# * `:params` (default) — URL-style string: `name ~ "foo"&login ! "bar"`.
# * `:json` — JSON array: `[{"name":{"operator":"~","values":["foo"]}}, ...]`.
# Only meaningful when this component owns the controller
# (`wrap_with_controller: true`); otherwise the host's controller wrapper
# decides.
#
# `autocomplete_append_to:` forwards an `appendTo` selector (or DOM reference
# string ng-select understands, e.g. `"#my-dialog"` or `"body"`) to every
# autocompleter the component renders. Use this when the component is embedded
# in a Primer dialog or another container that clips overflow, so the dropdown
# portal renders outside that container instead of being clipped.
class Filters::FilterFormComponent < ApplicationComponent
include OpPrimer::AttributesHelper
include Primer::FetchOrFallbackHelper
OUTPUT_FORMATS = %i[params json].freeze
def initialize(builder:,
query:,
allowed_filters: nil,
wrap_with_controller: false,
hidden_input_name: nil,
output_format: nil,
autocomplete_append_to: nil,
**wrapper_arguments)
super()
@builder = builder
@query = query
@allowed_filters = allowed_filters || query.available_advanced_filters
@wrap_with_controller = wrap_with_controller
@hidden_input_name = hidden_input_name
@output_format = fetch_or_fallback(OUTPUT_FORMATS, output_format.to_sym) if output_format
@autocomplete_append_to = autocomplete_append_to
@wrapper_arguments = wrapper_arguments
@wrapper_arguments[:tag] ||= :div
@wrapper_arguments[:classes] = class_names(
"op-filters-form -expanded",
@wrapper_arguments[:classes]
)
@wrapper_arguments[:data] = merge_data(
@wrapper_arguments,
{
data: {
controller: "filter--filters-form",
filter__filters_form_output_format_value: @output_format&.to_s
}
}
)
end
private
attr_reader :query, :allowed_filters
def form_list
Primer::Forms::FormList.new(*sub_forms)
end
def hidden_filters_input
hidden_field_tag(
@hidden_input_name,
"",
data: { filter__filters_form_target: "filtersInput" }
)
end
def sub_forms
forms = map_filter do |filter, active, additional_attributes|
filter_form_class(filter)
.new(@builder, filter:, additional_attributes:, active:)
end
forms << Filters::Inputs::AddFilterForm.new(
@builder,
allowed_filters:,
active_filter_names: query.filters.map(&:name)
)
end
def map_filter
allowed_filters.map do |allowed_filter|
active_filter = query.find_active_filter(allowed_filter.name)
filter = active_filter || allowed_filter
yield filter, active_filter.present?, additional_filter_attributes(filter)
end
end
def additional_filter_attributes(filter)
opts = filter.autocomplete_options
opts = opts.merge(appendTo: @autocomplete_append_to) if @autocomplete_append_to
opts.any? ? { autocomplete_options: opts } : {}
end
def filter_form_class(filter)
if filter.is_a?(Queries::Filters::Shared::BooleanFilter)
Filters::Inputs::BooleanForm
elsif filter.autocomplete_options.any?
Filters::Inputs::AutocompleteForm
elsif filter.type.in? %i[list list_optional list_all]
Filters::Inputs::ListForm
elsif filter.type.in? %i[datetime_past date]
Filters::Inputs::DateForm
else
Filters::Inputs::TextForm
end
end
end
@@ -0,0 +1,39 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Homescreen
module Blocks
class FavoriteProjects < Grids::WidgetComponent
def call
render(Grids::Widgets::FavoriteProjects.new(current_user:))
end
end
end
end
@@ -1,57 +0,0 @@
<%= widget_wrapper do %>
<% if @favorite_projects.any? %>
<p class="widget-box--additional-info"><%= t("projects.lists.favorited") %></p>
<ul class="widget-box--arrow-links">
<% @favorite_projects.each do |project| %>
<li>
<%= render(
Primer::Beta::Octicon.new(
icon: "star-fill",
classes: "op-primer--star-icon",
"aria-label": I18n.t(:label_favorite)
)
) %>
<%= link_to project, project_path(project),
title: short_project_description(project),
data: { test_selector: "favorite-project" } %>
</li>
<% end %>
</ul>
<% end %>
<% if @newest_projects.empty? %>
<p class="widget-box--additional-info">
<%= t("homescreen.additional.no_visible_projects") %>
</p>
<% else %>
<p class="widget-box--additional-info"><%= t("homescreen.additional.projects") %></p>
<ul class="widget-box--arrow-links">
<% @newest_projects.each do |project| %>
<li>
<%= link_to project, project_path(project), title: short_project_description(project) %>
<small>(<%= format_date(project.created_at) %>)</small>
</li>
<% end %>
</ul>
<% end %>
<div class="widget-box--blocks--buttons">
<% if current_user.allowed_globally?(:add_project) %>
<%= link_to new_project_path,
{ class: "button -primary",
aria: { label: t(:label_project_new) },
title: t(:label_project_new) } do %>
<%= op_icon("button--icon icon-add") %>
<span class="button--text"><%= Project.model_name.human %></span>
<% end %>
<% end %>
<%# If any project exists %>
<% unless @newest_projects.empty? %>
<%= link_to t(:label_project_view_all), projects_path,
class: "button -highlight-inverted",
title: t(:label_project_view_all) %>
<% end %>
</div>
<% end %>
@@ -0,0 +1,47 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpPrimer
# Drop-in replacement for `Primer::AttributesHelper` that treats the Stimulus
# `controller` data attribute as plural. Stimulus supports multiple
# controllers on one element (space-separated `data-controller="a b"`), but
# upstream Primer omits `controller` from its plural data attributes, so
# `merge_data` would silently drop a caller's controller when a component
# merges in its own. Treating it as plural concatenates them instead.
module AttributesHelper
include Primer::AttributesHelper
PLURAL_DATA_ATTRIBUTES = (Primer::AttributesHelper::PLURAL_DATA_ATTRIBUTES + %i[controller]).freeze
def merge_data(*hashes)
merge_prefixed_attribute_hashes(*hashes, prefix: :data, plural_keys: PLURAL_DATA_ATTRIBUTES)
end
end
end
@@ -27,13 +27,15 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= render(Primer::Alpha::SegmentedControl.new("aria-label": @name, full_width: false)) do |control| %>
<% items.each do |item| %>
<% control.with_item(
tag: :a,
href: href_for(item.value),
label: item.label,
selected: current_value == item.value
) %>
<%= component_wrapper do %>
<%= render(Primer::Alpha::SegmentedControl.new("aria-label": @name, full_width: false)) do |control| %>
<% items.each do |item| %>
<% control.with_item(
tag: :a,
href: href_for(item.value),
label: item.label,
selected: current_value == item.value
) %>
<% end %>
<% end %>
<% end %>
@@ -32,6 +32,7 @@ module OpPrimer
module QuickFilter
class SegmentedComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable
renders_many :items, Item
@@ -45,9 +46,9 @@ module OpPrimer
@orders = orders
end
def render?
items.any?
end
def wrapper_key = "quick-filter-#{@filter_key}"
def render? = items.any?
private
@@ -84,10 +84,8 @@ module Projects
def allowed_new_workspace_types
@allowed_new_workspace_types ||= [].tap do |types|
if OpenProject::FeatureDecisions.portfolio_models_active?
types << "portfolio" if @current_user.allowed_globally?(:add_portfolios)
types << "program" if @current_user.allowed_globally?(:add_programs)
end
types << "portfolio" if @current_user.allowed_globally?(:add_portfolios)
types << "program" if @current_user.allowed_globally?(:add_programs)
types << "project" if @current_user.allowed_globally?(:add_project)
end
end
-1
View File
@@ -172,7 +172,6 @@ module Projects
end
def workspace_type_badge
return unless OpenProject::FeatureDecisions.portfolio_models_active?
# Only show icon and type for non-project workspaces
return unless project.workspace_type.in?(["portfolio", "program"])
@@ -66,7 +66,7 @@ module Projects
end
def render_view_all_link(widget)
widget.with_row do
widget.with_footer do
helpers.link_to(
I18n.t("projects.settings.versions.show_work_packages"),
helpers.project_work_packages_version_path(version)
+6 -2
View File
@@ -89,8 +89,12 @@ class RowComponent < ApplicationComponent
:default
end
def checkmark(condition)
if condition
def checkmark(condition, primerized: false)
return unless condition
if primerized
render(Primer::Beta::Octicon.new(icon: :check))
else
helpers.op_icon "icon icon-checkmark"
end
end
+1 -1
View File
@@ -42,7 +42,7 @@ See COPYRIGHT and LICENSE files for more details.
<% if sortable_column?(name) %>
<%= helpers.sort_header_tag(name, **options) %>
<% else %>
<th>
<th aria-label="<%= options[:caption] %>">
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
@@ -38,28 +38,5 @@ module Users
.grep_v(Queries::Users::Filters::BlockedFilter)
.sort_by(&:human_name)
end
protected
def additional_filter_attributes(filter)
case filter
when Queries::Users::Filters::GroupFilter
{
autocomplete_options: {
component: "opce-user-autocompleter",
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
filters: [
{ name: "type", operator: "=", values: %w[Group] }
],
searchKey: "any_name_attribute",
inputValue: filter.values,
bindValue: "id"
}
}
else
super
end
end
end
end
@@ -36,6 +36,7 @@ module Attachments
attribute :digest
attribute :description
attribute :content_type
attribute :charset
attribute :container
attribute :container_type
attribute :author
@@ -36,11 +36,9 @@ module OAuthClients
validates :client_id, presence: true, length: { maximum: 255 }
attribute :client_secret, writable: true
validates :client_secret, presence: true, if: :client_secret_required?
validates :client_secret, presence: true
validates :client_secret, length: { maximum: 255 }
def client_secret_required? = true
attribute :integration_type, writable: true
validates :integration_type, presence: true
@@ -0,0 +1,35 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Admin::Settings
class ExportsSettingsController < ::Admin::SettingsController
menu_item :settings_exports
end
end
@@ -30,8 +30,25 @@
module Admin::Settings
class WorkingDaysAndHoursSettingsController < ::Admin::SettingsController
include OpTurbo::ComponentStream
menu_item :working_days_and_hours
def confirm_changes
return update unless working_days_changed? || non_working_days_changed?
removed_days = non_working_days_params
.select { |nwd| nwd["_destroy"].present? }
.filter_map { |nwd| removed_non_working_day_date(nwd) }
component = Admin::Settings::WorkingDays::ConfirmDialogComponent.new(
form_values: params.expect(settings: {}).to_h,
removed_non_working_days: removed_days
)
respond_with_dialog(component)
end
def failure_callback(call)
@modified_non_working_days = modified_non_working_days_for(call.result)
flash[:error] = call.message || I18n.t(:notice_internal_server_error)
@@ -53,15 +70,31 @@ module Admin::Settings
private
def working_days_changed?
working_days_params(params.expect(settings: {})) != Setting.working_days.map(&:to_i)
end
def non_working_days_changed?
non_working_days_params.any?
end
def working_days_params(settings)
settings[:working_days] ? settings[:working_days].compact_blank.map(&:to_i).uniq : []
end
def non_working_days_params
non_working_days = params[:settings].to_unsafe_hash[:non_working_days_attributes] || {}
non_working_days = params.expect(settings: {})[:non_working_days_attributes] || {}
non_working_days.to_h.values
end
def removed_non_working_day_date(non_working_day_params)
date = NonWorkingDay.find_by(id: non_working_day_params["id"])&.date || non_working_day_params["date"]
I18n.l(date.to_date, format: :long)
rescue Date::Error, NoMethodError
nil
end
def modified_non_working_days_for(result)
return if result.nil?
@@ -0,0 +1,41 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class OpenprojectMetadataController < ApplicationController
no_authorization_required! :show
skip_before_action :check_if_login_required
def show
render json: {
installation_uuid: Setting.installation_uuid
}
end
end
@@ -31,7 +31,6 @@ module Portfolios
class MenusController < ApplicationController
authorization_checked! :show
before_action :not_authorized_on_feature_flag_inactive
before_action :authorize_portfolio_access, only: %i[show]
def show
@@ -47,9 +46,5 @@ module Portfolios
render_403 unless User.current.allowed_globally?(:add_portfolios) ||
Project.portfolio.allowed_to(User.current, :view_project).any?
end
def not_authorized_on_feature_flag_inactive
render_403 unless OpenProject::FeatureDecisions.portfolio_models_active?
end
end
end
-1
View File
@@ -34,7 +34,6 @@
# projects in these cases.
class PortfoliosController < ProjectsController
before_action :authorize_portfolio_access, only: %i[index]
before_action :not_authorized_on_feature_flag_inactive
skip_before_action :load_query_or_deny_access # skip using the superclass's before action because the next must be called first
before_action :set_default_query, only: %i[index] # Must be called before `load_query_or_deny_access`
-1
View File
@@ -29,5 +29,4 @@
# ++
class ProgramsController < ProjectsController
before_action :not_authorized_on_feature_flag_inactive
end
-4
View File
@@ -330,10 +330,6 @@ class ProjectsController < ApplicationController
::Exports::Register.list_formats(Project).map(&:to_s)
end
def not_authorized_on_feature_flag_inactive
render_403 unless OpenProject::FeatureDecisions.portfolio_models_active?
end
def layout_for_new
if portfolio_management_feature_missing?
"global"
@@ -0,0 +1,49 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Admin
module Settings
class ExportsSettingsForm < ApplicationForm
settings_form do |sf|
sf.text_field(
name: :work_packages_projects_export_limit,
type: :number,
input_width: :xsmall,
caption: I18n.t(:setting_work_packages_projects_export_limit_text)
)
sf.check_box(
name: :csv_escape_formulas,
caption: I18n.t(:setting_csv_escape_formulas_text)
)
end
end
end
end
@@ -82,12 +82,6 @@ module Admin
input_width: :xsmall
)
sf.text_field(
name: :work_packages_projects_export_limit,
type: :number,
input_width: :xsmall
)
sf.text_field(
name: :file_max_size_displayed,
type: :number,
@@ -0,0 +1,58 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::AddFilterForm < ApplicationForm
def initialize(allowed_filters:, active_filter_names:)
super()
@allowed_filters = allowed_filters
@active_filter_names = active_filter_names
end
form do |form|
form.select_list(
name: :add_filter_select,
label: I18n.t(:label_filter_add),
scope_name_to_model: false,
prompt: I18n.t(:actionview_instancetag_blank_option),
data: {
"filter--filters-form-target": "addFilterSelect",
action: "change->filter--filters-form#addFilter:prevent"
}
) do |select|
@allowed_filters.each do |filter|
select.option(
label: filter.human_name,
value: filter.name,
disabled: @active_filter_names.include?(filter.name)
)
end
end
end
end
@@ -0,0 +1,54 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::AutocompleteForm < Filters::Inputs::BaseFilterForm
def add_operand(group)
group.autocompleter(
name: operand_name,
label: @filter.human_name,
visually_hide_label: true,
required: true,
wrapper_classes: ["advanced-filters--filter-value"],
wrapper_data_attributes: {
"filter--filters-form-target": "filterValueContainer",
"filter-name": @filter.name,
"filter-autocomplete": "true"
},
autocomplete_options: @additional_attributes[:autocomplete_options].merge(
id: operand_name,
multiple: true,
multipleAsSeparateInputs: false,
inputName: "value",
inputValue: @filter.values,
hiddenFieldAction: "change->filter--filters-form#autocompleteSendForm"
)
)
end
end
@@ -0,0 +1,135 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::BaseFilterForm < ApplicationForm
def initialize(filter:, additional_attributes:, active:)
super()
@filter = filter
@additional_attributes = additional_attributes
@active = active
end
def self.inherited(subclass)
super
subclass.form do |form|
form.group(**filter_row_arguments) do |group|
add_label(group)
add_operator(group)
add_operand(group)
add_delete_button(group)
end
end
end
protected
def add_label(group)
filter_human_name = @filter.human_name
for_id = operator_hidden? ? operand_input_id : "operator_#{@filter.name}"
group.html_content do
content_tag(:label, filter_human_name, class: "advanced-filters--filter-name", for: for_id)
end
end
def operator_hidden?
@filter.is_a?(Queries::Filters::Shared::BooleanFilter)
end
def operand_input_id
nil
end
def add_operand(_group)
raise SubclassResponsibilityError
end
def filter_row_arguments
args = {
layout: :horizontal,
classes: "advanced-filters--filter",
data: {
"filter--filters-form-target": "filter",
"filter-name": @filter.name,
"filter-type": @filter.type
}
}
args[:hidden] = "hidden" unless @active
args
end
def operand_name
"#{@filter.name}_value"
end
private
def add_operator(group)
selected_operator = @filter.operator || @filter.default_operator.symbol
group.select_list(
name: :"operator_#{@filter.name}",
label: @filter.human_name,
visually_hide_label: true,
scope_name_to_model: false,
hidden: @filter.is_a?(Queries::Filters::Shared::BooleanFilter),
data: {
action: "change->filter--filters-form#setValueVisibility",
"filter--filters-form-filter-name-param": @filter.name,
"filter--filters-form-target": "operator",
"filter-name": @filter.name
}
) do |select|
@filter.available_operators.each do |operator|
select.option(
label: operator.human_name,
value: operator.symbol,
selected: operator.symbol == selected_operator,
**(operator.requires_value? ? {} : { "data-no-value": true })
)
end
end
end
def add_delete_button(group)
filter_name = @filter.name
group.html_content do
render(Primer::Beta::IconButton.new(
icon: :x,
scheme: :invisible,
classes: "advanced-filters--remove-filter",
aria: { label: I18n.t("button_delete") },
data: {
action: "click->filter--filters-form#removeFilter",
"filter--filters-form-filter-name-param": filter_name
}
))
end
end
end
+48
View File
@@ -0,0 +1,48 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::BooleanForm < Filters::Inputs::BaseFilterForm
def add_operand(group)
group.segmented_control(
name: operand_name,
label: @filter.human_name,
visually_hide_label: true,
value: @filter.values.first,
items: [
{ value: "f", label: I18n.t("general_text_No") },
{ value: "t", label: I18n.t("general_text_Yes") }
],
wrapper_data_attributes: {
"filter--filters-form-target": "filterValueContainer",
"filter-name": @filter.name
}
)
end
end
+116
View File
@@ -0,0 +1,116 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::DateForm < Filters::Inputs::BaseFilterForm
DAYS_OPERATORS = %w[>t- <t- t- <t+ >t+ t+].freeze
def add_operand(group)
filter_name = @filter.name
filter_values = @filter.values
fmt = date_format
days_value = fmt == "days" ? filter_values.fetch(0, "") : nil
on_date_value = fmt == "on-date" ? filter_values.fetch(0, "") : nil
from_value = fmt == "between-dates" ? filter_values.fetch(0, "") : nil
to_value = fmt == "between-dates" ? filter_values.fetch(1, "") : nil
# The multi name intentionally uses @filter.name (not operand_name) because
# parseDateFilterValue in filters-form.controller.ts locates the datepicker
# inputs via findTargetById(filterName, …), which matches the id derived from
# the multi name. Switching to operand_name would require migrating that
# lookup to findTargetByName and adding data-filter-name to the picker inputs.
group.multi(name: filter_name, label: filter_name, visually_hide_label: true,
class: ["advanced-filters--filter-value"],
data: {
"filter--filters-form-target": "filterValueContainer",
"filter-name": filter_name
}) do |builder|
days_div(builder, filter_name, days_value)
on_date_div(builder, filter_name, on_date_value)
between_dates_div(builder, filter_name, from_value, to_value)
end
end
private
def date_format
op = @filter.operator || @filter.default_operator.symbol
@date_format ||= if DAYS_OPERATORS.include?(op)
"days"
elsif op == "=d"
"on-date"
elsif op == "<>d"
"between-dates"
end
end
def days_div(builder, filter_name, value)
field_arguments = {
name: :days,
label: I18n.t("datetime.units.day.other"),
visually_hide_label: true,
trailing_visual: { text: { text: I18n.t("datetime.units.day.other") } },
scope_name_to_model: false,
value:,
hidden: value.nil?,
data: {
"filter--filters-form-target": "days",
"filter-name": filter_name
}
}
builder.text_field(**field_arguments, type: :number, step: "any", class: "days")
end
def on_date_div(builder, filter_name, value)
builder.single_date_picker(
name: :singleDay,
label: :singleDay,
hidden: value.nil?,
leading_visual: { icon: :calendar },
value: value || "",
datepicker_options: { input_attributes: { "data-filter--filters-form-target" => "singleDay" } },
data: { "filter-name": filter_name }
)
end
def between_dates_div(builder, filter_name, from_value, to_value)
value = [from_value, to_value].compact.join(" - ").presence
builder.range_date_picker(
name: :dateRange,
label: :dateRange,
hidden: value.nil?,
leading_visual: { icon: :calendar },
value: value || "-",
datepicker_options: { input_attributes: { "data-filter--filters-form-target" => "dateRange" } },
data: { "filter-name": filter_name }
)
end
end
+64
View File
@@ -0,0 +1,64 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::ListForm < Filters::Inputs::BaseFilterForm
def add_operand(group)
filter_name = @filter.name
filter_values = @filter.values || []
items = @filter.allowed_values.map { |name, id| { name:, id: } }
group.autocompleter(
name: operand_name,
label: :value,
visually_hide_label: true,
wrapper_classes: ["advanced-filters--filter-value"],
wrapper_data_attributes: {
"filter--filters-form-target": "filterValueContainer",
"filter-name": filter_name,
"filter-autocomplete": "true"
},
autocomplete_options: {
component: "opce-autocompleter",
id: operand_name,
multiple: true,
multipleAsSeparateInputs: false,
inputName: "value",
inputValue: filter_values,
items:,
model: items.select { |item| filter_values.include?(item[:id]) },
bindLabel: "name",
bindValue: "id",
hideSelected: true,
defaultData: false,
hiddenFieldAction: "change->filter--filters-form#autocompleteSendForm"
}.merge(@additional_attributes[:autocomplete_options] || {})
)
end
end
+50
View File
@@ -0,0 +1,50 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class Filters::Inputs::TextForm < Filters::Inputs::BaseFilterForm
def add_operand(group)
field_arguments = {
name: operand_name,
label: @filter.human_name,
visually_hide_label: true,
scope_name_to_model: false,
value: @filter.values.first,
data: {
"filter--filters-form-target": "filterValueContainer simpleValue",
"filter-name": @filter.name
}
}
if @filter.type.in? %i[integer float]
group.text_field(**field_arguments, type: :number, step: "any")
else
group.text_field(**field_arguments)
end
end
end
-2
View File
@@ -49,8 +49,6 @@ module FrontendAssetHelper
# or referencing the running CLI proxy that hosts the assets in memory.
def include_frontend_assets
capture do
concat nonced_javascript_include_tag variable_asset_path("jquery.js"), skip_pipeline: true
%w(polyfills.js main.js).each do |file|
concat nonced_javascript_include_tag variable_asset_path(file), skip_pipeline: true, type: "module"
end
+7 -1
View File
@@ -338,7 +338,7 @@ module SortHelper
# Extracts the given `options` and provides them to a block.
# See #sort_header_tag and #sort_header_with_action_menu for usage examples.
def with_sort_header_options(column, allowed_params: nil, with_title: false, **options)
def with_sort_header_options(column, allowed_params: nil, with_title: false, **options) # rubocop:disable Metrics/AbcSize
caption = get_caption(column, options)
default_order = options.delete(:default_order) || "asc"
@@ -349,6 +349,12 @@ module SortHelper
options[:title] = sort_header_title(column, caption, options) if with_title
options[:icon_only_header] = column == :favorited
# Give the header cell an explicit accessible name equal to the plain caption.
# Without it, the column header's accessible name is derived from its contents,
# which can include the action menu trigger text and is uppercased by the
# `text-transform` styling, neither of which should be part of the name.
options[:"aria-label"] = caption if caption.present?
within_sort_header_tag_hierarchy(options, sort_class(column)) do
yield(column, caption, default_order, allowed_params:, param:, lang:, title: options[:title],
sortable: options.fetch(:sortable, false), data:)
+28 -2
View File
@@ -93,7 +93,9 @@ class Attachment < ApplicationRecord
# specifically when using S3 for attachments. In the case of S3 the file name for the downloaded
# file will still be correct as it's part of the URL before the query.
def external_url_options(expires_in: nil)
{ content_disposition: content_disposition(include_filename: false), expires_in: }
{ content_disposition: content_disposition(include_filename: false),
content_type: served_content_type,
expires_in: }
end
def external_storage?
@@ -119,6 +121,30 @@ class Attachment < ApplicationRecord
end
end
# Returns the Content-Type to use when serving this file inline in a browser.
# Text files are normalised to text/plain (prevents script execution) with an
# explicit charset. Non-inlineable files get application/octet-stream so the
# browser is forced to download them.
def served_content_type
if is_text?
"text/plain; charset=#{charset.presence || Setting.attachment_default_charset}"
elsif inlineable?
content_type
else
"application/octet-stream"
end
end
# Returns the content type to use when serving the file to a browser.
# For text files, ensures a charset is always present so browsers don't
# fall back to ISO-8859-1. Preserves the real MIME subtype (e.g. text/x-ruby)
# unlike served_content_type which normalises to text/plain for security.
def serving_content_type
return content_type unless is_text?
"#{content_type}; charset=#{charset.presence || Setting.attachment_default_charset}"
end
def visible?(user = User.current)
allowed_or_author?(user) do
container.attachments_visible?(user)
@@ -227,7 +253,7 @@ class Attachment < ApplicationRecord
end
def set_content_type(file)
self.content_type = self.class.content_type_for(file.path)
self.content_type, self.charset = OpenProject::ContentTypeDetector.new(file.path).detect_with_charset
end
def set_digest(file)
+2 -1
View File
@@ -47,7 +47,8 @@ module Exports
def encode_csv_columns(columns, encoding = I18n.t(:general_csv_encoding))
columns.map do |cell|
Redmine::CodesetUtil.from_utf8(cell.to_s, encoding)
sanitized = Exports::Concerns::CSVFormulaSanitization.sanitize(cell)
Redmine::CodesetUtil.from_utf8(sanitized, encoding)
end
end
@@ -0,0 +1,76 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Exports
module Concerns
# Escape cells that begin with a spreadsheet formula-triggering character,
# if the +csv_escape_formulas+ setting is active.
module CSVFormulaSanitization
module_function
# Leading characters that always indicate a formula and will always be escaped
ALWAYS_ESCAPE = %W[= @ \t \r].freeze
# Leading characters that trigger formula evaluation but may also legitimately
# be a number (negative/positive values).
POSSIBLE_NUMBER_START = %w[- +].freeze
# A single, optionally signed number as it appears in exported cells:
# thousands/decimal separators, optional surrounding whitespace and an
# optional currency symbol or percent sign (e.g. "-5.00", "-1.234,56 €").
# Anything with an internal operator (e.g. "+1+1") or letters/parentheses
# fails this match and will be treated as a formula again
PLAIN_NUMBER = /\A[+-]?[\p{Sc}\s]*\d[\d.,'\s]*[\p{Sc}%]?\z/u
# Escape a single CSV cell value if the setting is active
#
# @param value [Object] the raw cell value
# @return [String] the (possibly escaped) string value
def sanitize(value)
str = value.to_s
return str unless needs_escaping?(str)
"'#{str}"
end
def needs_escaping?(str)
return false unless Setting.csv_escape_formulas?
return false if str.empty?
first = str[0]
return true if ALWAYS_ESCAPE.include?(first)
return false unless POSSIBLE_NUMBER_START.include?(first)
# Leading - or +: only escape when the value is not a plain signed number.
!str.match?(PLAIN_NUMBER)
end
end
end
end
+9 -1
View File
@@ -31,7 +31,7 @@
class PersistedView < ApplicationRecord
belongs_to :project, optional: true
belongs_to :principal, optional: true, inverse_of: :persisted_views
belongs_to :query, polymorphic: true, optional: true
belongs_to :query, polymorphic: true, optional: true, autosave: true
belongs_to :parent, class_name: "PersistedView", optional: true
has_many :children, class_name: "PersistedView", foreign_key: "parent_id", dependent: :destroy, inverse_of: :parent
@@ -49,6 +49,7 @@ class PersistedView < ApplicationRecord
scope :public_views, -> { where(public: true) }
scope :private_views, ->(principal = User.current) { where(public: false, principal_id: principal.id) }
scope :with_children, -> { includes(:children) }
scope :visible, (lambda do |principal = User.current|
public_views.or(private_views(principal))
@@ -68,6 +69,13 @@ class PersistedView < ApplicationRecord
attr_writer :allowed_children
end
# Resolves a (potentially user-supplied) class name to a view class, but only
# if it is an allowed child of this view. Returns nil for any unknown or
# forbidden name.
def self.allowed_child_class(name)
allowed_children.index_with(&:constantize)[name.to_s]
end
# Returns the query of this view or, if not set, the query of the parent view.
def effective_query
query || parent&.effective_query
+4 -1
View File
@@ -85,6 +85,8 @@ class Project < ApplicationRecord
}, dependent: :destroy
has_many :time_entries, dependent: :delete_all
has_many :time_entry_activities_projects, dependent: :delete_all
has_many :cost_types_projects, dependent: :delete_all
has_many :cost_types, through: :cost_types_projects
has_many :queries, dependent: :destroy
has_many :news, -> { includes(:author) }, dependent: :destroy
has_many :categories, -> { order("#{Category.table_name}.name") }, dependent: :delete_all
@@ -193,7 +195,8 @@ class Project < ApplicationRecord
scope :templated, -> { where(templated: true) }
scopes :activated_time_activity,
:visible_with_activated_time_activity
:visible_with_activated_time_activity,
:available_cost_types
enum :status_code, {
on_track: 0,
+4
View File
@@ -182,6 +182,10 @@ class Queries::Filters::Base
errors.full_message(human_name, messages)
end
def autocomplete_options
{}
end
protected
def type_strategy
@@ -37,6 +37,23 @@ module Queries
true
end
def autocomplete_options
items = allowed_values.map do |name, id|
path = name.split(" / ")
{ name: path.last, id:, depth: path.length - 1 }
end
{
component: "opce-autocompleter",
bindValue: "id",
bindLabel: "name",
hideSelected: true,
defaultData: false,
items:,
model: items.select { |item| values.include?(item[:id]) }
}
end
def value_objects
CustomField::Hierarchy::Item
.where(id: @values)
@@ -59,6 +59,24 @@ module Queries::Filters::Shared
:list_optional
end
def autocomplete_options # rubocop:disable Metrics/AbcSize
all_items = if custom_field.version?
allowed_values.map { |name, id, project_name| { name:, id:, project_name: } }
else
allowed_values.map { |name, id| { name:, id: } }
end
options = { items: all_items }
options[:groupBy] = "project_name" if custom_field.version?
{
component: "opce-autocompleter",
bindValue: "id",
bindLabel: "name",
hideSelected: true,
defaultData: false
}.merge(options).merge(model: all_items.select { |item| values.include?(item[:id]) })
end
protected
def condition
@@ -75,8 +93,7 @@ module Queries::Filters::Shared
end
def customized_strategy?
operator_strategy == Queries::Operators::CustomFields::EqualsAll ||
operator_strategy == Queries::Operators::CustomFields::NotEqualsAll
[Queries::Operators::CustomFields::EqualsAll, Queries::Operators::CustomFields::NotEqualsAll].include?(operator_strategy)
end
def type_strategy_class
@@ -48,6 +48,22 @@ module Queries::Filters::Shared
vals + user_groups_added(vals)
end
def autocomplete_options
{
component: "opce-user-autocompleter",
hideSelected: true,
defaultData: false,
placeholder: I18n.t(:label_user_search),
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
filters: [
{ name: "status", operator: "!", values: [Principal.statuses["locked"].to_s] }
],
searchKey: "any_name_attribute",
focusDirectly: false
}
end
private
def group_members_added(vals)
@@ -51,6 +51,14 @@ module Queries::Filters::Shared::ProjectFilter::Optional
# harm.
@type_strategy ||= ::Queries::Filters::Strategies::IntegerListOptional.new(self)
end
def autocomplete_options
{
component: "opce-project-autocompleter",
resource: "projects",
filters: [{ name: "active", operator: "=", values: ["t"] }]
}
end
end
module ClassMethods
@@ -51,6 +51,14 @@ module Queries::Filters::Shared::ProjectFilter::Required
# harm.
@type_strategy ||= ::Queries::Filters::Strategies::IntegerList.new(self)
end
def autocomplete_options
{
component: "opce-project-autocompleter",
resource: "projects",
filters: [{ name: "active", operator: "=", values: ["t"] }]
}
end
end
module ClassMethods
@@ -41,6 +41,19 @@ class Queries::Projects::Filters::ProjectStatusFilter < Queries::Projects::Filte
:list_optional
end
def autocomplete_options
all_items = allowed_values.map { |name, id| { name:, id: } }
{
component: "opce-autocompleter",
bindValue: "id",
bindLabel: "name",
hideSelected: true,
defaultData: false,
items: all_items,
model: all_items.select { |item| values.include?(item[:id]) }
}
end
def where
operator_strategy.sql_for_field(values, model.table_name, :status_code)
end
@@ -45,6 +45,19 @@ class Queries::Projects::Filters::TypeFilter < Queries::Projects::Filters::Base
:list
end
def autocomplete_options
all_items = allowed_values.map { |name, id| { name:, id: } }
{
component: "opce-autocompleter",
bindValue: "id",
bindLabel: "name",
hideSelected: true,
defaultData: false,
items: all_items,
model: all_items.select { |item| values.include?(item[:id]) }
}
end
def self.key
:type_id
end
@@ -34,4 +34,16 @@ class Queries::Users::Filters::GroupFilter < Queries::Users::Filters::UserFilter
def human_name
I18n.t(:label_group)
end
def autocomplete_options
{
component: "opce-user-autocompleter",
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
filters: [{ name: "type", operator: "=", values: %w[Group] }],
searchKey: "any_name_attribute",
inputValue: values,
bindValue: "id"
}
end
end
@@ -37,6 +37,27 @@ module Queries::WorkPackages::Filter::FilterForWpMixin
raise NotImplementedError, "There would be too many candidates"
end
# Tell `Filters::FilterFormComponent`'s dispatch to render these filters with a
# server-side autocompleter (the candidate set is too large for an inline
# `<select>`). Mirrors what the legacy Angular WP filter UI does — see
# `filter-searchable-multiselect-value.component.html`, which renders an
# `op-autocompleter` against the `work_packages` resource using the
# `typeahead` filter for search.
#
# Subclasses that override `type` to something other than `:list` (e.g.
# `SearchFilter` / `SubjectOrIdFilter` use `:search`, `RelatableFilter`
# uses `:relation`) get an empty hash so the dispatch falls back to the
# appropriate non-autocomplete input.
def autocomplete_options
return {} unless type == :list
{
component: "opce-autocompleter",
resource: "work_packages",
searchKey: "typeahead"
}
end
def value_objects
objects = visible_scope.find(no_templated_values)
+20
View File
@@ -63,6 +63,7 @@ class Query < ApplicationRecord
validate :validate_timestamps
include Scopes::Scoped
scopes :visible,
:having_views
@@ -208,6 +209,25 @@ class Query < ApplicationRecord
filters.delete_if { |f| f.field.to_s == name.to_s }
end
# Mirrors `Queries::BaseQuery#find_active_filter` so that consumers built
# on top of the modern query API (e.g. `Filters::FilterFormComponent`) can ask any
# query — including this legacy work-package one — for its active filter
# by name. Signature kept identical to BaseQuery's (symbol arg in, filter
# or nil out).
def find_active_filter(name)
filters.detect { |f| f.name == name }
end
# The manual-sort filter is added programmatically when the user drags
# work packages to reorder them — it has no operator/value UI of its own
# (type `:empty_value`), so it doesn't belong in the picker that
# `Filters::FilterFormComponent` builds. Mirrors how
# `Queries::Filters::AvailableFilters#available_advanced_filters` already
# excludes the inline `name_and_identifier` quick-filter on projects.
def available_advanced_filters
super.grep_v(::Queries::WorkPackages::Filter::ManualSortFilter)
end
def normalized_name
name.parameterize.underscore
end
+2
View File
@@ -85,6 +85,8 @@ module Authorization
if perms.blank?
if !OpenProject::AccessControl.disabled_permission?(action)
# See https://www.openproject.org/docs/development/concepts/permissions/#definition-of-permissions
# if you are wondering where to define permissions
Rails.logger.debug { "Used permission \"#{action}\" that is not defined. It will never return true." }
raise UnknownPermissionError.new(action) if raise_on_unknown
end
@@ -52,6 +52,8 @@ class WorkPackages::ActivitiesTab::Paginator
include Pagy::Method
include WorkPackages::ActivitiesTab::JournalSortingInquirable
MAX_PAGES = 100
def self.paginate(work_package, params = {})
new(work_package, params).call
end
@@ -72,7 +74,7 @@ class WorkPackages::ActivitiesTab::Paginator
@filter = :all # Ignore filter when jumping to specific journal
pagy_array_for_target_journal(anchor_type, target_record_id)
else
pagy(:offset, base_journals, **pagy_options)
pagy(:offset, capped_journals, **pagy_options)
end
# For UI display: if user wants "oldest first" UI, reverse the array
@@ -87,7 +89,6 @@ class WorkPackages::ActivitiesTab::Paginator
{
page: params[:page] || 1,
limit: params[:limit] || Pagy::DEFAULT[:limit],
max_pages: 100,
request: { params: }
}.compact
end
@@ -127,6 +128,11 @@ class WorkPackages::ActivitiesTab::Paginator
combine_and_sort_records(fetch_journals, fetch_revisions)
end
def capped_journals
max_records = (params[:limit] || Pagy::DEFAULT[:limit]) * MAX_PAGES
base_journals.first(max_records)
end
def fetch_journals
API::V3::Activities::ActivityEagerLoadingWrapper.wrap(fetch_ar_journals)
end
+13 -9
View File
@@ -74,6 +74,7 @@ class FogFileUploader < CarrierWave::Uploader::Base
#
# @param options [Hash] Options hash.
# @option options [String] :content_disposition Pass this content disposition to S3 so that it serves the file with it.
# @option options [String] :content_type Pass this content type to S3 so that it serves the file with it.
# @option options [DateTime] :expires_at Date at which the link should expire (default: now + 5 minutes)
# @option options [ActiveSupport::Duration] :expires_in Duration in which the link should expire.
#
@@ -82,6 +83,7 @@ class FogFileUploader < CarrierWave::Uploader::Base
url_options = {}
set_content_disposition!(url_options, options:)
set_content_type!(url_options, options:)
set_expires_at!(url_options, options:)
remote_file.url url_options
@@ -103,15 +105,17 @@ class FogFileUploader < CarrierWave::Uploader::Base
private
def set_content_disposition!(url_options, options:)
if options[:content_disposition].present?
url_options[:query] = {
# Passing this option to S3 will make it serve the file with the
# respective content disposition. Without it no content disposition
# header is sent. This only works for S3 but we don't support
# anything else anyway (see carrierwave.rb).
"response-content-disposition" => options[:content_disposition]
}
end
return if options[:content_disposition].blank?
(url_options[:query] ||= {})["response-content-disposition"] = options[:content_disposition]
end
def set_content_type!(url_options, options:)
return if options[:content_type].blank?
# Like the content disposition above, this makes S3 serve the file with the
# given Content-Type, overriding the stored object type.
(url_options[:query] ||= {})["response-content-type"] = options[:content_type]
end
def set_expires_at!(url_options, options:)
@@ -0,0 +1,56 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 3.
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
Copyright (C) 2006-2013 Jean-Philippe Lang
Copyright (C) 2010-2013 the ChiliProject Team
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<% html_title t(:label_administration), t(:label_export_plural) %>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t(:label_export_plural) }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_general_path, text: t(:label_system_settings) },
t(:label_export_plural)]
)
end
%>
<%=
settings_primer_form_with(
url: admin_settings_exports_path,
scope: :settings,
method: :patch
) do |f|
render(
Primer::Forms::FormList.new(
Admin::Settings::ExportsSettingsForm.new(f),
Admin::Settings::Save.new(f)
)
)
end
%>
@@ -41,9 +41,9 @@ See COPYRIGHT and LICENSE files for more details.
%>
<%= styled_form_tag(
admin_settings_working_days_and_hours_path,
method: :patch,
class: "op-working-days-admin-settings"
confirm_changes_admin_settings_working_days_and_hours_path,
class: "op-working-days-admin-settings",
data: { turbo_stream: true }
) do %>
<section class="form--section">
@@ -13,20 +13,20 @@
<% if body_subheader %>
<tr>
<td style="<%= placeholder_text_styles("font-weight": "bold") %>">
<%= format_text body_subheader %>
<%= format_mail_html body_subheader %>
</td>
</tr>
<% end %>
<% if body_header %>
<tr>
<td style="<%= placeholder_text_styles(color: "#333333", "line-height": "36px", "font-size": "18px", "font-weight": "bold") %>">
<%= format_text body_header %>
<%= format_mail_html body_header %>
</td>
</tr>
<% end %>
<tr>
<td style="<%= placeholder_text_styles %>">
<%= format_text body %>
<%= format_mail_html body %>
</td>
</tr>
</table>
-22
View File
@@ -1,22 +0,0 @@
<%= content_tag :div,
class: "advanced-filters--filter-value #{value_visibility}",
data: {
filter_autocomplete: true,
filter__filters_form_target: "filterValueContainer",
filter_name: filter.name
} do %>
<%= angular_component_tag autocomplete_options[:component],
inputs: {
inputName: "value",
multiple: true,
multipleAsSeparateInputs: false,
inputValue: filter.values,
id: "#{filter.name}_value",
hiddenFieldAction: "change->filter--filters-form#autocompleteSendForm"
# Set on the template as there is a timing issue where the
# angular component is not yet ready in the DOM and hence
# the action can't be registered on the input field at the time
# of the #connect lifecycle hook of the filter--filters-form
# Stimulus controller.
}.merge(autocomplete_options.except(:component)) %>
<% end %>
-12
View File
@@ -1,12 +0,0 @@
<label class="advanced-filters--filter-value"
data-filter--filters-form-target="filterValueContainer"
data-filter-name="<%= filter.name %>">
<%= angular_component_tag "opce-spot-switch",
inputs: {
name: "v-" + filter.class.key.to_s,
checked: filter.values.first != "f",
filterName: "boolean",
id: "#{filter.name}_value"
} %>
</label>
<div class="advanced-filters--filter-operator" style="visibility: hidden"></div>
-47
View File
@@ -1,47 +0,0 @@
<div class="advanced-filters--filter-value <%= value_visibility %>"
data-filter--filters-form-target="filterValueContainer"
data-filter-name="<%= filter.name %>">
<% if filter.type == :string %>
<div>
<%= text_field_tag :value,
filter.values.first,
class: "advanced-filters--text-field -slim",
data: {
"filter--filters-form-target": "simpleValue",
"filter-name": filter.name
} %>
</div>
<% elsif filter.type == :text %>
<div>
<%= text_field_tag :value,
filter.values.first,
class: "advanced-filters--text-field -slim",
data: {
"filter--filters-form-target": "simpleValue",
"filter-name": filter.name
} %>
</div>
<% elsif filter.type == :integer %>
<div>
<%= number_field_tag :value,
filter.values.first,
class: "advanced-filters--text-field -slim",
step: "any",
data: {
"filter--filters-form-target": "simpleValue",
"filter-name": filter.name
} %>
</div>
<% elsif filter.type == :float %>
<div>
<%= number_field_tag :value,
filter.values.first,
class: "advanced-filters--text-field -slim",
step: "any",
data: {
"filter--filters-form-target": "simpleValue",
"filter-name": filter.name
} %>
</div>
<% end %>
</div>
@@ -1,16 +0,0 @@
<div class="between-dates">
<span><%= t(:label_date_from) %>:</span>
<%= angular_component_tag "opce-basic-single-date-picker",
inputs: {
value: from_value,
id: "between-dates-from-value-#{filter.name}",
name: "from_value"
} %>
<span><%= t(:label_date_to) %>:</span>
<%= angular_component_tag "opce-basic-single-date-picker",
inputs: {
value: to_value,
id: "between-dates-to-value-#{filter.name}",
name: "to_value"
} %>
</div>
-15
View File
@@ -1,15 +0,0 @@
<div class="days">
<div class="form--field-container">
<div class="form--text-field-container -xslim">
<span class="inline-label">
<%= number_field_tag :value,
value,
id: "#{filter.name}_value",
class: "advanced-filters--text-field -slim",
"data-filter--filters-form-target": "days",
"data-filter-name": filter.name %>
<label for="<%= "#{filter.name}_value" %>" class="form-label -transparent"><%= t("datetime.units.day.other") %></label>
</span>
</div>
</div>
</div>
@@ -1,24 +0,0 @@
<%
value_format = ""
if [">t-", "<t-", "t-", "<t+", ">t+", "t+"].include? selected_operator
value_format = "days"
elsif selected_operator == "=d"
value_format = "on-date"
elsif selected_operator == "<>d"
value_format = "between-dates"
end
%>
<div class="advanced-filters--filter-value <%= value_visibility %> <%= value_format %>"
data-filter--filters-form-target="filterValueContainer"
data-filter-name="<%= filter.name %>">
<%= render partial: "filters/date/days",
locals: { filter: filter,
value: value_format == "days" ? filter.values.first : "" } %>
<%= render partial: "filters/date/on_date",
locals: { filter: filter,
value: value_format == "on-date" ? filter.values.first : "" } %>
<%= render partial: "filters/date/between_dates",
locals: { filter: filter,
from_value: value_format == "between-dates" ? filter.values.first : "",
to_value: value_format == "between-dates" ? filter.values.second : "" } %>
</div>
-10
View File
@@ -1,10 +0,0 @@
<div class="on-date">
<div class="form--field-container -visible-overflow">
<%= angular_component_tag "opce-basic-single-date-picker",
inputs: {
value: value,
id: "on-date-value-#{filter.name}",
name: "value"
} %>
</div>
</div>
@@ -1,8 +0,0 @@
<% filter_values = filter.values || [] %>
<div class="advanced-filters--filter-value <%= value_visibility %> <%= filter_values.size > 1 ? "multi-value" : "" %>"
data-filter--filters-form-target="filterValueContainer"
data-filter-name="<%= filter.name %>">
<%= render partial: "filters/list/select", locals: { filter: filter, selected_values: filter_values.first, multi_value: false } %>
<%= render partial: "filters/list/select", locals: { filter: filter, selected_values: filter_values, multi_value: true } %>
</div>
-30
View File
@@ -1,30 +0,0 @@
<div class="<%= multi_value ? "multi-select" : "single-select" %>">
<span class="inline-label">
<% select_options = [:value,
options_from_collection_for_select(
filter.allowed_values,
:second,
:first,
selected_values
),
{
class: "form--select -slim",
"data-filter--filters-form-target": "filterValueSelect",
"data-filter-name": filter.name,
id: "#{filter.name}_value"
}]
if multi_value
select_options.third[:multiple] = true
end %>
<%= select_tag *select_options %>
<a href=""
class="form-label no-decoration-on-hover -transparent multi-select-toggle"
tabindex="0"
data-action="click->filter--filters-form#toggleMultiSelect"
data-filter--filters-form-filter-name-param="<%= filter.name %>">
<span class="icon-context icon-button <%= multi_value ? "icon-minus2" : "icon-add" %> icon4">
<span class="sr-only"><%= t(:label_enable_multi_select) %></span>
</span>
</a>
</span>
</div>
+2 -2
View File
@@ -102,7 +102,7 @@ See COPYRIGHT and LICENSE files for more details.
<table class="header">
<tr>
<td>
<%= OpenProject::TextFormatting::Renderer.format_text(Setting.localized_emails_header) %>
<%= format_mail_html(Setting.localized_emails_header) %>
</td>
</tr>
</table>
@@ -118,7 +118,7 @@ See COPYRIGHT and LICENSE files for more details.
<table class="footer">
<tr>
<td>
<%= OpenProject::TextFormatting::Renderer.format_text(Setting.localized_emails_footer) %>
<%= format_mail_html(Setting.localized_emails_footer) %>
</td>
</tr>
</table>
@@ -6,7 +6,7 @@
)
%>
<%= format_text(@message) %>
<%= format_mail_html(@message) %>
<%= I18n.t(:"mail_member_added_project.body.roles") %>
<ul>
@@ -5,7 +5,7 @@
)
%>
<%= format_text(@message) %>
<%= format_mail_html(@message) %>
<%= I18n.t(:"mail_member_updated_global.body.roles") %>
<ul>
@@ -6,7 +6,7 @@
)
%>
<%= format_text(@message) %>
<%= format_mail_html(@message) %>
<%= I18n.t(:"mail_member_updated_project.body.roles") %>
<ul>
@@ -29,7 +29,7 @@ See COPYRIGHT and LICENSE files for more details.
<h1><%= link_to @project.name, project_url(@project) %></h1>
<%= format_text(@notification_text, project: @project) %>
<%= format_mail_html(@notification_text, project: @project) %>
<% if @work_package&.visible? %>
<p>
@@ -29,7 +29,7 @@ See COPYRIGHT and LICENSE files for more details.
<h1><%= link_to @project.name, project_url(@project) %></h1>
<%= format_text(@notification_text, project: @project) %>
<%= format_mail_html(@notification_text, project: @project) %>
<% if @project.project_creation_wizard_enabled? %>
<p>

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