Release OpenProject 17.1.0

This commit is contained in:
Oliver Günther
2026-02-11 10:33:25 +01:00
1855 changed files with 56244 additions and 26779 deletions
+3
View File
@@ -44,6 +44,9 @@ PORT=3000
FE_HOST=localhost
FE_PORT=4200
# Default TLD for docker dev stack (e.g. when set to "local", services will be openproject.local, nextcloud.local, etc.)
OPENPROJECT_DOCKER_DEV_TLD=local
# Use this variables to configure hostnames for frontend and backend, e.g. to enable HTTPS in docker development setup
OPENPROJECT_DEV_HOST=localhost
OPENPROJECT_DEV_URL=http://${OPENPROJECT_DEV_HOST}:${FE_PORT}
+38 -283
View File
@@ -1,336 +1,91 @@
# OpenProject Coding Agent Instructions
## Repository Overview
See [AGENTS.md](../AGENTS.md) for all agent instructions.
**OpenProject** is a web-based, open-source project management software written in Ruby on Rails. It uses PostgreSQL for data persistence and supports features like project planning, task management, Agile/Scrum, time tracking, wikis, and forums.
## Additional Context for GitHub Copilot
- **Size**: Large monorepo (~840MB, ~1M+ lines of code)
- **History**: Originally forked from Redmine over a decade ago, evolved significantly as an independent project
- **Backend**: Ruby 3.4.5, Rails ~8.0.3
- **Frontend**: Node.js 22.21.0, npm 10.1.0+, TypeScript
- **Database**: PostgreSQL (required)
- **Architecture**: Server-rendered HTML with Hotwire (Turbo + Stimulus). Legacy Angular components exist and are being migrated to custom elements. Uses GitHub's Primer Design System via ViewComponent.
- **Editions**: OpenProject comes in Community and Enterprise editions
- **Enterprise Edition**: Includes additional features like Single sign-on (OIDC & SAML), LDAP, Nextcloud integration, SCIM API, and more (requires token for development)
- **BIM Edition**: Tailored for construction industry needs. Code in `modules/bim/`, docs in `docs/bim-guide/`. Existing instances can be switched to BIM edition.
### Common Issues and Workarounds
## Critical Setup Requirements
### Ruby and Node Versions
**ALWAYS verify versions before building:**
- Ruby: `3.4.5` (see `.ruby-version`)
- Node: `^22.21.0` (see `package.json` engines)
- Bundler: Latest 2.x
### Development Environment Options
**Docker (Recommended for Quick Start)**
```bash
# ALWAYS run these commands in sequence:
cp .env.example .env
cp docker-compose.override.example.yml docker-compose.override.yml
docker compose run --rm backend setup
docker compose run --rm frontend npm install
docker compose up -d backend
# Access at http://localhost:3000
```
**Local Development Setup**
```bash
# Install dependencies (ALWAYS run in this order):
bundle install # Install Ruby gems
cd frontend && npm ci && cd .. # Install Node packages (use 'ci' not 'install' for reproducibility)
bundle exec rake db:migrate # Setup database
bundle exec rails openproject:plugins:register_frontend assets:export_locales
# Start services (use bin/dev for all-in-one):
bin/dev # Starts Rails, frontend dev server, and Good Job worker
# OR manually:
# Terminal 1: bundle exec rails server
# Terminal 2: npm run serve
# Terminal 3: bundle exec good_job start
```
**Important**: The `config/database.yml` file MUST NOT exist when using Docker. Delete or rename it if present.
## Building and Testing
### Linting (Run Before Committing)
**Ruby (Rubocop)**
```bash
bundle exec rubocop # Check all files
bin/dirty-rubocop --uncommitted # Check only uncommitted changes
bin/dirty-rubocop --uncommitted --force-exclusion {files} # Check specific files
```
**JavaScript/TypeScript (ESLint)**
```bash
cd frontend
npx eslint src/ # Lint all frontend code
cd ..
```
**ERB Templates (erb_lint)**
```bash
erb_lint {files} # Lint ERB template files
```
**Install Git Hooks** (optional but recommended):
```bash
bundle exec lefthook install # Sets up pre-commit hooks for linting
```
### Running Tests
**Backend Tests (RSpec)**
```bash
# Run specific tests (ALWAYS preferred over running all tests):
bundle exec rspec spec/models/user_spec.rb # Single file
bundle exec rspec spec/models/user_spec.rb:42 # Single line
bundle exec rspec spec/features # Directory
# Run all tests (slow, ~40 minutes on CI):
bundle exec rspec
# Parallel execution (faster):
bundle exec rake parallel:spec
# With Docker:
docker compose run --rm backend-test "bundle exec rspec spec/features/work_package_show_spec.rb"
```
**Frontend Tests (Jasmine/Karma)**
```bash
cd frontend
npm test # Run all frontend unit tests
npm run test:ci # Run in CI mode (single run)
cd ..
```
**Debugging Failed GitHub Actions Tests**
```bash
# Extract and run all failed tests from CI:
./script/github_pr_errors
./script/github_pr_errors | xargs bundle exec rspec
# Run flaky tests multiple times:
./script/bulk_run_rspec spec/path/to/flaky_spec.rb
```
### Running the Application Locally
**Development Mode**
```bash
bin/dev # Uses Overmind or Foreman to start all services
# Access at http://localhost:3000
```
**Individual Services**
```bash
bundle exec rails server # Rails backend (port 3000)
npm run serve # Frontend dev server (proxied through Rails)
bundle exec good_job start # Background job worker
```
## Project Structure
### Key Directories
- `app/` - Rails application code (models, controllers, services, views, components)
- `app/components/` - ViewComponent-based UI components (Ruby + ERB)
- `app/contracts/` - Validation and authorization contracts
- `app/controllers/` - Rails controllers
- `app/models/` - ActiveRecord models
- `app/services/` - Service objects (business logic)
- `app/workers/` - Background job workers
- `config/` - Rails configuration
- `config/application.rb` - Application configuration
- `config/locales/` - I18n translations
- `config/routes.rb` - Rails routes
- `db/` - Database migrations and seeds
- `docker/` - Docker build contexts
- `frontend/src/` - Frontend
- `frontend/src/app/` - Angular modules, components, services (legacy Angular code)
- `frontend/src/main.ts` - Angular Application bootstrap entry point
- `frontend/src/react` - React components (currently only used for experimental BlockNote integration)
- `frontend/src/stimulus` - Stimulus controllers, helpers
- `frontend/src/turbo` - Turbo integration (e.g. custom Turbo Stream actions)
- `lib/` - Ruby libraries and extensions
- `lookbook/` - Lookbook component previews for ViewComponents (see https://github.com/lookbook-hq/lookbook)
- `modules/` - OpenProject plugin modules
- `spec/` - RSpec test suite
- `spec/features/` - System/feature tests (Capybara)
- `spec/models/` - Model unit tests
- `spec/requests/` - API/integration tests
- `spec/services/` - Service tests
### Configuration Files
- `.erb_lint.yml` - ERB template linting
- `.rubocop.yml` - Ruby linting rules
- `.ruby-version` - Ruby version (check this file for current version)
- `docker-compose.yml` - Docker development environment
- `frontend/eslint.config.mjs` - JavaScript/TypeScript linting
- `Gemfile` / `Gemfile.lock` - Ruby dependencies
- `lefthook.yml` - Git hooks configuration
- `package.json` / `frontend/package.json` - Node.js dependencies
- `Procfile.dev` - Services for `bin/dev`
## GitHub Actions CI/CD
### Main Workflows
- **test-core.yml** - Main test suite (units + features, ~40 min, runs on all PRs)
- **rubocop-core.yml** - Ruby linting (runs on all PRs with Ruby changes)
- **eslint-core.yml** - JS/TS linting (runs on all PRs with JS/TS changes)
- **test-frontend-unit.yml** - Frontend unit tests
- **brakeman-scan-core.yml** - Security scanning
- **codeql-scan-core.yml** - Code quality/security analysis
### CI Requirements for Merge
- All linting checks must pass (Rubocop, ESLint, erb_lint)
- Test suite must be green
- No security vulnerabilities introduced (Brakeman, CodeQL)
**Skip CI**: Add `[ci skip]` to commit message to skip CI (use sparingly).
## Common Issues and Workarounds
### Database Configuration
#### Database Configuration
- **Issue**: Docker fails with "database.yml exists"
- **Fix**: Delete or rename `config/database.yml` when using Docker
### Memory Issues in Docker
#### Memory Issues in Docker
- **Issue**: Frontend container exits with status 137
- **Fix**: Increase Docker memory limit to at least 4GB
### Test Failures on CI but Passing Locally
#### Test Failures on CI but Passing Locally
- Run with `CI=true` environment variable (eager loads app)
- Check for `OPENPROJECT_*` environment variables
- Match the random seed: `bundle exec rspec --seed 18352`
- Use `--bisect` to find order-dependent failures
- View browser tests with `OPENPROJECT_TESTING_NO_HEADLESS=1`
### Frontend Build Issues
#### Frontend Build Issues
- **Issue**: "jQuery not defined", frontend asset errors, or blank page
- **Fix**: Run `bin/setup_dev` to rebuild frontend completely
### Parallel Test Failures
#### Parallel Test Failures
- Tests run in parallel on CI with different random seeds per group
- Check `tmp/parallel_runtime.log` for execution times
- **Flaky specs**: Some tests may fail randomly; see `docs/development/running-tests/` for handling flaky tests
- Use `script/bulk_run_rspec` to run tests multiple times to identify flaky behavior
## Code Style Guidelines
### Extended Details
### Ruby
- Follow [Ruby community style guide](https://github.com/bbatsov/ruby-style-guide)
- Use service objects for complex business logic
- Return results using the `ServiceResult` class (well-documented in codebase)
- Some services use monads via [dry-monads](https://github.com/dry-rb/dry-monads) for result modeling
- Use contracts for validation and authorization
- Keep controllers thin, models focused
- Document code units and patterns with [YARD](https://yardoc.org/)
- Write tests for all new features (RSpec)
- Unit tests for models, services, and other components
- Feature specs use Capybara (with Cuprite and Selenium WebDriver)
- Feature specs can use A11y selectors ([capybara_accessible_selectors](https://github.com/citizensadvice/capybara_accessible_selectors)), test IDs, or page objects (in `spec/support/pages/`)
#### Service Objects and Result Modeling
- Return results using the `ServiceResult` class (well-documented in codebase)
- Some services use monads via [dry-monads](https://github.com/dry-rb/dry-monads) for result modeling
### Database Migrations
- Follow Rails migration conventions
#### Testing with Capybara
- Feature specs use Capybara (with Cuprite and Selenium WebDriver)
- Feature specs can use A11y selectors ([capybara_accessible_selectors](https://github.com/citizensadvice/capybara_accessible_selectors)), test IDs, or page objects (in `spec/support/pages/`)
#### Database Migrations
- OpenProject implements migration "squashing" between major releases
- See `docs/development/migrations/` for details on the squashing process
- Migrations are consolidated to manage database changes across major versions
- OpenProject does not currently aim for zero downtime migrations
### JavaScript/TypeScript
- **New development**: Use Hotwire (Turbo + Stimulus) with server-rendered HTML
- **Legacy code**: Follow ESLint recommended rules (eslint, typescript-eslint, Angular ESLint)
- Prefer TypeScript over JavaScript
- **Design system**: Use GitHub's [Primer Design System](https://primer.style/product/) via ViewComponent
- [primer_view_components](https://github.com/opf/primer_view_components) - OpenProject's fork of Primer Rails/ViewComponent
- [openproject-octicons](https://github.com/opf/openproject-octicons) - OpenProject's fork of Primer Octicons
- [commonmark-ckeditor-build](https://github.com/opf/commonmark-ckeditor-build) - Custom CKEditor build with CommonMark Markdown support
- Write unit tests for components (Jasmine for legacy Angular, RSpec for ViewComponents)
#### Design System Components
- [primer_view_components](https://github.com/opf/primer_view_components) - OpenProject's fork of Primer Rails/ViewComponent
- [openproject-octicons](https://github.com/opf/openproject-octicons) - OpenProject's fork of Primer Octicons
- [commonmark-ckeditor-build](https://github.com/opf/commonmark-ckeditor-build) - Custom CKEditor build with CommonMark Markdown support
### Templates
- Use ERB for server-rendered views
- Use ViewComponents for reusable UI components
- Document new ViewComponents with API/Yard docs and Lookbook previews
- Lookbook deployed at: https://qa.openproject-edge.com/lookbook/
- See https://github.com/lookbook-hq/lookbook for Lookbook documentation
- Lint with erb_lint before committing
#### Enterprise and BIM Editions
- **Enterprise Edition**: Includes additional features like Single sign-on (OIDC & SAML), LDAP, Nextcloud integration, SCIM API, and more (requires token for development)
- **BIM Edition**: Tailored for construction industry needs. Code in `modules/bim/`, docs in `docs/bim-guide/`. Existing instances can be switched to BIM edition.
### Commit Messages
- First line: < 72 characters
- Blank line
- Detailed description wrapped to 72 characters
- Reference work packages when applicable
- See [code review guidelines](docs/development/code-review-guidelines/) for more details
- **Merge strategy**: Use "Merge pull request" (not squash) to retain commit history, except for single-commit PRs which can use "Rebase and merge"
#### GitHub Actions CI/CD
- **test-core.yml** - Main test suite (units + features, ~40 min, runs on all PRs)
- **rubocop-core.yml** - Ruby linting (runs on all PRs with Ruby changes)
- **eslint-core.yml** - JS/TS linting (runs on all PRs with JS/TS changes)
- **test-frontend-unit.yml** - Frontend unit tests
- **brakeman-scan-core.yml** - Security scanning
- **codeql-scan-core.yml** - Code quality/security analysis
- **Skip CI**: Add `[ci skip]` to commit message to skip CI (use sparingly)
### Translations
- OpenProject is a multilingual product with officially supported and community-supported languages
- UI translations are managed via [Crowdin](https://crowdin.com/)
- Don't modify translation files directly; contributions should go through Crowdin
- Exception: Source translations in `**/config/locales/en.yml` can be modified directly
- UI strings should never be hard-coded; always use translation keys for accessibility and internationalization
## Performance Considerations
### CI Timeouts
#### Performance Considerations
- Main test suite: 40 minutes timeout
- Individual jobs: varies by type
- Use parallel execution when available
### Build Times
- Full Docker build: ~10-15 minutes (first time)
- Bundle install: ~2-5 minutes
- npm install: ~3-7 minutes
- Database setup: ~1-2 minutes
- Asset compilation: ~30-40 seconds
## Important Commands Reference
### Additional Commands
```bash
# Setup
bin/setup # Initial Rails setup (creates DB, runs migrations)
bin/setup_dev # Full dev environment setup (backend + frontend)
# Database
bundle exec rake db:migrate # Run pending migrations
bundle exec rake db:rollback # Rollback last migration
bundle exec rake db:seed # Seed database with sample data
bundle exec rake db:migrate:status # Check migration status
# Testing
bundle exec rspec # Run RSpec tests
bundle exec rake parallel:spec # Run tests in parallel
cd frontend && npm test # Run frontend tests
# Linting
bundle exec rubocop # Ruby linting
cd frontend && npx eslint src/ # JavaScript/TypeScript linting
erb_lint {files} # ERB template linting
# Development
bin/dev # Start all services
bundle exec rails console # Rails console
bundle exec rails routes # List all routes
# Frontend
bundle exec rails openproject:plugins:register_frontend assets:export_locales
# Docker
bin/compose setup # Setup Docker environment
bin/compose start # Start Docker services
bin/compose run # Run with backend in foreground
bin/compose rspec {test_file} # Run tests in Docker
docker compose run --rm backend-test "bundle exec rspec spec/features/work_package_show_spec.rb"
```
## Trust These Instructions
These instructions are comprehensive and validated. Only search for additional information if:
1. You encounter an error not documented here
2. You need specific implementation details for a feature
3. The instructions appear outdated (e.g., version mismatches)
For any issues, consult:
For detailed documentation, consult:
- `docs/development/` - Development documentation
- `docs/development/running-tests/` - Testing guide
- `docs/development/code-review-guidelines/` - Code review standards
+2 -1
View File
@@ -76,7 +76,8 @@ jobs:
vonTronje,
vspielau,
wielinde,
yanzubrytskyi
yanzubrytskyi,
ehassan01
# the followings are the optional inputs - If the optional inputs are not given, then default values will be taken
remote-organization-name: opf
+3 -3
View File
@@ -16,8 +16,8 @@ jobs:
build-release-candidate:
# References to release/X.Y and X.Y-rc are being
# updated from the devkit (UpdateWorkflows step) whenever a new release branch is created
uses: opf/openproject/.github/workflows/docker.yml@release/17.0
uses: opf/openproject/.github/workflows/docker.yml@release/17.1
with:
branch: release/17.0
tag: 17.0-rc
branch: release/17.1
tag: 17.1-rc
secrets: inherit
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
trigger_saas_tests:
permissions:
contents: none
if: github.repository == 'opf/openproject'
if: github.repository == 'opf/openproject' && github.actor != 'dependabot[bot]'
name: SaaS tests
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send mail
uses: dawidd6/action-send-mail@v6
uses: dawidd6/action-send-mail@v7
with:
subject: ${{ inputs.subject }}
body: ${{ inputs.body }}
-1
View File
@@ -37,7 +37,6 @@ jobs:
echo "OPENPROJECT_ENTERPRISE__CHARGEBEE__SITE=openproject-enterprise-test" >> .env.pullpreview
echo "OPENPROJECT_ENTERPRISE__TRIAL__CREATION__HOST=https://start.openproject-edge.com" >> .env.pullpreview
echo "OPENPROJECT_FEATURE_BLOCK_NOTE_EDITOR=true" >> .env.pullpreview
echo "OPENPROJECT_FEATURE_WP_ACTIVITY_TAB_LAZY_PAGINATION_ACTIVE=true" >> .env.pullpreview
- name: Boot as BIM edition
if: contains(github.ref, 'bim/') || contains(github.head_ref, 'bim/')
run: |
+4
View File
@@ -133,6 +133,10 @@ db/*.sql
lefthook-local.yml
.rubocop-local.yml
# Local AI coding agent instruction overrides
AGENTS.local.md
CLAUDE.local.md
/.lefthook-local/
frontend/package-lock.json
+140 -135
View File
@@ -59,6 +59,9 @@ Layout/MultilineMethodCallIndentation:
Layout/MultilineOperationIndentation:
Enabled: false
Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]
Lint/AmbiguousOperator:
Enabled: false
@@ -98,14 +101,11 @@ Lint/UnderscorePrefixedVariableName:
Lint/Void:
Enabled: false
Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/AbcSize:
Enabled: true
Exclude:
- "spec/**/*.rb"
- "modules/*/spec/**/*.rb"
Metrics/BlockLength:
Enabled: false
@@ -113,6 +113,12 @@ Metrics/BlockLength:
Metrics/BlockNesting:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/MethodLength:
Enabled: false
@@ -122,12 +128,6 @@ Metrics/ModuleLength:
Metrics/ParameterLists:
Enabled: false
Metrics/AbcSize:
Enabled: true
Exclude:
- "spec/**/*.rb"
- "modules/*/spec/**/*.rb"
Naming/AccessorMethodName:
Enabled: false
@@ -150,10 +150,15 @@ Naming/VariableNumber:
- '\w_20\d\d' # allow dates like christmas_2022 or date_2034_04_12
- '\w\d++(_\d++)+' # allow hierarchical data like child1_2_5 (second + in regex is possessive qualifier)
- 'custom_field_\d+' # allow custom field method names to be called with send :custom_field_1001
# There are valid cases in which to use methods like:
# * update_all
# * touch_all
Rails/SkipsModelValidations:
OpenProject/AddPreviewForViewComponent:
Include:
- app/components/op_turbo/**.rb
- app/components/op_primer/**.rb
- app/components/open_project/**.rb
- app/components/concerns/**.rb
Performance/Casecmp:
Enabled: false
# Don't force us to use tag instead of content_tag
@@ -161,55 +166,6 @@ Rails/SkipsModelValidations:
Rails/ContentTag:
Enabled: false
# Disable I18n.locale = in specs, where it is reset
# by us explicitly
Rails/I18nLocaleAssignment:
Enabled: true
Exclude:
- "spec/**/*.rb"
# Do not bother if `let` statements use an index in their name
RSpec/IndexedLet:
Enabled: false
# The http verbs in Rack::Test do not accept named parameters (params: params)
Rails/HttpPositionalArguments:
Enabled: false
# require_dependency is an obsolete method for Rails applications running in Zeitwerk mode.
Rails/RequireDependency:
Enabled: true
# For feature specs, we tend to have longer specs that cover a larger part of the functionality.
# This is done for multiple reasons:
# * performance, as setting up integration tests is costly
# * following a scenario that is closer to how a user interacts
RSpec/ExampleLength:
Max: 25
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# We have specs that have no expect(..) syntax,
# but only helper classes that expect themselves
RSpec/NoExpectationExample:
Enabled: false
RSpec/DescribeClass:
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# Nothing wrong with `include_examples` when used properly.
RSpec/IncludeExamples:
Enabled: false
# Allow number HTTP status codes in specs
RSpecRails/HttpStatus:
Enabled: false
# dynamic finders cop clashes with capybara ID cop
Rails/DynamicFindBy:
Enabled: true
@@ -220,6 +176,7 @@ Rails/DynamicFindBy:
Whitelist:
- find_by_login
- find_by_mail
- find_by_plaintext_value
# Allow reorder to prevent find each cop triggering
Rails/FindEach:
@@ -230,53 +187,38 @@ Rails/FindEach:
- select
- lock
# The http verbs in Rack::Test do not accept named parameters (params: params)
Rails/HttpPositionalArguments:
Enabled: false
# Disable I18n.locale = in specs, where it is reset
# by us explicitly
Rails/I18nLocaleAssignment:
Enabled: true
Exclude:
- "spec/**/*.rb"
# We have config.active_record.belongs_to_required_by_default = false ,
# which means, we do have to declare presence validators on belongs_to relations.
Rails/RedundantPresenceValidationOnBelongsTo:
Enabled: false
# See RSpec/ExampleLength for why feature specs are excluded
RSpec/MultipleExpectations:
Max: 15
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
RSpec/MultipleMemoizedHelpers:
Enabled: false
RSpec/NestedGroups:
Enabled: false
# Don't force the second argument of describe
# to be .class_method or #instance_method
RSpec/DescribeMethod:
Enabled: false
# Don't force the second argument of describe
# to match the exact file name
RSpec/SpecFilePathFormat:
CustomTransform:
OpenIDConnect: openid_connect
OAuthClients: oauth_clients
OAuth: oauth
ICal: ical
IgnoreMethods: true
# Prevent "fit" or similar to be committed
RSpec/Focus:
# require_dependency is an obsolete method for Rails applications running in Zeitwerk mode.
Rails/RequireDependency:
Enabled: true
# We use let!() to ensure dependencies are created
# instead of let() and referencing them explicitly
RSpec/LetSetup:
# Require save! to prevent saving without validation when saving outside of a condition.
Rails/SaveBang:
Enabled: true
# There are valid cases in which to use methods like:
# * update_all
# * touch_all
Rails/SkipsModelValidations:
Enabled: false
RSpec/LeadingSubject:
Enabled: false
RSpec/NamedSubject:
# Allow number HTTP status codes in specs
RSpecRails/HttpStatus:
Enabled: false
# expect not_to change is not working as expected
@@ -303,6 +245,79 @@ RSpec/ContextWording:
- within
- without
RSpec/DescribeClass:
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# Don't force the second argument of describe
# to be .class_method or #instance_method
RSpec/DescribeMethod:
Enabled: false
# For feature specs, we tend to have longer specs that cover a larger part of the functionality.
# This is done for multiple reasons:
# * performance, as setting up integration tests is costly
# * following a scenario that is closer to how a user interacts
RSpec/ExampleLength:
Max: 25
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
# Prevent "fit" or similar to be committed
RSpec/Focus:
Enabled: true
# Nothing wrong with `include_examples` when used properly.
RSpec/IncludeExamples:
Enabled: false
# Do not bother if `let` statements use an index in their name
RSpec/IndexedLet:
Enabled: false
RSpec/LeadingSubject:
Enabled: false
# We use let!() to ensure dependencies are created
# instead of let() and referencing them explicitly
RSpec/LetSetup:
Enabled: false
# We have specs that have no expect(..) syntax,
# but only helper classes that expect themselves
RSpec/NoExpectationExample:
Enabled: false
# See RSpec/ExampleLength for why feature specs are excluded
RSpec/MultipleExpectations:
Max: 15
Enabled: true
Exclude:
- "spec/features/**/*.rb"
- "modules/*/spec/features/**/*.rb"
RSpec/MultipleMemoizedHelpers:
Enabled: false
RSpec/NestedGroups:
Enabled: false
# Don't force the second argument of describe
# to match the exact file name
RSpec/SpecFilePathFormat:
CustomTransform:
OpenIDConnect: openid_connect
OAuthClients: oauth_clients
EnforcedInflector: active_support
IgnoreMethods: true
RSpec/NamedSubject:
Enabled: false
Style/Alias:
Enabled: false
@@ -367,6 +382,19 @@ Style/FormatString:
Style/FormatStringToken:
AllowedMethods: [redirect]
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always_true
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Style/GlobalVars:
Enabled: false
@@ -409,6 +437,13 @@ Style/NilComparison:
Style/Not:
Enabled: false
Style/NumericLiterals:
Enabled: false
# Avoid enforcing "positive?"
Style/NumericPredicate:
Enabled: false
Style/OneLineConditional:
Enabled: false
@@ -471,33 +506,3 @@ Style/WhileUntilModifier:
Style/WordArray:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always_true
Style/NumericLiterals:
Enabled: false
# Avoid enforcing "positive?"
Style/NumericPredicate:
Enabled: false
Style/HashEachMethods:
Enabled: true
Style/HashTransformKeys:
Enabled: true
Style/HashTransformValues:
Enabled: true
Performance/Casecmp:
Enabled: false
OpenProject/AddPreviewForViewComponent:
Include:
- app/components/op_turbo/**.rb
- app/components/op_primer/**.rb
- app/components/open_project/**.rb
- app/components/concerns/**.rb
+237
View File
@@ -0,0 +1,237 @@
# OpenProject AI Coding Agent Instructions
> **Note for developers**: You can create `AGENTS.local.md` (or `CLAUDE.local.md`) in this directory to add your own custom instructions or preferences for AI coding agents. These files are git-ignored and will not be committed to the repository.
## Repository Overview
**OpenProject** is a web-based, open-source project management software written in Ruby on Rails with PostgreSQL for data persistence.
- **Size**: Large monorepo (~840MB, ~1M+ lines of code)
- **Backend**: Ruby 3.4.7, Rails ~8.0.3
- **Frontend**: Node.js 22.21.0, npm 10.1.0+, TypeScript
- **Database**: PostgreSQL (required)
- **Architecture**: Server-rendered HTML with Hotwire (Turbo + Stimulus). Legacy Angular components exist and are being migrated to custom elements. Uses GitHub's Primer Design System via ViewComponent.
- **Editions**: Community, Enterprise (SSO, LDAP, SCIM), and BIM (construction industry, code in `modules/bim/`)
## Critical Setup Requirements
**ALWAYS verify versions before building:**
- Ruby: `3.4.7` (see `.ruby-version`)
- Node: `^22.21.0` (see `package.json` engines)
- Bundler: Latest 2.x
OpenProject supports two development setups: **Local** and **Docker**. Choose one based on your preference.
### Local Development Setup
```bash
bundle install # Install Ruby gems
cd frontend && npm ci && cd .. # Install Node packages
bundle exec rake db:migrate # Setup database
bin/dev # Start all services (Rails, frontend, Good Job worker)
# Access at http://localhost:3000
```
### Docker Development Setup
The Docker development environment uses configurations in `docker/dev/` and the `bin/compose` wrapper script.
```bash
# Initial setup (first time only)
bin/compose setup # Installs backend and frontend dependencies
# Starting services
bin/compose start # Start backend and frontend in background
bin/compose run # Start frontend in background, backend in foreground (for debugging with pry)
# Running tests
bin/compose rspec spec/models/user_spec.rb # Run specific tests in backend-test container
# Other operations
bin/compose reset # Remove all containers and volumes (requires setup again)
bin/compose <command> # Pass any docker-compose command directly
```
**Important Docker Notes:**
- **CRITICAL**: `config/database.yml` must NOT exist when using Docker (rename or delete it)
- Most developers use a local `docker-compose.override.yml` for custom port mappings and configurations
- Copy `docker-compose.override.example.yml` to `docker-compose.override.yml` and customize as needed
- Default ports: Backend at http://localhost:3000 (or 4200 for frontend dev server)
- Services: `backend`, `frontend`, `worker`, `db`, `db-test`, `backend-test`, `cache`
- Persisted volumes: `pgdata`, `bundle`, `npm`, `tmp`, `opdata` (data survives container restarts)
- Docker build context: Uses Dockerfiles in `docker/dev/backend/` and `docker/dev/frontend/`
## Project Structure
### Key Directories
- `app/` - Rails application code
- `app/components/` - ViewComponent-based UI components (Ruby + ERB)
- `app/contracts/` - Validation and authorization contracts
- `app/controllers/` - Rails controllers
- `app/models/` - ActiveRecord models
- `app/services/` - Service objects (business logic)
- `app/workers/` - Background job workers
- `config/` - Rails configuration, routes, locales
- `db/` - Database migrations and seeds
- `frontend/src/` - Frontend code
- `frontend/src/app/` - Legacy Angular modules/components
- `frontend/src/stimulus/` - Stimulus controllers
- `frontend/src/turbo/` - Turbo integration
- `lib/` - Ruby libraries and extensions
- `lookbook/` - ViewComponent previews (https://qa.openproject-edge.com/lookbook/)
- `modules/` - OpenProject plugin modules
- `spec/` - RSpec test suite
- `spec/features/` - System/feature tests (Capybara)
- `spec/models/` - Model unit tests
- `spec/requests/` - API/integration tests
- `spec/services/` - Service tests
### Configuration Files
- `.ruby-version` - Ruby version
- `.rubocop.yml` - Ruby linting rules
- `.erb_lint.yml` - ERB template linting
- `frontend/eslint.config.mjs` - JavaScript/TypeScript linting
- `Gemfile` - Ruby dependencies
- `package.json` / `frontend/package.json` - Node.js dependencies
- `lefthook.yml` - Git hooks configuration
## Building and Testing
### Linting (Run Before Committing)
```bash
# Ruby
bundle exec rubocop # Check all files
bin/dirty-rubocop --uncommitted # Check only uncommitted changes
# JavaScript/TypeScript
cd frontend && npx eslint src/ && cd ..
# ERB Templates
erb_lint {files}
# Install Git Hooks (recommended)
bundle exec lefthook install
```
### Running Tests
```bash
# Backend (RSpec) - prefer specific tests over running all
bundle exec rspec spec/models/user_spec.rb # Single file
bundle exec rspec spec/models/user_spec.rb:42 # Single line
bundle exec rspec spec/features # Directory
bundle exec rake parallel:spec # Parallel execution
# Frontend (Jasmine/Karma)
cd frontend && npm test && cd ..
```
### Debugging CI Failures
```bash
./script/github_pr_errors | xargs bundle exec rspec # Run failed tests from CI
./script/bulk_run_rspec spec/path/to/flaky_spec.rb # Run tests multiple times
```
## Code Style Guidelines
### Ruby
- Follow [Ruby community style guide](https://github.com/bbatsov/ruby-style-guide)
- Use service objects for complex business logic (return `ServiceResult`)
- Use contracts for validation and authorization
- Keep controllers thin, models focused
- Document with [YARD](https://yardoc.org/)
- Write RSpec tests for all new features
### JavaScript/TypeScript
- **New development**: Use Hotwire (Turbo + Stimulus) with server-rendered HTML
- **Legacy code**: Follow ESLint rules
- Prefer TypeScript over JavaScript
- Use [Primer Design System](https://primer.style/product/) via ViewComponent
### Templates
- Use ERB for server-rendered views
- Use ViewComponents for reusable UI (with Lookbook previews)
- Lint with erb_lint before committing
### Database Migrations
- Follow Rails migration conventions
- Migrations are "squashed" between major releases (see `docs/development/migrations/`)
### Translations
- UI strings must use translation keys (never hard-coded)
- Source translations in `**/config/locales/en.yml` can be modified directly
- Other translations managed via Crowdin
### Commit Messages
- First line: < 72 characters, then blank line, then detailed description
- Reference work packages when applicable
- Merge strategy: "Merge pull request" (not squash), except single-commit PRs can use "Rebase and merge"
## Important Commands Reference
### Local Development Commands
```bash
# Setup
bin/setup # Initial Rails setup
bin/setup_dev # Full dev environment setup
# Database
bundle exec rake db:migrate # Run migrations
bundle exec rake db:rollback # Rollback last migration
bundle exec rake db:seed # Seed sample data
# Development
bin/dev # Start all services
bundle exec rails console # Rails console
bundle exec rails routes # List routes
# Testing
bundle exec rspec # Run RSpec tests
bundle exec rake parallel:spec # Parallel tests
cd frontend && npm test # Frontend tests
# Linting
bundle exec rubocop # Ruby linting
cd frontend && npx eslint src/ # JS/TS linting
erb_lint {files} # ERB linting
```
### Docker Development Commands
```bash
# Setup and lifecycle
bin/compose setup # Setup Docker environment (first time)
bin/compose start # Start all services in background
bin/compose run # Start frontend in background, backend in foreground
bin/compose reset # Remove all containers and volumes
bin/compose stop # Stop all services
bin/compose down # Stop and remove containers
# Testing
bin/compose rspec spec/models/user_spec.rb # Run specific tests
bin/compose exec backend bundle exec rspec # Run tests directly in backend container
# Development
bin/compose exec backend bundle exec rails console # Rails console
bin/compose logs backend # View backend logs
bin/compose logs -f backend # Follow backend logs
bin/compose ps # List running containers
# Database
bin/compose exec backend bundle exec rake db:migrate # Run migrations
bin/compose exec backend bundle exec rake db:seed # Seed data
# Direct docker-compose commands
bin/compose up -d # Start services
bin/compose restart backend # Restart backend service
```
## Additional Documentation
- `docs/development/` - Development documentation
- `docs/development/running-tests/` - Testing guide
- `docs/development/code-review-guidelines/` - Code review standards
- `CONTRIBUTING.md` - Contribution workflow
- `.github/copilot-instructions.md` - Extended agent instructions with troubleshooting
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+24 -20
View File
@@ -46,6 +46,8 @@ gem "responders", "~> 3.2"
gem "ffi", "~> 1.15"
gem "connection_pool", "~> 2.5.5"
gem "rdoc", ">= 2.4.2"
gem "doorkeeper", "~> 5.8.0"
@@ -60,13 +62,13 @@ gem "warden-basic_auth", "~> 0.2.1"
gem "pagy"
gem "will_paginate", "~> 4.0.0"
gem "friendly_id", "~> 5.5.0"
gem "friendly_id", "~> 5.6.0"
gem "scimitar", "~> 2.13"
gem "acts_as_list", "~> 1.2.6"
gem "acts_as_tree", "~> 2.9.0"
gem "awesome_nested_set", "~> 3.8.0"
gem "awesome_nested_set", "~> 3.9.0"
gem "closure_tree", "~> 9.3.0"
gem "rubytree", "~> 2.1.0"
@@ -95,7 +97,7 @@ gem "deckar01-task_list", "~> 2.3.1"
# Requires escape-utils for faster escaping
gem "escape_utils", "~> 1.3"
# Syntax highlighting used in html-pipeline with rouge
gem "rouge", "~> 4.6.1"
gem "rouge", "~> 4.7.0"
# HTML sanitization used for html-pipeline
gem "sanitize", "~> 7.0.0"
# HTML autolinking for mails and urls (replaces autolink)
@@ -109,7 +111,7 @@ gem "svg-graph", "~> 2.2.0"
gem "date_validator", "~> 0.12.0"
gem "email_validator", "~> 2.2.3"
gem "json_schemer", "~> 2.4.0"
gem "json_schemer", "~> 2.5.0"
gem "ruby-duration", "~> 3.2.0"
gem "mail", "2.9.0"
@@ -121,7 +123,7 @@ gem "sys-filesystem", "~> 1.5.0", require: false
gem "bcrypt", "~> 3.1.6"
gem "multi_json", "~> 1.17.0"
gem "multi_json", "~> 1.19.0"
gem "oj", "~> 3.16.12"
gem "daemons"
@@ -139,7 +141,7 @@ gem "rack-attack", "~> 6.8.0"
gem "browser", "~> 6.2.0"
# Providing health checks
gem "okcomputer", "~> 1.19.0"
gem "okcomputer", "~> 1.19.1"
# Lograge to provide sane and non-verbose logging
gem "lograge", "~> 0.14.0"
@@ -151,7 +153,7 @@ gem "structured_warnings", "~> 0.5.0"
# don't require by default, instead load on-demand when actually configured
gem "airbrake", "~> 13.0.0", require: false
gem "markly", "~> 0.14" # another Markdown parser like commonmarker, but with AST support used in PDF export
gem "markly", "~> 0.15" # another markdown parser like commonmarker, but with AST support used in PDF export
gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "6c565541bfa390c58d90d49aa9b487777704fc66"
gem "prawn", "~> 2.4"
gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved.
@@ -159,6 +161,8 @@ 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.4.0"
gem "meta-tags", "~> 2.22.2"
gem "paper_trail", "~> 17.0.0"
@@ -179,7 +183,7 @@ group :production do
end
gem "i18n-js", "~> 4.2.4"
gem "rails-i18n", "~> 8.0.0"
gem "rails-i18n", "~> 8.1.0"
gem "sprockets", "~> 3.7.2" # lock sprockets below 4.0
gem "sprockets-rails", "~> 3.5.1"
@@ -188,19 +192,19 @@ gem "puma", "~> 7.1"
gem "puma-plugin-statsd", "~> 2.7"
gem "rack-timeout", "~> 0.7.0", require: "rack/timeout/base"
gem "nokogiri", "~> 1.18.10"
gem "nokogiri", "~> 1.19.0"
gem "carrierwave", "~> 1.3.4"
gem "carrierwave_direct", "~> 2.1.0"
gem "fog-aws"
gem "aws-sdk-core", "~> 3.239"
gem "aws-sdk-core", "~> 3.241"
# File upload via fog + screenshots on travis
gem "aws-sdk-s3", "~> 1.205"
gem "aws-sdk-s3", "~> 1.211"
gem "openproject-token", "~> 8.3.0"
gem "openproject-token", "~> 8.6.0"
gem "plaintext", "~> 0.3.2"
gem "plaintext", "~> 0.3.7"
gem "ruby-progressbar", "~> 1.13.0", require: false
@@ -227,12 +231,12 @@ gem "yabeda-rails"
# opentelemetry
gem "opentelemetry-exporter-otlp", "~> 0.31.0", require: false
gem "opentelemetry-instrumentation-all", "~> 0.87.0", require: false
gem "opentelemetry-instrumentation-all", "~> 0.89.0", require: false
gem "opentelemetry-sdk", "~> 1.10", require: false
gem "view_component", "~> 4.1.1"
gem "view_component", "~> 4.2.0"
# Lookbook
gem "lookbook", "2.3.13"
gem "lookbook", "2.3.14"
gem "inline_svg", "~> 1.10.0"
@@ -358,7 +362,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.33.3", require: false # 2.33.4 has issues with Rails/ActionControllerFlashBeforeRender
gem "rubocop-rails", "~> 2.34.2"
gem "rubocop-rspec", require: false
gem "rubocop-rspec_rails", require: false
@@ -376,7 +380,7 @@ group :development, :test do
gem "active_record_doctor", "~> 2.0.1"
end
gem "bootsnap", "~> 1.19.0", require: false
gem "bootsnap", "~> 1.20.0", require: false
# API gems
gem "grape", "~> 2.4.0"
@@ -404,7 +408,7 @@ group :postgres do
end
# Support application loading when no database exists yet.
gem "activerecord-nulldb-adapter", "~> 1.1.1"
gem "activerecord-nulldb-adapter", "~> 1.2.2"
# Have application level locks on the database to have a mutex shared between workers/hosts.
# We e.g. employ this to safeguard the creation of journals.
@@ -424,4 +428,4 @@ end
gem "openproject-octicons", "~>19.32.0"
gem "openproject-octicons_helper", "~>19.32.0"
gem "openproject-primer_view_components", "~>0.79.1"
gem "openproject-primer_view_components", "~>0.80.2"
+351 -369
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -8,7 +8,7 @@ gem 'omniauth-openid_connect-providers',
gem 'omniauth-openid-connect',
git: 'https://github.com/opf/omniauth-openid-connect.git',
ref: 'f0c1ecdb26e39017a9e929af75a166c772d960bb'
ref: '825d06235b64f6bc872bba709f1c2d48fd5cede4'
group :opf_plugins do
# included so that engines can reference OpenProject::Version
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

@@ -0,0 +1,45 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="64" height="64">
<style>
.s0 { fill: none }
.s1 { fill: #ffffff }
.s2 { fill: #0070ba }
.s3 { fill: #66cb92 }
.s4 { fill: #9fcde0 }
</style>
<g>
<path fill-rule="evenodd" class="s0" d="m271 446.1c-49.44 0-89.4-39.96-89.4-89.4 0-49.44 39.96-89.4 89.4-89.4 49.44 0 89.4 39.96 89.4 89.4 0 49.44-39.96 89.4-89.4 89.4z"/>
<path class="s0" d="m564.7 209c28.5 0 51.6-23.1 51.6-51.6 0-28.5-23.1-51.6-51.6-51.6-28.5 0-51.6 23.1-51.6 51.6 0 28.5 23.2 51.6 51.6 51.6z"/>
<path class="s0" d="m786.8 629.8h23.8c0-2.7 2.2-4.9 4.9-5l-0.8-28.5z"/>
<path class="s0" d="m582.3 594.6l-1.8 30.2c2.6 0.2 4.7 2.3 4.7 5h26.4z"/>
<path class="s0" d="m450.8 178.2l23.1 1.7 1 3.4c2 7.1 4.9 14 8.6 20.5l1.7 3-15 17.6c-0.1 0.3 0 0.7 0.3 1l26.4 26.4c0.2 0.2 0.6 0.3 0.9 0.2l17.7-15 3 1.7c6.4 3.7 13.3 6.6 20.4 8.6l3.3 1 1.7 23.1c0.2 0.3 0.5 0.5 0.9 0.5h39.9c0.3 0 0.7-0.2 0.8-0.5l1.7-23.1 3.4-1c7.1-2 14-4.9 20.5-8.6l3-1.7 17.6 15c0.3 0.1 0.7 0 1-0.3l26.4-26.4c0.2-0.2 0.3-0.6 0.2-0.9l-15-17.6 1.7-3c3.7-6.4 6.6-13.3 8.6-20.5l1-3.4 23.1-1.7c0.3-0.2 0.5-0.5 0.5-0.9v-39.9c0-0.3-0.2-0.7-0.5-0.8l-23.1-1.7-1-3.4c-2-7.1-4.9-14-8.6-20.5l-1.7-3 15-17.6c0.1-0.3 0-0.7-0.2-0.9l-26.4-26.4c-0.2-0.2-0.6-0.3-0.9-0.2l-17.6 15-3-1.7c-6.4-3.7-13.3-6.6-20.5-8.6l-3.4-1-1.7-23.1c-0.2-0.3-0.5-0.5-0.8-0.5h-39.9c-0.3 0-0.6 0.2-0.8 0.5l-1.6 23.1-3.4 1c-7.1 2-14 4.9-20.5 8.6l-3 1.7-17.7-15c-0.3-0.1-0.7 0-1 0.3l-26.4 26.4c-0.2 0.2-0.3 0.6-0.2 0.9l15 17.6-1.7 3.1c-3.7 6.4-6.6 13.3-8.6 20.5l-1 3.4-23.1 1.7c-0.3 0.2-0.5 0.5-0.5 0.8v39.9c-0.1 0.2 0.1 0.6 0.4 0.7zm113.9-82.3c34 0 61.6 27.6 61.6 61.6 0 34-27.6 61.5-61.6 61.5-34 0-61.6-27.6-61.6-61.6 0-34 27.7-61.5 61.6-61.5z"/>
<path class="s1" d="m860.9 782.6h-559.4c-11.6 0-21 9.4-21 21v95.8c0 11.6 9.4 21 21 21h56.5 119.6 207.2 119.6 56.5c11.6 0 21-9.4 21-21v-95.8c0-11.5-9.4-21-21-21zm-495.3 102.9c-17.7 0-32.2-14.4-32.2-32.2 0-17.8 14.4-32.2 32.2-32.2 17.7 0 32.2 14.4 32.2 32.2 0 17.8-14.5 32.2-32.2 32.2zm125.4-31.4c0 6.7-5.5 12.2-12.2 12.2h-1.5c-6.7 0-12.2-5.5-12.2-12.2v-1.5c0-6.7 5.5-12.2 12.2-12.2h1.5c6.7 0 12.2 5.5 12.2 12.2zm324.8 0c0 6.7-5.5 12.2-12.2 12.2h-230.2c-6.7 0-12.2-5.5-12.2-12.2v-1.5c0-6.7 5.5-12.2 12.2-12.2h230.2c6.7 0 12.2 5.5 12.2 12.2z"/>
<path class="s1" d="m478.8 847.4h-1.5c-2.9 0-5.2 2.3-5.2 5.2v1.5c0 2.9 2.3 5.2 5.2 5.2h1.5c2.9 0 5.2-2.3 5.2-5.2v-1.5c0-2.9-2.3-5.2-5.2-5.2z"/>
<path class="s1" d="m803.6 847.4h-230.2c-2.9 0-5.2 2.3-5.2 5.2v1.5c0 2.9 2.3 5.2 5.2 5.2h230.2c2.9 0 5.2-2.3 5.2-5.2v-1.5c0-2.9-2.3-5.2-5.2-5.2z"/>
<path class="s2" d="m877.7 777.6c8.5-5.5 14.2-15.1 14.2-26v-95.8c0-10.9-5.6-20.5-14.2-26 8.5-5.5 14.2-15.1 14.2-26v-95.8c0-17.1-13.9-31-31-31h-49.2c-2.8 0-5 2.2-5 5 0 2.8 2.2 5 5 5h0.1 49.1c11.6 0 21 9.4 21 21v95.8c0 11.6-9.4 21-21 21h-45.3-0.1c-2.7 0.1-4.9 2.3-4.9 5 0 2.8 2.2 5 5 5h45.1c0.1 0 0.1 0 0.2 0 11.6 0 21 9.4 21 21v95.8c0 11.6-9.4 21-21 21h-559.4c-11.6 0-21-9.4-21-21v-95.8c0-11.6 9.4-21 21-21h219.8c0.1 0 0.1 0 0.2 0h58.8c2.8 0 5-2.2 5-5 0-2.7-2.1-4.8-4.7-5q-0.15 0-0.3 0h-278.8c-11.6 0-21-9.4-21-21v-95.8c0-11.6 9.4-21 21-21h287.1 0.3c2.8 0 5-2.2 5-5 0-2.8-2.2-5-5-5h-287.4c-17.1 0-31 13.9-31 31v95.8c0 10.9 5.6 20.5 14.2 26-8.5 5.5-14.2 15.1-14.2 26v95.8c0 10.9 5.7 20.5 14.2 26-8.5 5.5-14.2 15.1-14.2 26v95.8c0 17.1 13.9 31 31 31h51.5v7.8c0 16.5 13.5 30 30 30h69.6c16.5 0 30-13.5 30-30v-7.8h197.2v7.8c0 16.5 13.5 30 30 30h69.6c16.5 0 30-13.5 30-30v-7.8h51.5c17.1 0 31-13.9 31-31v-95.8c0-10.8-5.6-20.5-14.2-26zm4.2 121.8c0 11.6-9.4 21-21 21h-56.5-119.6-207.2-119.6-56.5c-11.6 0-21-9.4-21-21v-95.8c0-11.6 9.4-21 21-21h559.4c11.6 0 21 9.4 21 21zm-82.5 38.8c0 11-9 20-20 20h-69.6c-11 0-20-9-20-20v-7.8h109.6zm-326.8 0c0 11-9 20-20 20h-69.6c-11 0-20-9-20-20v-7.8h109.6z"/>
<path class="s3" d="m365.6 828.2c-13.9 0-25.2 11.3-25.2 25.2 0 13.9 11.3 25.2 25.2 25.2 13.9 0 25.2-11.3 25.2-25.2 0-13.9-11.3-25.2-25.2-25.2z"/>
<path class="s2" d="m365.6 821.2c-17.7 0-32.2 14.4-32.2 32.2 0 17.8 14.4 32.2 32.2 32.2 17.7 0 32.2-14.4 32.2-32.2 0-17.8-14.5-32.2-32.2-32.2zm0 57.3c-13.9 0-25.2-11.3-25.2-25.2 0-13.9 11.3-25.2 25.2-25.2 13.9 0 25.2 11.3 25.2 25.2 0 13.9-11.3 25.2-25.2 25.2z"/>
<path class="s2" d="m803.6 840.4h-230.2c-6.7 0-12.2 5.5-12.2 12.2v1.5c0 6.7 5.5 12.2 12.2 12.2h230.2c6.7 0 12.2-5.5 12.2-12.2v-1.5c0-6.7-5.5-12.2-12.2-12.2zm5.2 13.7c0 2.9-2.3 5.2-5.2 5.2h-230.2c-2.9 0-5.2-2.3-5.2-5.2v-1.5c0-2.9 2.3-5.2 5.2-5.2h230.2c2.9 0 5.2 2.3 5.2 5.2z"/>
<path class="s2" d="m478.8 840.4h-1.5c-6.7 0-12.2 5.5-12.2 12.2v1.5c0 6.7 5.5 12.2 12.2 12.2h1.5c6.7 0 12.2-5.5 12.2-12.2v-1.5c0-6.7-5.5-12.2-12.2-12.2zm5.2 13.7c0 2.9-2.3 5.2-5.2 5.2h-1.5c-2.9 0-5.2-2.3-5.2-5.2v-1.5c0-2.9 2.3-5.2 5.2-5.2h1.5c2.9 0 5.2 2.3 5.2 5.2z"/>
<path class="s1" d="m710.7 721.3c-0.6 0.7-1.2 1.3-1.9 1.9-2.7 2.3-6 3.5-9.5 3.5-0.5 0-0.9 0-1.4-0.1-4-0.4-7.6-2.2-10.1-5.3l-76.1-91.5h-26.4c0 2.8-2.2 5-5 5h-58.8c-0.1 0-0.1 0-0.2 0h-219.8c-11.6 0-21 9.4-21 21v95.8c0 11.6 9.4 21 21 21h559.4c11.6 0 21-9.4 21-21v-95.8c0-11.6-9.4-21-21-21-0.1 0-0.1 0-0.2 0h-45.1c-2.8 0-5-2.2-5-5h-23.8zm-345.1 16.4c-17.7 0-32.2-14.4-32.2-32.2 0-17.7 14.4-32.2 32.2-32.2 17.7 0 32.2 14.4 32.2 32.2 0 17.8-14.5 32.2-32.2 32.2zm125.4-31.5c0 6.7-5.5 12.2-12.2 12.2h-1.5c-6.7 0-12.2-5.5-12.2-12.2v-1.5c0-6.7 5.5-12.2 12.2-12.2h1.5c6.7 0 12.2 5.5 12.2 12.2zm156.1 12.3h-72.9c-7.1 0-12.9-5.8-13-12.9 0-7.1 5.8-13 13-13h60.2c1.9 0 3.5 1.6 3.5 3.5 0 1.9-1.6 3.5-3.5 3.5h-60.2c-3.3 0-6 2.7-6 5.9 0 3.3 2.7 5.9 6 5.9h72.9c1.9 0 3.5 1.6 3.5 3.5 0 1.9-1.6 3.6-3.5 3.6zm168.7-13c0 7.1-5.8 12.9-13 12.9h-58.2c-1.9 0-3.5-1.6-3.5-3.5 0-1.9 1.6-3.5 3.5-3.5h58.2c3.3 0 5.9-2.7 6-5.9 0-3.3-2.7-5.9-6-5.9h-41.1c-1.9 0-3.5-1.6-3.5-3.5 0-1.9 1.6-3.5 3.5-3.5h41.1c7.2 0 13 5.8 13 12.9z"/>
<path class="s1" d="m478.8 699.6h-1.5c-2.9 0-5.2 2.3-5.2 5.2v1.5c0 2.9 2.3 5.2 5.2 5.2h1.5c2.9 0 5.2-2.3 5.2-5.2v-1.5c0-2.9-2.3-5.2-5.2-5.2z"/>
<path class="s3" d="m365.6 680.4c-13.9 0-25.2 11.3-25.2 25.2 0 13.9 11.3 25.2 25.2 25.2 13.9 0 25.2-11.3 25.2-25.2 0-13.9-11.3-25.2-25.2-25.2z"/>
<path class="s2" d="m365.6 673.4c-17.7 0-32.2 14.4-32.2 32.2 0 17.7 14.4 32.2 32.2 32.2 17.7 0 32.2-14.4 32.2-32.2 0-17.8-14.5-32.2-32.2-32.2zm0 57.3c-13.9 0-25.2-11.3-25.2-25.2 0-13.9 11.3-25.2 25.2-25.2 13.9 0 25.2 11.3 25.2 25.2 0 13.9-11.3 25.2-25.2 25.2z"/>
<path class="s2" d="m647.1 711.5h-72.9c-3.3 0-5.9-2.7-6-5.9 0-3.3 2.7-5.9 6-5.9h60.2c1.9 0 3.5-1.6 3.5-3.5 0-1.9-1.6-3.5-3.5-3.5h-60.2c-7.1 0-13 5.8-13 13 0 7.1 5.8 12.9 13 12.9h72.9c1.9 0 3.5-1.6 3.5-3.5 0-1.9-1.6-3.6-3.5-3.6z"/>
<path class="s2" d="m758.2 696.1c0 1.9 1.6 3.5 3.5 3.5h41.1c3.3 0 6 2.7 6 5.9 0 3.3-2.7 5.9-6 5.9h-58.2c-1.9 0-3.5 1.6-3.5 3.5 0 1.9 1.6 3.5 3.5 3.5h58.2c7.1 0 12.9-5.8 13-12.9 0-7.1-5.8-12.9-13-12.9h-41.1c-1.9 0-3.5 1.5-3.5 3.5z"/>
<path class="s2" d="m478.8 692.6h-1.5c-6.7 0-12.2 5.5-12.2 12.2v1.5c0 6.7 5.5 12.2 12.2 12.2h1.5c6.7 0 12.2-5.5 12.2-12.2v-1.5c0-6.8-5.5-12.2-12.2-12.2zm5.2 13.6c0 2.9-2.3 5.2-5.2 5.2h-1.5c-2.9 0-5.2-2.3-5.2-5.2v-1.5c0-2.9 2.3-5.2 5.2-5.2h1.5c2.9 0 5.2 2.3 5.2 5.2z"/>
<path class="s1" d="m280.5 508v95.8c0 11.6 9.4 21 21 21h278.8q0.15 0 0.3 0l1.8-30.2-32-38.4c-2.2-2.7-3.4-6-3.4-9.5 0-4 1.5-7.7 4.4-10.6 2.8-2.8 6.6-4.4 10.5-4.4h24.1l2.6-44.8h-287.1c-11.6 0.1-21 9.5-21 21.1zm184.6 49c0-6.7 5.5-12.2 12.2-12.2h1.5c6.7 0 12.2 5.5 12.2 12.2v1.5c0 6.7-5.5 12.2-12.2 12.2h-1.5c-6.7 0-12.2-5.5-12.2-12.2zm-99.5-31.5c17.7 0 32.2 14.4 32.2 32.2 0 17.8-14.4 32.2-32.2 32.2-17.7 0-32.2-14.4-32.2-32.2 0-17.8 14.5-32.2 32.2-32.2z"/>
<path class="s1" d="m477.3 563.6h1.5c2.9 0 5.2-2.3 5.2-5.2v-1.4c0-2.9-2.3-5.2-5.2-5.2h-1.5c-2.9 0-5.2 2.3-5.2 5.2v1.5c0 2.8 2.3 5.1 5.2 5.1z"/>
<path class="s1" d="m846.1 535.2c6.3 5.3 7.2 14.7 1.9 21l-33.3 40.1 0.8 28.5h0.1 45.3c11.6 0 21-9.4 21-21v-95.8c0-11.6-9.4-21-21-21h-49.1l1.2 44.7h23.6c3.4 0 6.8 1.2 9.5 3.5z"/>
<path class="s3" d="m340.4 557.7c0 13.9 11.3 25.2 25.2 25.2 13.9 0 25.2-11.3 25.2-25.2 0-13.9-11.3-25.2-25.2-25.2-13.9 0-25.2 11.3-25.2 25.2z"/>
<path class="s2" d="m365.6 589.9c17.7 0 32.2-14.4 32.2-32.2 0-17.8-14.4-32.2-32.2-32.2-17.7 0-32.2 14.4-32.2 32.2 0 17.8 14.5 32.2 32.2 32.2zm25.2-32.2c0 13.9-11.3 25.2-25.2 25.2-13.9 0-25.2-11.3-25.2-25.2 0-13.9 11.3-25.2 25.2-25.2 13.9 0 25.2 11.3 25.2 25.2z"/>
<path class="s2" d="m477.3 570.6h1.5c6.7 0 12.2-5.5 12.2-12.2v-1.4c0-6.7-5.5-12.2-12.2-12.2h-1.5c-6.7 0-12.2 5.5-12.2 12.2v1.5c0 6.7 5.5 12.1 12.2 12.1zm-5.2-13.6c0-2.9 2.3-5.2 5.2-5.2h1.5c2.9 0 5.2 2.3 5.2 5.2v1.5c0 2.9-2.3 5.2-5.2 5.2h-1.5c-2.9 0-5.2-2.3-5.2-5.2z"/>
<path class="s4" d="m363 938.2c0 11 9 20 20 20h69.6c11 0 20-9 20-20v-7.8h-109.6z"/>
<path class="s4" d="m689.8 938.2c0 11 9 20 20 20h69.6c11 0 20-9 20-20v-7.8h-109.6z"/>
<path class="s4" d="m585.4 541.8h-23.6c-1.3 0-2.5 0.5-3.5 1.4-0.9 0.9-1.4 2.2-1.4 3.5 0 1.1 0.4 2.2 1.1 3.1l25.1 30.2 41.5 49.8 70.8 85.1c0.8 1 2 1.6 3.3 1.8 1.3 0.1 2.6-0.3 3.6-1.1q0.3-0.3 0.6-0.6l70.8-85.1 40.5-48.7 26.1-31.3c1.7-2.1 1.5-5.2-0.6-6.9-0.9-0.7-2-1.1-3.2-1.1h-23.3-43.5v-228.3c0-2.7-2.2-4.9-4.9-4.9h-131.3c-1.3 0-2.6 0.5-3.5 1.4-0.9 0.9-1.4 2.2-1.4 3.5h-5 5v228.1h-43.2z"/>
<path class="s2" d="m551.3 536.1c-2.8 2.8-4.4 6.6-4.4 10.6 0 3.5 1.2 6.8 3.4 9.5l32 38.4 29.3 35.2 76.1 91.5c2.6 3.1 6.1 5 10.1 5.3 0.5 0 0.9 0.1 1.4 0.1 3.5 0 6.8-1.2 9.5-3.5 0.7-0.6 1.3-1.2 1.9-1.9l76.1-91.5 27.9-33.6 33.3-40.1c5.3-6.3 4.4-15.8-1.9-21-2.7-2.2-6.1-3.5-9.5-3.5h-23.5-33.2v-218c0-8.2-6.7-14.9-14.9-14.9h-131.4c-4 0-7.7 1.5-10.5 4.4-2.8 2.8-4.4 6.6-4.4 10.6v218.1h-32.6-24.2c-3.9 0-7.7 1.5-10.5 4.3zm72.3-222.5h5c0-1.3 0.5-2.5 1.4-3.5 0.9-0.9 2.2-1.4 3.5-1.4h131.3c2.7 0 4.9 2.2 4.9 4.9v228.1h43.5 23.3c1.1 0 2.3 0.4 3.2 1.1 2.1 1.7 2.4 4.9 0.6 6.9l-26.1 31.3-40.5 48.7-70.7 85.2q-0.3 0.3-0.6 0.6c-1 0.8-2.3 1.2-3.6 1.1-1.3-0.1-2.5-0.7-3.3-1.8l-70.8-85.1-41.5-49.7-25.1-30.2c-0.7-0.9-1.1-2-1.1-3.1 0-1.3 0.5-2.6 1.4-3.5 0.9-0.9 2.2-1.4 3.5-1.4h23.6 43.2v-228.2z"/>
<path class="s2" d="m227 509.9c-12.2-3.5-23.9-8.4-34.9-14.7l-3-1.7-28 23.9c-1.9 0.9-4.1 0.6-5.6-0.9l-44.1-44.1c-1.5-1.5-1.8-3.7-0.9-5.6l23.9-28.1-1.7-3c-6.3-11-11.2-22.7-14.7-34.8l-1-3.4-36.8-2.7c-2-0.7-3.3-2.5-3.3-4.7v-66.6c0-2.1 1.4-4 3.3-4.7l36.8-2.7 1-3.4c3.5-12.1 8.4-23.9 14.7-34.8l1.7-3-23.9-28c-0.9-1.9-0.6-4.1 0.9-5.6l44.1-44.1c1.5-1.5 3.8-1.8 5.6-0.9l28 23.9 3-1.7c11-6.3 22.7-11.2 34.9-14.7l3.4-1 2.7-36.7c0.7-2 2.5-3.3 4.7-3.3h66.6c2.1 0 4 1.4 4.7 3.3l2.7 36.7 3.4 1c12.2 3.5 23.9 8.4 34.9 14.7l3 1.7 28-23.9c1.9-0.9 4.1-0.6 5.6 0.9l44.1 44.1c1.5 1.5 1.8 3.8 0.9 5.6l-23.9 28 1.7 3.1c6.3 11 11.2 22.7 14.7 34.8l1 3.3 36.7 2.7c2 0.7 3.3 2.5 3.3 4.7v66.5c0 2.1-1.4 4-3.3 4.7l-36.7 2.7-1 3.4c-3.5 12.1-8.5 23.9-14.7 34.8-1.4 2.4-0.5 5.5 1.9 6.8 2.4 1.4 5.5 0.5 6.8-1.9 6.1-10.7 11-22 14.7-33.7l30.8-2.2 0.4-0.1c6.6-1.7 11.2-7.6 11.2-14.4v-66.6c0-6.8-4.6-12.8-11.2-14.4l-0.4-0.1-30.8-2.3c-3.3-10.6-7.6-20.9-12.9-30.6l20-23.5 0.2-0.4c3.4-5.8 2.5-13.3-2.3-18.1l-44.1-44.1c-4.8-4.8-12.2-5.7-18.1-2.3l-0.4 0.2-23.5 20c-9.8-5.3-20.1-9.6-30.7-12.9l-2.3-30.8-0.1-0.4c-1.7-6.6-7.6-11.2-14.4-11.2h-66.6c-6.8 0-12.8 4.6-14.4 11.2l-0.1 0.4-2.3 30.8c-10.6 3.3-20.9 7.6-30.7 12.9l-23.5-20-0.4-0.2c-5.8-3.4-13.3-2.5-18.1 2.3l-44 44c-4.8 4.8-5.7 12.2-2.3 18.1l0.2 0.4 20 23.5c-5.3 9.7-9.6 20-12.9 30.6l-30.8 2.3-0.4 0.1c-6.6 1.7-11.2 7.6-11.2 14.4v66.6c0 6.8 4.6 12.8 11.2 14.4l0.4 0.1 30.8 2.3c3.3 10.6 7.6 20.9 12.9 30.6l-20 23.5-0.2 0.4c-3.4 5.8-2.5 13.3 2.3 18.1l44.1 44.1c2.9 2.9 6.7 4.4 10.5 4.4 2.6 0 5.2-0.7 7.5-2l0.4-0.2 23.5-20c10.8 5.9 22.2 10.5 34 13.9 2.7 0.8 5.4-0.8 6.2-3.4 0.8-2.8-0.8-5.6-3.4-6.3z"/>
<path class="s2" d="m271 257.3c-54.8 0-99.4 44.6-99.4 99.4 0 54.8 44.6 99.4 99.4 99.4 54.8 0 99.4-44.6 99.4-99.4 0-54.8-44.6-99.4-99.4-99.4zm0 188.8c-49.3 0-89.4-40.1-89.4-89.4 0-49.3 40.1-89.4 89.4-89.4 49.3 0 89.4 40.1 89.4 89.4 0 49.3-40.1 89.4-89.4 89.4z"/>
<path class="s2" d="m564.7 219c34 0 61.6-27.6 61.6-61.6 0-34-27.6-61.6-61.6-61.6-34 0-61.6 27.6-61.6 61.6 0 34 27.7 61.6 61.6 61.6zm0-113.1c28.5 0 51.6 23.1 51.6 51.6 0 28.5-23.1 51.6-51.6 51.6-28.5 0-51.6-23.1-51.6-51.6 0-28.5 23.2-51.6 51.6-51.6z"/>
<path class="s2" d="m448.5 188l0.4 0.1 17.3 1.3c1.8 5.6 4.1 11 6.8 16.2l-11.2 13.2-0.2 0.4c-2.5 4.3-1.8 9.7 1.7 13.2l26.5 26.5c3.5 3.5 8.9 4.2 13.2 1.7l0.4-0.2 13.2-11.2c5.2 2.7 10.6 5 16.2 6.8l1.3 17.3 0.1 0.4c1.3 4.8 5.6 8.2 10.6 8.2h39.9c5 0 9.4-3.4 10.6-8.2l0.1-0.4 1.3-17.3c5.6-1.8 11-4.1 16.2-6.8l13.2 11.2 0.4 0.2c4.3 2.5 9.7 1.8 13.2-1.7l26.5-26.5c3.5-3.5 4.2-8.9 1.7-13.2l-0.2-0.4-11.2-13.2c2.7-5.2 5-10.6 6.8-16.2l17.3-1.3 0.4-0.1c4.8-1.3 8.2-5.6 8.2-10.6v-39.9c0-5-3.4-9.4-8.2-10.6l-0.4-0.1-17.3-1.3c-1.8-5.6-4.1-11-6.8-16.2l11.2-13.2 0.2-0.4c2.5-4.3 1.8-9.8-1.7-13.3l-26.4-26.4c-3.5-3.5-9-4.2-13.3-1.7l-0.4 0.2-13.2 11.2c-5.2-2.7-10.6-5-16.2-6.8l-1.3-17.3-0.1-0.4c-1.2-4.8-5.6-8.2-10.6-8.2h-40c-4.9 0-9.3 3.4-10.6 8.2l-0.1 0.5-1.2 17.3c-5.6 1.8-11 4.1-16.2 6.8l-13.2-11.2-0.4-0.2c-4.3-2.5-9.7-1.8-13.2 1.7l-26.5 26.5c-3.5 3.5-4.2 8.9-1.7 13.2l0.2 0.4 11.2 13.2c-2.7 5.2-5 10.6-6.8 16.2l-17.3 1.3-0.4 0.1c-4.9 1.2-8.2 5.6-8.2 10.6v40c0 4.8 3.4 9.1 8.2 10.4zm1.8-50.5c0-0.3 0.2-0.7 0.5-0.8l23.1-1.7 1-3.4c2-7.1 4.9-14 8.6-20.5l1.7-3.1-15-17.6c-0.1-0.3 0-0.7 0.2-0.9l26.4-26.4c0.3-0.3 0.6-0.3 1-0.3l17.7 15 3-1.7c6.4-3.7 13.3-6.6 20.5-8.6l3.4-1 1.6-23.1c0.2-0.3 0.5-0.5 0.8-0.5h39.9c0.3 0 0.7 0.2 0.8 0.5l1.7 23.1 3.4 1c7.1 2 14 4.9 20.5 8.6l3 1.7 17.6-15c0.3-0.1 0.7 0 0.9 0.2l26.4 26.4c0.2 0.2 0.3 0.6 0.2 0.9l-15 17.6 1.7 3c3.7 6.4 6.6 13.3 8.6 20.5l1 3.4 23.1 1.7c0.3 0.2 0.5 0.5 0.5 0.8v39.9c0 0.4-0.2 0.7-0.5 0.9l-23.1 1.7-1 3.4c-2 7.1-5 14-8.6 20.5l-1.7 3 15 17.6c0.1 0.3 0 0.7-0.2 0.9l-26.4 26.4c-0.3 0.3-0.6 0.3-1 0.3l-17.6-15-3 1.7c-6.4 3.7-13.3 6.6-20.5 8.6l-3.4 1-1.7 23.1c-0.2 0.3-0.5 0.5-0.8 0.5h-39.9c-0.4 0-0.7-0.2-0.9-0.5l-1.7-23.1-3.3-1c-7.1-2-14-5-20.4-8.6l-3-1.7-17.7 15c-0.3 0.1-0.7 0-0.9-0.2l-26.4-26.4c-0.3-0.3-0.3-0.6-0.3-1l15-17.6-1.7-3c-3.7-6.4-6.6-13.3-8.6-20.5l-1-3.4-23.1-1.7c-0.3-0.2-0.5-0.5-0.5-0.8v-39.8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

+1
View File
@@ -1,6 +1,7 @@
@import "enterprise_edition/banner_component"
@import "filter/filters_component"
@import "op_primer/border_box_table_component"
@import "op_primer/full_page_prompt_component"
@import "op_primer/form_helpers"
@import "open_project/common/attribute_component"
@import "open_project/common/attribute_help_text_component"
@@ -46,12 +46,15 @@ See COPYRIGHT and LICENSE files for more details.
) do |tree_view|
parent = @hierarchy_item.parent
descendants = hierarchy_service.get_descendants(item: @hierarchy_item).value!
item_formatter = standard_tree_view_item_formatter
label_fn = lambda { |item| item_formatter.format(item:) }
item_options = {
select_variant: :single,
expanded: [parent],
current: @hierarchy_item,
disabled: [parent] + descendants
disabled: [parent] + descendants,
label_fn:
}
populate_tree_view(tree_view, @custom_field, show_root: true, item_options:)
@@ -49,6 +49,7 @@ See COPYRIGHT and LICENSE files for more details.
end
end
label_addition = secondary_text
if label_addition.present?
item_information.with_column(mr: 2) do
render(Primer::Beta::Text.new(color: :subtle)) { label_addition }
@@ -55,6 +55,15 @@ module Admin
self.class.menu_id(item: model)
end
def secondary_text
::CustomFields::Hierarchy::HierarchicalItemFormatter
.new(label: false,
number_length_limit: 42,
number_integer_digit_limit: 40,
number_precision: 40)
.format(item: model)
end
def item_link
if project_custom_field_context?
admin_settings_project_custom_field_item_path(custom_field_id, model)
@@ -30,10 +30,12 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Primer::Alpha::TreeView.new(node_variant: :anchor)) do |tree_view|
item_formatter = standard_tree_view_item_formatter
item_options = {
href_fn: lambda { |item| href_for(item) },
expanded: [@active_item, @active_item.parent],
current: @active_item
current: @active_item,
label_fn: lambda { |item| item_formatter.format(item:) }
}
populate_tree_view(tree_view, @custom_field, item_options:)
@@ -51,8 +51,8 @@ module Admin
def drop_target_config
{
"is-drag-and-drop-target": true,
"target-container-accessor": "& > ul",
generic_drag_and_drop_target: "container",
"target-container-accessor": ":scope > ul",
"target-allowed-drag-type": "enumeration"
}
end
@@ -3,7 +3,7 @@
flex_layout(align_items: :center, justify_content: :space_between) do |enumeration_container|
enumeration_container.with_column(flex_layout: true) do |enumeration_info|
enumeration_info.with_column(mr: 2) do
render(Primer::OpenProject::DragHandle.new(draggable: true))
render(Primer::OpenProject::DragHandle.new)
end
if colored?
@@ -43,11 +43,11 @@ module WorkPackages
def quote_comments_stimulus_controller(suffix = nil) = "#{stimulus_controller_namespace}--quote-comment#{suffix}"
def index_component_dom_selector
"##{WorkPackages::ActivitiesTab::IndexComponent.index_content_wrapper_key}"
"##{WorkPackages::ActivitiesTab::LazyIndexComponent.index_content_wrapper_key}"
end
def add_comment_component_dom_selector
"##{WorkPackages::ActivitiesTab::IndexComponent.add_comment_wrapper_key}"
"##{WorkPackages::ActivitiesTab::LazyIndexComponent.add_comment_wrapper_key}"
end
def stimulus_controller_namespace = "work-packages--activities-tab"
@@ -103,21 +103,14 @@ module EnterpriseEdition
def more_info_button
return if @feature_key == :teaser
render(Primer::Beta::Link.new(href: enterprise_link, target: "_blank")) do |link|
link.with_trailing_visual_icon(icon: "link-external")
link_title
end
fallback_link = OpenProject::Static::Links.url_for(:enterprise_features, :default)
helpers.static_link_to(:enterprise_features, feature_key,
href: fallback_link,
label: link_title)
end
def link_title
I18n.t("ee.upsell.#{feature_key}.link_title", default: I18n.t(:label_more_information))
end
def enterprise_link
href_value = OpenProject::Static::Links.url_for(:enterprise_features, feature_key)
default_value = OpenProject::Static::Links.url_for(:enterprise_features, :default)
href_value || default_value
end
end
end
@@ -168,7 +168,6 @@ module Filter
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
filters: [
{ name: "type", operator: "=", values: ["User"] },
{ name: "status", operator: "!", values: [Principal.statuses["locked"].to_s] }
],
searchKey: "any_name_attribute",
@@ -12,61 +12,31 @@
<%= static_link_to(EnterpriseToken.active? ? :enterprise_support : :enterprise_support_as_community) %>
</li>
<li>
<%= link_to(
t("label_openproject_website"),
OpenProject::Static::Links.url_for(
:website,
url_params: {
utm_source: "unknown",
utm_medium: "op-instance",
utm_campaign: "website-home-screen"
}
),
{
aria: { label: t("label_openproject_website") },
target: "_blank",
title: t("label_openproject_website"),
rel: "noopener"
}
) %>
<%= static_link_to :website,
label: t("label_openproject_website"),
url_params: {
utm_source: "unknown",
utm_medium: "op-instance",
utm_campaign: "website-home-screen"
} %>
</li>
<li>
<%= link_to(
t("homescreen.links.security_alerts"),
OpenProject::Static::Links.url_for(
:security_alerts,
url_params: {
utm_source: "unknown",
utm_medium: "op-instance",
utm_campaign: "security-alerts-home-screen"
}
),
{
aria: { label: t("homescreen.links.security_alerts") },
target: "_blank",
title: t("homescreen.links.security_alerts"),
rel: "noopener"
}
) %>
<%= static_link_to :security_alerts,
label: t("homescreen.links.security_alerts"),
url_params: {
utm_source: "unknown",
utm_medium: "op-instance",
utm_campaign: "security-alerts-home-screen"
} %>
</li>
<li>
<%= link_to(
t("homescreen.links.newsletter"),
OpenProject::Static::Links.url_for(
:newsletter,
url_params: {
utm_source: "unknown",
utm_medium: "op-instance",
utm_campaign: "newsletter-home-screen"
}
),
{
aria: { label: t("homescreen.links.newsletter") },
target: "_blank",
title: t("homescreen.links.newsletter"),
rel: "noopener"
}
) %>
<%= static_link_to :newsletter,
label: t("homescreen.links.newsletter"),
url_params: {
utm_source: "unknown",
utm_medium: "op-instance",
utm_campaign: "newsletter-home-screen"
} %>
</li>
<li>
<%= static_link_to :blog %>
@@ -30,6 +30,7 @@ See COPYRIGHT and LICENSE files for more details.
<%= link_to url,
class: "homescreen--links--item",
target: "_blank",
data: { allow_external_link: true },
aria_label: title,
rel: "noopener" do %>
<%= render(Primer::Beta::Octicon.new(icon: link[:icon], size: :medium)) %>
@@ -0,0 +1,85 @@
# 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 McpConfigurations
class EditRowComponent < OpPrimer::BorderBoxRowComponent
def name
render(
Primer::Beta::Text.new(
font_weight: :bold,
data: { test_selector: "mcp-configuration--config-row-name" }
)
) { config.identifier.split("/", 2).last }
end
def title
render(
Primer::Alpha::TextField.new(
name: "mcp_configurations[#{config.identifier}][title]",
label: McpConfiguration.human_attribute_name(:title),
visually_hide_label: true,
value: config.title,
data: { test_selector: "mcp-configuration--title-input-#{config.identifier}" }
)
)
end
def description
render(
Primer::Alpha::TextArea.new(
name: "mcp_configurations[#{config.identifier}][description]",
label: McpConfiguration.human_attribute_name(:description),
visually_hide_label: true,
value: config.description,
rows: 4,
data: { test_selector: "mcp-configuration--description-input-#{config.identifier}" }
)
)
end
def enabled
render(
Primer::Alpha::CheckBox.new(
name: "mcp_configurations[#{config.identifier}][enabled]",
label: McpConfiguration.human_attribute_name(:enabled),
visually_hide_label: true,
checked: config.enabled,
test_selector: "mcp-configuration--enabled-input-#{config.identifier}"
)
)
end
private
def config
model
end
end
end
@@ -0,0 +1,52 @@
# 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 McpConfigurations
class EditTableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :title, :description, :enabled
def headers
[
[:name, { caption: McpConfiguration.human_attribute_name(:name) }],
[:title, { caption: McpConfiguration.human_attribute_name(:title) }],
[:description, { caption: McpConfiguration.human_attribute_name(:description) }],
[:enabled, { caption: McpConfiguration.human_attribute_name(:enabled) }]
]
end
def mobile_title
"TODO: Does mobile even make sense?"
end
def row_class
EditRowComponent
end
end
end
@@ -0,0 +1,34 @@
<%#-- 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.
++#%>
<%= primer_form_with(url: multi_update_mcp_configurations_path) do %>
<%= render(McpConfigurations::EditTableComponent.new(rows: configurations)) %>
<%= render(Primer::Beta::Button.new(type: :submit, mt: 3)) { submit_label } %>
<% end %>
@@ -0,0 +1,45 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module McpConfigurations
class MultipleConfigurationsComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
attr_reader :submit_label
alias_method :configurations, :model
def initialize(*, submit_label:, **)
super(*, **)
@submit_label = submit_label
end
end
end
@@ -67,7 +67,7 @@ See COPYRIGHT and LICENSE files for more details.
end
flex.with_row do
render(Primer::Alpha::Banner.new(scheme: :warning, icon: :alert)) do
I18n.t(:warning, scope: i18n_scope)
I18n.t("my.access_token.created_dialog.one_time_warning")
end
end
end
@@ -23,7 +23,7 @@
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
@@ -31,14 +31,14 @@
module My
module AccessToken
module API
class RowComponent < ::RowComponent
class RowComponent < OpPrimer::BorderBoxRowComponent
def api_token
model
end
def token_name
if api_token.token_name.nil?
t("my_account.access_tokens.api.static_token_name")
if !api_token.respond_to?(:token_name) || api_token.token_name.nil?
t(:static_token_name, scope: i18n_token_scope)
else
api_token.token_name
end
@@ -57,27 +57,34 @@ module My
end
def delete_link
link_to "",
{
action: delete_action,
access_token_id: api_token.id
},
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation"),
test_selector: "api-token-revoke"
},
class: "icon icon-delete"
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: delete_path,
"aria-label": t(:button_delete),
tooltip_direction: :w,
test_selector: "api-token-revoke",
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation")
}
))
end
private
def delete_action
def delete_path
case model
when Token::API then :revoke_api_key
when Token::ICalMeeting then :revoke_ical_meeting_token
when Token::API then my_access_token_revoke_api_key_path(api_token.id)
when Token::ICalMeeting then my_access_token_revoke_ical_meeting_token_path(api_token.id)
when Token::RSS then revoke_rss_key_my_access_tokens_path(api_token.id)
end
end
def i18n_token_scope
[:my_account, :access_tokens, api_token.class.model_name.i18n_key]
end
end
end
end
@@ -31,25 +31,54 @@
module My
module AccessToken
module API
class TableComponent < ::TableComponent
def initial_sort
%i[id asc]
end
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :token_name, :created_at, :expires_on
main_column :token_name
mobile_labels :created_at, :expires_on
def sortable?
false
def initialize(title:, token_type:, **)
super(**)
@title = title
@token_type = token_type
end
def headers
[
["token_name", { caption: I18n.t("attributes.name") }],
["created_at", { caption: User.human_attribute_name(:created_at) }],
["expires_on", { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
[:token_name, { caption: I18n.t("attributes.name") }],
[:created_at, { caption: User.human_attribute_name(:created_at) }],
[:expires_on, { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
]
end
def columns
headers.map(&:first)
def mobile_title
@title
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t(:blank_title, scope: i18n_token_scope)
end
def blank_description
I18n.t(:blank_description, scope: i18n_token_scope)
end
def blank_icon
nil
end
private
def i18n_token_scope
[:my_account, :access_tokens, @token_type.model_name.i18n_key]
end
end
end
@@ -27,44 +27,37 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= component_wrapper do %>
<div class="attributes-group">
<div class="attributes-group--header">
<div class="attributes-group--header-container">
<h3 class="attributes-group--header-text"><%= t(:title, scope: i18n_scope) %></h3>
</div>
</div>
<div>
<p> <%= t(:text_hint, scope: i18n_scope) %> </p>
<% if token_available? %>
<% if @tokens.any? %>
<%= render My::AccessToken::API::TableComponent.new(rows: @tokens) %>
<% end %>
<%= render(
Primer::Beta::Button.new(
tag: :a,
mt: 3,
scheme: :secondary,
test_selector: "#{token_type.model_name.element}-token-add",
href: dialog_my_access_tokens_path(token_type: token_type.model_name.element),
data: { turbo_stream: true },
aria: { label: t(:add_button, scope: i18n_scope) },
role: "button",
title: t(:add_button, scope: i18n_scope)
)
) do |button|
button.with_leading_visual_icon(icon: add_button_icon)
t(:add_button, scope: i18n_scope)
end %>
<% else %>
<div tabindex="0" class="-info op-toast">
<div role="alert" aria-atomic="true" class="op-toast--content">
<p>
<span><%= t(:disabled_text, scope: i18n_scope) %></span>
</p>
</div>
</div>
<% end %>
</div>
</div>
<%= component_wrapper(class: "mb-4") do %>
<%= render(Primer::Beta::Subhead.new) do |component|
component.with_heading(tag: :h3, size: :medium) do
t(:title, scope: i18n_scope)
end
component.with_description do
t(:text_hint, scope: i18n_scope)
end
end %>
<% if token_available? %>
<%= render My::AccessToken::API::TableComponent.new(rows: @tokens, title: t(:table_title, scope: i18n_scope), token_type:) %>
<% if show_add_button? %>
<%= render(
Primer::Beta::Button.new(
tag: :a,
mt: 3,
scheme: :secondary,
test_selector: "#{token_type.model_name.element}-token-add",
href: add_button_path,
data: { turbo_stream: true, turbo_method: add_button_method },
aria: { label: t(:add_button, scope: i18n_scope) },
role: "button",
title: t(:add_button, scope: i18n_scope)
)
) do |button|
button.with_leading_visual_icon(icon: add_button_icon)
t(:add_button, scope: i18n_scope)
end %>
<% end %>
<% else %>
<%= render(Primer::Alpha::Banner.new(icon: :info)) { t(:disabled_text, scope: i18n_scope) } %>
<% end %>
<% end %>
@@ -58,16 +58,37 @@ module My
case token_type.to_s
when "Token::API" then Setting.rest_api_enabled?
when "Token::ICalMeeting" then Setting.ical_enabled?
when "Token::RSS" then Setting.feeds_enabled?
else raise ArgumentError, "Unknown token type: #{token_type}"
end
end
def show_add_button?
return @tokens.empty? if token_type.to_s == "Token::RSS"
true
end
def add_button_icon
case token_type.to_s
when "Token::ICalMeeting" then :rss
when "Token::RSS", "Token::ICalMeeting" then :rss
else :plus
end
end
def add_button_method
case token_type.to_s
when "Token::RSS" then :post
else :get
end
end
def add_button_path
case token_type.to_s
when "Token::RSS" then generate_rss_key_my_access_tokens_path
else dialog_my_access_tokens_path(token_type: token_type.model_name.element)
end
end
end
end
end
@@ -0,0 +1,90 @@
# 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 My
module AccessToken
module ICal
class RowComponent < OpPrimer::BorderBoxRowComponent
def api_token
model
end
def name
render(Primer::Beta::Text.new(test_selector: "ical-token-#{api_token.id}-name")) do
api_token.ical_token_query_assignment.name
end
end
def calendar
render(
Primer::Beta::Link.new(
href: project_calendar_path(id: api_token.query.id, project_id: api_token.query.project_id),
test_selector: "ical-token-#{api_token.id}-query-name"
)
) { api_token.query.name }
end
def project
render(Primer::Beta::Text.new(test_selector: "ical-token-#{api_token.id}-project-name")) do
api_token.query.project.name
end
end
def created_at
helpers.format_time(api_token.created_at)
end
def expires_on
I18n.t("my_account.access_tokens.indefinite_expiration")
end
def button_links
[delete_link].compact
end
def delete_link
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: my_access_token_revoke_ical_token_path(access_token_id: api_token.id),
"aria-label": t(:button_delete),
tooltip_direction: :w,
test_selector: "ical-token-#{api_token.id}-revoke",
data: {
turbo_method: :delete,
turbo_confirm: t("my_account.access_tokens.simple_revoke_confirmation")
}
))
end
end
end
end
end
@@ -0,0 +1,75 @@
# 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 My
module AccessToken
module ICal
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :calendar, :project, :created_at, :expires_on
main_column :name
mobile_labels :created_at, :expires_on
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
[:calendar, { caption: Token::ICal.human_attribute_name(:calendar) }],
[:project, { caption: WorkPackage.human_attribute_name(:project) }],
[:created_at, { caption: User.human_attribute_name(:created_at) }],
[:expires_on, { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
]
end
def mobile_title
I18n.t("my_account.access_tokens.ical.table_title")
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t("my_account.access_tokens.ical.blank_title")
end
def blank_description
I18n.t("my_account.access_tokens.ical.blank_description")
end
def blank_icon
nil
end
end
end
end
end
@@ -0,0 +1,89 @@
# 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 My
module AccessToken
module OAuthApplication
class RowComponent < OpPrimer::BorderBoxRowComponent
def oauth_application
model.first
end
def oauth_application_tokens
model.last
end
def name
render(Primer::Beta::Text.new(test_selector: "oauth-application-#{oauth_application.id}-name")) do
oauth_application.name
end
end
def active_tokens
render(Primer::Beta::Text.new(test_selector: "oauth-application-#{oauth_application.id}-active-tokens")) do
oauth_application_tokens.count { |t| !t.expired? && !t.revoked? }.to_s
end
end
def last_used_at
return "" if oauth_application_tokens.empty?
helpers.format_time(oauth_application_tokens.max_by(&:created_at).created_at)
end
def button_links
[delete_link].compact
end
def delete_link
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: revoke_my_oauth_application_path(application_id: oauth_application.id),
"aria-label": t(:button_delete),
tooltip_direction: :w,
test_selector: "oauth-token-row-#{oauth_application.id}-revoke",
data: {
turbo_method: :post,
turbo_confirm: t(
"oauth.revoke_my_application_confirmation",
token_count: t(
"oauth.x_active_tokens",
count: oauth_application_tokens.count
)
)
}
))
end
end
end
end
end
@@ -0,0 +1,73 @@
# 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 My
module AccessToken
module OAuthApplication
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :active_tokens, :last_used_at
main_column :name
mobile_labels :active_tokens, :last_used_at
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
[:active_tokens, { caption: I18n.t("my_account.access_tokens.oauth_application.active_tokens") }],
[:last_used_at, { caption: I18n.t("my_account.access_tokens.oauth_application.last_used_at") }]
]
end
def mobile_title
I18n.t("my_account.access_tokens.oauth_application.table_title")
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t("my_account.access_tokens.oauth_application.blank_title")
end
def blank_description
I18n.t("my_account.access_tokens.oauth_application.blank_description")
end
def blank_icon
nil
end
end
end
end
end
@@ -0,0 +1,91 @@
# 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 My
module AccessToken
module OAuthClient
class RowComponent < OpPrimer::BorderBoxRowComponent
def client_token
model
end
def name
render(Primer::Beta::Text.new(test_selector: "oauth-client-token-#{row.id}")) do
client_token.oauth_client.integration.name
end
end
def integration_type
integration_class_name = client_token.oauth_client.integration_type
integration_class = begin
integration_class_name.constantize
rescue NameError
nil
end
return I18n.t("my_account.access_tokens.oauth_client.unknown_integration") unless integration_class
integration_class.model_name.human
end
def created_at
helpers.format_time(client_token.created_at)
end
def expires_on
helpers.format_time(client_token.updated_at + client_token.expires_in.seconds)
end
def button_links
[delete_link].compact
end
def delete_link
render(Primer::Beta::IconButton.new(
icon: :trash,
scheme: :danger,
tag: :a,
href: my_access_token_remove_oauth_client_token_path(client_token),
"aria-label": t(:button_delete),
tooltip_direction: :w,
test_selector: "oauth-client-token-#{client_token.id}-remove",
data: {
turbo_method: :delete,
turbo_confirm: t(
"my_account.access_tokens.oauth_client.remove_token",
integration: client_token.oauth_client.integration.name
)
}
))
end
end
end
end
end
@@ -0,0 +1,74 @@
# 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 My
module AccessToken
module OAuthClient
class TableComponent < OpPrimer::BorderBoxTableComponent
columns :name, :integration_type, :created_at, :expires_on
main_column :name
mobile_labels :created_at, :expires_on
def headers
[
[:name, { caption: I18n.t("attributes.name") }],
[:integration_type, { caption: I18n.t("my_account.access_tokens.oauth_client.integration_type") }],
[:created_at, { caption: User.human_attribute_name(:created_at) }],
[:expires_on, { caption: I18n.t("my_account.access_tokens.headers.expiration") }]
]
end
def mobile_title
I18n.t("my_account.access_tokens.oauth_client.table_title")
end
def row_class
RowComponent
end
def has_actions?
true
end
def blank_title
I18n.t("my_account.access_tokens.oauth_client.blank_title")
end
def blank_description
I18n.t("my_account.access_tokens.oauth_client.blank_description")
end
def blank_icon
nil
end
end
end
end
end
@@ -0,0 +1,61 @@
<%#-- 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.
++#%>
<%= render(Primer::Alpha::Dialog.new(
id: "password-confirmation-dialog",
title: t(".title"),
data: { controller: "password-confirmation-dialog" },
test_selector: "op-my--password-confirmation-dialog"
)) do |dialog|
dialog.with_body do
content_tag(:form, id: "password-confirmation-form", data: { password_confirmation_dialog_target: "form" }) do
render(Primer::Alpha::TextField.new(
type: "password",
name: "password_confirmation",
label: User.human_attribute_name(:password),
caption: t(".confirmation_required"),
data: { password_confirmation_dialog_target: "passwordInput" }
))
end
end
dialog.with_footer do
concat(render(Primer::Beta::Button.new(data: { "close-dialog-id": "password-confirmation-dialog" })) { t("button_cancel") })
concat(
render(Primer::Beta::Button.new(
scheme: :primary,
type: :submit,
form: "password-confirmation-form",
test_selector: "op-my--password-confirmation-dialog--submit-button",
data: { password_confirmation_dialog_target: "submitButton" }
)) { t("button_confirm") }
)
end
end
%>
@@ -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 My
class PasswordConfirmationDialog < ApplicationComponent
include OpTurbo::Streamable
end
end
@@ -26,27 +26,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See COPYRIGHT and LICENSE files for more details.
++#%>
<colgroup>
<% column_headers&.length&.times do %>
<col>
<%= render(Primer::Box.new(classes: "op-full-page-prompt")) do %>
<%= icon %>
<%= title %>
<%= content %>
<%= render(Primer::Box.new(classes: "op-full-page-prompt--action-box", mt: 3)) do %>
<%= action %>
<% end %>
<col>
</colgroup>
<thead>
<tr>
<% column_headers&.each do |column_header| %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<%= column_header %>
</div>
</div>
</th>
<% end %>
<th>
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header"></div>
</div>
</th>
</tr>
</thead>
<% end %>
@@ -29,31 +29,31 @@
#++
module OpPrimer
# A simple component to render warning text.
#
# The warning text is rendered in the "attention" Primer color and
# uses a leading alert Octicon for additional emphasis. This component
# is designed to be used "inline", e.g. table cells, and in places
# where a Banner component might be overkill.
class WarningText < Primer::Component # rubocop:disable OpenProject/AddPreviewForViewComponent
# @param show_warning_label [Boolean] whether to show a leading "Warning:" label
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(show_warning_label: true, **system_arguments)
class FullPagePromptComponent < Primer::Component
attr_reader :system_arguments
renders_one :icon, lambda { |icon:, size: :medium, **system_arguments|
Primer::Beta::Octicon.new(icon:, size:, **system_arguments)
}
renders_one :title, lambda { |tag: :h2, **system_arguments|
Primer::Beta::Heading.new(tag:, mb: 2, font_size: 5, **system_arguments)
}
renders_one :action, types: {
button: lambda { |**system_arguments|
system_arguments[:classes] = class_names(
system_arguments[:classes],
"op-full-page-prompt--action"
)
Primer::Beta::Button.new(**system_arguments)
}
}
def initialize(**system_arguments)
super()
@show_warning_label = show_warning_label
@system_arguments = system_arguments
@system_arguments[:display] = :inline_flex
@system_arguments[:align_items] = :center
@system_arguments[:color] = :attention
end
def show_warning_label?
!!@show_warning_label
end
def render?
content.present?
end
end
end
@@ -0,0 +1,17 @@
@import "helpers"
.op-full-page-prompt
margin: 1rem auto 0
padding: 2rem
width: 500px
text-align: center
&--action-box
display: flex
&--action
flex: 1
@media screen and (max-width: $breakpoint-md)
width: 100%
margin: 2rem 0 0 0
@@ -11,7 +11,7 @@
@items.each do |option|
menu.with_item(**option.item_arguments) do |item|
item.with_leading_visual_icon(icon: option.icon) if option.icon
item.with_leading_visual_icon(icon: option.icon, classes: highlight_class_name(option, :inline)) if option.icon
item.with_description.with_content(option.description) if option.description
classes = option.colored? ? highlight_class_name(option, :inline) : ""
@@ -1,7 +0,0 @@
<%= render(Primer::Beta::Text.new(**@system_arguments)) do %>
<%= render(Primer::Beta::Octicon.new(icon: :"alert-fill", size: :xsmall, mr: 2, aria: { hidden: true })) %>
<span>
<%= render(Primer::Beta::Text.new(tag: :strong).with_content("#{I18n.t(:warning)}:")) if show_warning_label? %>
<%= content %>
</span>
<% end %>
@@ -1,13 +1,17 @@
<% unless check_all_button? %>
<% with_check_all_button # set the default %>
<% with_check_all_button { I18n.t(:button_check_all) } # set the default %>
<% end %>
<% unless uncheck_all_button? %>
<% with_uncheck_all_button # set the default %>
<% with_uncheck_all_button { I18n.t(:button_uncheck_all) } # set the default %>
<% end %>
<% unless separator? %>
<% with_separator { "|" } # set the default %>
<% end %>
<%= render(Primer::BaseComponent.new(**@system_arguments)) do %>
<%= check_all_button %>
|
<%= separator %>
<%= uncheck_all_button %>
<% end %>
@@ -37,10 +37,13 @@ module OpenProject
CHECKABLE_CONTROLLER_SELECTOR = "[data-controller~='checkable']"
renders_one :check_all_button, ->(text: I18n.t(:button_check_all), **system_arguments) {
renders_one :separator
renders_one :check_all_button, ->(**system_arguments) {
action = use_outlet? ? "check-all#checkAll:stop" : "checkable#checkAll:stop"
controls = checkable_id if use_outlet?
system_arguments[:scheme] ||= :link
system_arguments[:id] = "#{base_id}-check-all"
system_arguments[:data] = merge_data(
system_arguments, {
@@ -51,13 +54,14 @@ module OpenProject
system_arguments, { aria: { controls: } }
)
Primer::Beta::Button.new(scheme: :link, **system_arguments).with_content(text)
Primer::Beta::Button.new(**system_arguments)
}
renders_one :uncheck_all_button, ->(text: I18n.t(:button_uncheck_all), **system_arguments) {
renders_one :uncheck_all_button, ->(**system_arguments) {
action = use_outlet? ? "check-all#uncheckAll:stop" : "checkable#uncheckAll:stop"
controls = checkable_id if use_outlet?
system_arguments[:scheme] ||= :link
system_arguments[:id] = "#{base_id}-uncheck-all"
system_arguments[:data] = merge_data(
system_arguments, {
@@ -68,7 +72,7 @@ module OpenProject
system_arguments, { aria: { controls: } }
)
Primer::Beta::Button.new(scheme: :link, **system_arguments).with_content(text)
Primer::Beta::Button.new(**system_arguments)
}
# This Component can be used in *two* ways.
@@ -1,23 +0,0 @@
<%= render(
Primer::Alpha::Dialog.new(
title: t("js.label_export"),
id: MODAL_ID
)
) do |d| %>
<% d.with_header(variant: :large) %>
<% d.with_body do %>
<ul class="op-export-options">
<% helpers.supported_export_formats.each do |key| %>
<li class="op-export-options--option">
<%= link_to projects_path(format: key, **helpers.projects_query_params.except(:page, :per_page)),
"data-controller": "job-dialog",
"data-job-dialog-close-dialog-id-value": MODAL_ID,
class: "op-export-options--option-link" do %>
<%= helpers.op_icon("icon-big icon-export-#{key}") %>
<span class="op-export-options--option-label"><%= t("export.format.#{key}") %></span>
<% end %>
</li>
<% end %>
</ul>
<% end %>
<% end %>
@@ -126,13 +126,18 @@
end
if can_export?
menu.with_item(
tag: :a,
label: t("js.label_export"),
href: export_list_modal_projects_path(projects_query_params),
content_arguments: { data: { controller: "async-dialog" }, rel: "nofollow" }
) do |item|
item.with_leading_visual_icon(icon: :download)
menu.with_sub_menu_item(label: t("js.label_export")) do |submenu|
submenu.with_leading_visual_icon(icon: :download)
helpers.supported_export_formats.each do |key|
submenu.with_item(
label: t("export.format.#{key}"),
tag: :a,
href: projects_path(format: key, **helpers.projects_query_params.except(:page, :per_page)),
content_arguments: { data: { controller: "job-dialog" }, rel: "nofollow" }
) do |item|
item.with_leading_visual_icon(icon: "op-#{key == "csv" ? "file-csv" : key}")
end
end
end
end
@@ -32,18 +32,19 @@ module Projects
class ProjectCreationFooterComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def initialize(form_identifier:, project:, template:, current_step:)
def initialize(form_identifier:, project:, template:, current_step:, cancel_href:)
@form_identifier = form_identifier
@project = project
@template = template
@current_step = current_step
@cancel_href = cancel_href
super
end
def call
render(StepWizard::FooterComponent.new(form_identifier:, total_steps:, current_step:)) do |footer|
footer.with_cancel_button(href: projects_path)
footer.with_cancel_button(href: cancel_href)
footer.with_continue_button(**continue_button_args)
footer.with_submit_button(**submit_button_args)
if show_progress_bar?
@@ -52,7 +53,7 @@ module Projects
end
end
attr_reader :form_identifier, :project, :template, :current_step
attr_reader :form_identifier, :project, :template, :current_step, :cancel_href
private
+33 -20
View File
@@ -154,19 +154,45 @@ module Projects
end
def name
content = [content_tag(:i, "", class: "projects-table--hierarchy-icon")]
content = [
hierarchy_icon,
name_link_section,
archived_label,
workspace_type_badge
].compact_blank
content << helpers.link_to_project(project, {}, { data: { turbo: false } }, false)
content_tag(:div, safe_join(content), class: "projects-table--name")
end
if project.archived?
content << content_tag(:span, "(#{I18n.t('project.archive.archived')})", class: "archived-label")
def hierarchy_icon
content_tag(:i, "", class: "projects-table--hierarchy-icon")
end
def name_link_section
content_tag(:span, class: "projects-table--name-text") do
helpers.link_to_project(project, {}, { data: { turbo: false } }, false)
end
end
if workspace_type_badge && OpenProject::FeatureDecisions.portfolio_models_active?
content << workspace_type_badge
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"])
render(Primer::Beta::Text.new(classes: "projects-table--name-description")) do
icon = render(Primer::Beta::Octicon.new(
icon: helpers.workspace_icon(project.workspace_type),
size: :xsmall
))
safe_join([icon, " ", I18n.t(:"label_#{project.workspace_type}")])
end
end
safe_join(content, " ")
def archived_label
return unless project.archived?
content_tag(:span, "(#{I18n.t('project.archive.archived')})", class: "archived-label")
end
def project_status
@@ -429,18 +455,5 @@ module Projects
def current_page
table.model.current_page.to_s
end
def workspace_type_badge
# Only show icon and type for non-project workspaces
return unless project.workspace_type.in?(["portfolio", "program"])
render(Primer::Beta::Text.new(classes: "description")) do
icon = render(Primer::Beta::Octicon.new(
icon: helpers.workspace_icon(project.workspace_type),
size: :xsmall
))
safe_join([icon, " ", I18n.t(:"label_#{project.workspace_type}")])
end
end
end
end
@@ -32,18 +32,51 @@
end
end
custom_field_container.with_column(py: 1, mr: 2) do
render(
Primer::Alpha::ToggleSwitch.new(
src: toggle_path,
csrf_token: form_authenticity_token,
data: toggle_data_attributes,
checked: toggle_checked?,
enabled: toggle_enabled?,
size: :small,
status_label_position: :start,
classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator"
concat(
render(
Primer::Alpha::ToggleSwitch.new(
src: toggle_path,
csrf_token: form_authenticity_token,
data: toggle_data_attributes,
checked: toggle_checked?,
enabled: toggle_enabled?,
size: :small,
status_label_position: :start,
classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator"
)
)
)
# As a courtesy to users, we show a hover card explaining why the toggle is disabled.
if toggle_disabled?
concat(
content_tag(:div, class: "op-hover-card--hidden-container") do
flex_layout(
id: unique_hovercard_id,
classes: "op-project-custom-field--popover",
data: {
test_selector: "op-project-custom-field--hover-card-#{@project_custom_field.id}"
}
) do |hover_card|
hover_card.with_column do
render(Primer::Beta::Text.new) do
if configured_as_creation_wizard_assignee?
t(
"projects.settings.project_custom_fields.enabled_via_assignee_when_submitted_html",
pir_submission_url: project_settings_creation_wizard_path(tab: "submission")
)
else
t(
"projects.settings.creation_wizard.enabled_because_required_html",
admin_settings_url: admin_settings_project_custom_fields_path
)
end
end
end
end
end
)
end
end
end
end
@@ -45,6 +45,8 @@ module Projects
end
def toggle_checked?
return true if toggle_force_checked?
mapping = @project_custom_field_project_mappings.find do |m|
m.custom_field_id == @project_custom_field.id
end
@@ -57,15 +59,22 @@ module Projects
end
end
def toggle_enabled?
!@project_custom_field.required?
def toggle_force_checked?
@project_custom_field.required? ||
configured_as_creation_wizard_assignee?
end
def toggle_data_attributes
{
"turbo-method": :post,
test_selector: "toggle-creation-wizard-project-custom-field-#{@project_custom_field.id}"
}
}.tap do |data|
if toggle_disabled?
# Add hover card that explains why this toggle switch is disabled
data[:hover_card_trigger_target] = "trigger"
data[:hover_card_popover_id] = unique_hovercard_id
end
end
end
end
end
@@ -31,24 +31,55 @@
end
end
end
# py: 1 quick fix: prevents the row from bouncing as the toggle switch currently changes height while toggling
custom_field_container.with_column(py: 1, mr: 2) do
# buggy currently:
# small variant + status_label_position: :start leads to a small bounce while toggling
# behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small
# quick fix: don't display loading indicator which is causing the bounce
render(
Primer::Alpha::ToggleSwitch.new(
src: toggle_path,
csrf_token: form_authenticity_token,
data: toggle_data_attributes,
checked: toggle_checked?,
enabled: toggle_enabled?,
size: :small,
status_label_position: :start,
classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator"
concat(
# buggy currently:
# small variant + status_label_position: :start leads to a small bounce while toggling
# behavior can be seen on primer's viewbook as well -> https://view-components-storybook.eastus.cloudapp.azure.com/view-components/lookbook/inspect/primer/alpha/toggle_switch/small
# quick fix: don't display loading indicator which is causing the bounce
render(
Primer::Alpha::ToggleSwitch.new(
src: toggle_path,
csrf_token: form_authenticity_token,
data: toggle_data_attributes,
checked: toggle_checked?,
enabled: toggle_enabled?,
size: :small,
status_label_position: :start,
classes: "op-primer-adjustments__toggle-switch--hidden-loading-indicator"
)
)
)
# As a courtesy to users, we show a hover card explaining why the toggle is disabled.
if toggle_disabled?
concat(
content_tag(:div, class: "op-hover-card--hidden-container") do
flex_layout(
id: unique_hovercard_id,
classes: "op-project-custom-field--popover",
data: {
test_selector: "op-project-custom-field--hover-card-#{@project_custom_field.id}"
}
) do |hover_card|
hover_card.with_column do
render(Primer::Beta::Text.new) do
if configured_as_creation_wizard_assignee?
t(
"projects.settings.project_custom_fields.enabled_via_assignee_when_submitted_html",
pir_submission_url: project_settings_creation_wizard_path(tab: "submission")
)
else
t("projects.settings.project_custom_fields.is_for_all_blank_slate.description")
end
end
end
end
end
)
end
end
end
end
@@ -66,19 +66,39 @@ module Projects
end
def toggle_checked?
active_in_project?
active_in_project? || toggle_force_checked?
end
def toggle_enabled?
!@project_custom_field.is_for_all?
def toggle_force_checked?
@project_custom_field.is_for_all? ||
configured_as_creation_wizard_assignee?
end
def toggle_enabled? = !toggle_disabled?
def toggle_disabled? = toggle_force_checked?
def toggle_data_attributes
{
"turbo-method": :put,
"turbo-stream": true,
test_selector: "toggle-project-custom-field-mapping-#{@project_custom_field.id}"
}
}.tap do |data|
if toggle_disabled?
# Add hover card that explains why this toggle switch is disabled
data[:hover_card_trigger_target] = "trigger"
data[:hover_card_popover_id] = unique_hovercard_id
end
end
end
def configured_as_creation_wizard_assignee?
@project.project_creation_wizard_enabled? &&
@project.project_creation_wizard_assignee_custom_field_id == @project_custom_field.id
end
def unique_hovercard_id
"project-custom-field-#{@project_custom_field.id}-disabled-reason"
end
end
end
@@ -31,10 +31,12 @@
height: 100%
min-height: 0
grid-template-areas: "sidebar main help"
grid-template-columns: 300px 1fr 350px
gap: var(--base-size-16, 1rem)
grid-template-columns: 300px 1fr 330px
overflow: hidden
&--main
padding: 0 var(--base-size-16, 1rem)
&--sidebar,
&--help
border: var(--borderWidth-thin, 1px) solid var(--borderColor-default)
@@ -59,7 +59,7 @@ module Settings
def drop_target_config
{
"is-drag-and-drop-target": true,
generic_drag_and_drop_target: "container",
"target-allowed-drag-type": "section" # the type of dragged items which are allowed to be dropped in this target
}
end
@@ -56,7 +56,7 @@ module Settings
def drag_and_drop_target_config
{
"is-drag-and-drop-target": true,
generic_drag_and_drop_target: "container",
"target-container-accessor": ".Box > ul", # the accessor of the container that contains the drag and drop items
"target-id": @project_custom_field_section.id, # the id of the target
"target-allowed-drag-type": "custom-field" # the type of dragged items which are allowed to be dropped in this target
@@ -31,10 +31,12 @@ See COPYRIGHT and LICENSE files for more details.
component_wrapper do
flex_layout(data: wrapper_data_attributes) do |flex|
flex.with_row do
render EnterpriseEdition::BannerComponent.new(:customize_life_cycle,
variant: :medium,
image: "enterprise/project-lifecycle.png",
mb: 3)
render EnterpriseEdition::BannerComponent.new(
:customize_life_cycle,
variant: :medium,
image: "enterprise/project-lifecycle.png",
mb: 3
)
end
if allowed_to_customize_life_cycle?
@@ -56,11 +58,13 @@ See COPYRIGHT and LICENSE files for more details.
}
)
subheader.with_action_button(scheme: :primary,
leading_icon: :plus,
label: I18n.t("settings.project_phase_definitions.label_add_description"),
tag: :a,
href: new_admin_settings_project_phase_definition_path) do
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
label: I18n.t("settings.project_phase_definitions.label_add_description"),
tag: :a,
href: new_admin_settings_project_phase_definition_path
) do
I18n.t("settings.project_phase_definitions.label_add")
end
end
@@ -69,7 +73,7 @@ See COPYRIGHT and LICENSE files for more details.
flex.with_row do
render(border_box_container(mb: 3, data: drop_target_config)) do |component|
component.with_header(font_weight: :bold, py: 2) do
component.with_header(font_weight: :bold) do
flex_layout(justify_content: :space_between, align_items: :center) do |header_container|
header_container.with_column do
render(Primer::Beta::Text.new(font_weight: :bold)) do
@@ -47,8 +47,8 @@ module Settings
def drop_target_config
{
"is-drag-and-drop-target": true,
"target-container-accessor": "& > ul",
generic_drag_and_drop_target: "container",
"target-container-accessor": ":scope > ul",
"target-allowed-drag-type": "life-cycle-step-definition"
}
end
@@ -48,8 +48,8 @@ module WorkPackageTypes
def drag_and_drop_target_config
{
"is-drag-and-drop-target": true,
"target-container-accessor": "& > ul",
generic_drag_and_drop_target: "container",
"target-container-accessor": ":scope > ul",
"target-allowed-drag-type": "template",
test_selector: "pdf-export-template-rows"
}
@@ -30,7 +30,7 @@ See COPYRIGHT and LICENSE files for more details.
component_wrapper do
flex_layout(align_items: :center) do |item_information|
item_information.with_column(mr: 2) do
render(Primer::OpenProject::DragHandle.new(draggable: true))
render(Primer::OpenProject::DragHandle.new)
end
item_information.with_column(flex: 1, flex_layout: true) do |title_container|
title_container.with_column(pt: 1, mr: 2) do
@@ -1,53 +0,0 @@
<%=
if deferred
render(
WorkPackages::ActivitiesTab::Journals::IndexComponent.new(work_package:, filter:, deferred:)
)
else
component_wrapper(tag: "turbo-frame") do
flex_layout(classes: "work-packages-activities-tab-index-component") do |activities_tab_wrapper_container|
activities_tab_wrapper_container.with_row(classes: "work-packages-activities-tab-index-component--errors") do
render(
WorkPackages::ActivitiesTab::ErrorStreamComponent.new
)
end
activities_tab_wrapper_container.with_row(id: index_content_wrapper_key, data: wrapper_data_attributes) do
flex_layout do |activities_tab_container|
activities_tab_container.with_row(mb: 2) do
render(
WorkPackages::ActivitiesTab::Journals::FilterAndSortingComponent.new(
work_package:,
filter:
)
)
end
activities_tab_container.with_row(flex_layout: true, classes: "work-packages-activities-tab-index-component--content-container", mt: 3) do |journals_wrapper_container|
journals_wrapper_container.with_row(
classes: "work-packages-activities-tab-index-component--journals-container work-packages-activities-tab-index-component--journals-container_with-initial-input-compensation",
data: { "work-packages--activities-tab--index-target": "journalsContainer" }
) do
render(list_journals_component)
end
if adding_comment_allowed?
journals_wrapper_container.with_row(
id: add_comment_wrapper_key,
classes: "work-packages-activities-tab-index-component--input-container work-packages-activities-tab-index-component--input-container_sort-#{journal_sorting}",
p: 3,
bg: :subtle,
data: add_comment_wrapper_data_attributes
) do
render(
WorkPackages::ActivitiesTab::Journals::NewComponent.new(work_package:, filter:, last_server_timestamp:)
)
end
end
end
end
end
end
end
end
%>
@@ -1,129 +0,0 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module WorkPackages
module ActivitiesTab
class IndexComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include WorkPackages::ActivitiesTab::SharedHelpers
include WorkPackages::ActivitiesTab::StimulusControllers
def initialize(work_package:, last_server_timestamp:, filter: :all, deferred: false)
super
@work_package = work_package
@filter = filter
@last_server_timestamp = last_server_timestamp
@deferred = deferred
end
def self.wrapper_key = "work-package-activities-tab-content"
def self.index_content_wrapper_key = WorkPackages::ActivitiesTab::StimulusControllers.index_stimulus_controller
def self.add_comment_wrapper_key = "work-packages-activities-tab-add-comment-component"
delegate :index_content_wrapper_key, :add_comment_wrapper_key, to: :class
def list_journals_component
WorkPackages::ActivitiesTab::Journals::IndexComponent.new(work_package:, filter:)
end
private
attr_reader :work_package, :filter, :last_server_timestamp, :deferred
def wrapper_data_attributes # rubocop:disable Metrics/AbcSize
stimulus_controllers = {
controller: [
index_stimulus_controller,
polling_stimulus_controller,
editor_stimulus_controller,
auto_scrolling_stimulus_controller,
stems_stimulus_controller
].join(" ")
}
stimulus_controller_values = {
editor_stimulus_controller("-unsaved-changes-confirmation-message-value") => unsaved_changes_confirmation_message,
index_stimulus_controller("-notification-center-path-name-value") => notifications_path,
index_stimulus_controller("-sorting-value") => journal_sorting,
index_stimulus_controller("-filter-value") => filter,
index_stimulus_controller("-user-id-value") => User.current.id,
index_stimulus_controller("-work-package-id-value") => work_package.id,
polling_stimulus_controller("-last-server-timestamp-value") => last_server_timestamp,
polling_stimulus_controller("-polling-interval-in-ms-value") => polling_interval,
polling_stimulus_controller("-show-conflict-flash-message-url-value") => show_conflict_flash_message_work_packages_path,
polling_stimulus_controller("-update-streams-path-value") => update_streams_work_package_activities_path(work_package)
}
stimulus_controller_outlets = {
editor_stimulus_controller("-#{auto_scrolling_stimulus_controller}-outlet") => index_component_dom_selector,
editor_stimulus_controller("-#{polling_stimulus_controller}-outlet") => index_component_dom_selector,
editor_stimulus_controller("-#{stems_stimulus_controller}-outlet") => index_component_dom_selector,
polling_stimulus_controller("-#{auto_scrolling_stimulus_controller}-outlet") => index_component_dom_selector,
polling_stimulus_controller("-#{stems_stimulus_controller}-outlet") => index_component_dom_selector
}
{ test_selector: "op-wp-activity-tab" }
.merge(stimulus_controllers)
.merge(stimulus_controller_values)
.merge(stimulus_controller_outlets)
end
def add_comment_wrapper_data_attributes
{
test_selector: "op-work-package-journal--new-comment-component",
controller: internal_comment_stimulus_controller,
internal_comment_stimulus_controller("-target") => "formContainer",
action: editor_stimulus_controller(":onSubmit-end@window->#{internal_comment_stimulus_controller}#onSubmitEnd"),
internal_comment_stimulus_controller("-highlight-class") => "work-packages-activities-tab-journals-new-component--journal-notes-body__internal-comment", # rubocop:disable Layout/LineLength
internal_comment_stimulus_controller("-hidden-class") => "d-none",
internal_comment_stimulus_controller("-is-internal-value") => false, # Initial value
internal_comment_stimulus_controller("-#{editor_stimulus_controller}-outlet") => index_component_dom_selector
}
end
def polling_interval
# Polling interval should only be adjustable in test environment
if Rails.env.test?
ENV["WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS"].presence || 10000
else
10000
end
end
def adding_comment_allowed?
User.current.allowed_in_work_package?(:add_work_package_comments, @work_package)
end
def unsaved_changes_confirmation_message
I18n.t("activities.work_packages.activity_tab.unsaved_changes_confirmation_message")
end
end
end
end
@@ -1,76 +0,0 @@
<%=
if deferred
helpers.turbo_frame_tag("work-package-activities-tab-content-older-journals") do
flex_layout do |older_journals_container|
older_journals.each do |record|
older_journals_container.with_row do
if record.is_a?(Changeset)
render(WorkPackages::ActivitiesTab::Journals::RevisionComponent.new(changeset: record, filter:))
else
render(
WorkPackages::ActivitiesTab::Journals::ItemComponent.new(
journal: record, filter:,
grouped_emoji_reactions: wp_journals_grouped_emoji_reactions[record.id]
)
)
end
end
end
end
end
else
component_wrapper(class: "work-packages-activities-tab-journals-index-component") do
flex_layout(data: { test_selector: "op-wp-journals-#{filter}-#{journal_sorting}" }) do |journals_index_wrapper_container|
journals_index_wrapper_container.with_row(
classes: "work-packages-activities-tab-journals-index-component--journals-inner-container",
mb: inner_container_margin_bottom
) do
flex_layout(
id: insert_target_modifier_id,
data: { test_selector: "op-wp-journals-container" }
) do |journals_index_container|
if empty_state?
journals_index_container.with_row(mt: 2, mb: 3) do
render(
WorkPackages::ActivitiesTab::Journals::EmptyComponent.new
)
end
end
if !journal_sorting.desc? && journals.count > MAX_RECENT_JOURNALS
journals_index_container.with_row do
helpers.turbo_frame_tag("work-package-activities-tab-content-older-journals", src: work_package_activities_path(work_package, filter:, deferred: true))
end
end
recent_journals.each do |record|
journals_index_container.with_row do
if record.is_a?(Changeset)
render(WorkPackages::ActivitiesTab::Journals::RevisionComponent.new(changeset: record, filter:))
else
render(
WorkPackages::ActivitiesTab::Journals::ItemComponent.new(
journal: record, filter:,
grouped_emoji_reactions: wp_journals_grouped_emoji_reactions[record.id]
)
)
end
end
end
if journal_sorting.desc? && journals.count > MAX_RECENT_JOURNALS
journals_index_container.with_row do
helpers.turbo_frame_tag("work-package-activities-tab-content-older-journals", src: work_package_activities_path(work_package, filter:, deferred: true))
end
end
end
end
unless empty_state?
journals_index_wrapper_container
.with_row(classes: "work-packages-activities-tab-journals-index-component--stem-connection")
end
end
end
end
%>
@@ -1,141 +0,0 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module WorkPackages
module ActivitiesTab
module Journals
class IndexComponent < ApplicationComponent
MAX_RECENT_JOURNALS = 30
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include WorkPackages::ActivitiesTab::SharedHelpers
def initialize(work_package:, filter: :all, deferred: false)
super
@work_package = work_package
@filter = filter
@deferred = deferred
end
private
attr_reader :work_package, :filter, :deferred
def insert_target_modified?
true
end
def insert_target_modifier_id
"work-package-journal-days"
end
def base_journals
combine_and_sort_records(fetch_journals, fetch_revisions)
end
def fetch_journals
API::V3::Activities::ActivityEagerLoadingWrapper.wrap(
work_package
.journals
.internal_visible
.includes(:user, :customizable_journals, :attachable_journals, :storable_journals, :notifications)
.reorder(version: journal_sorting)
.with_sequence_version
)
end
def fetch_revisions
work_package.changesets.includes(:user, :repository)
end
def combine_and_sort_records(journals, revisions)
(journals + revisions).sort_by do |record|
timestamp = record_timestamp(record)
journal_sorting.desc? ? [-timestamp, -record.id] : [timestamp, record.id]
end
end
def record_timestamp(record)
if record.is_a?(API::V3::Activities::ActivityEagerLoadingWrapper)
record.created_at&.to_i
elsif record.is_a?(Changeset)
record.committed_on.to_i
end
end
def journals
base_journals
end
def recent_journals
if journal_sorting.desc?
base_journals.first(MAX_RECENT_JOURNALS)
else
base_journals.last(MAX_RECENT_JOURNALS)
end
end
def older_journals
if journal_sorting.desc?
base_journals.drop(MAX_RECENT_JOURNALS)
else
base_journals.take(base_journals.size - MAX_RECENT_JOURNALS)
end
end
def journal_with_notes
work_package
.journals
.where.not(notes: "")
end
def wp_journals_grouped_emoji_reactions
@wp_journals_grouped_emoji_reactions ||=
EmojiReactions::GroupedQueries.grouped_work_package_journals_emoji_reactions_by_reactable(work_package)
end
def empty_state?
filter == :only_comments && journal_with_notes.empty?
end
def inner_container_margin_bottom
if journal_sorting.desc?
3
else
0
end
end
end
end
end
end
@@ -31,12 +31,19 @@
module WorkPackages
module ActivitiesTab
module Journals
class LazyIndexComponent < IndexComponent
def initialize(work_package:, journals:, paginator:, filter: :all)
super(work_package:, filter:, deferred: false)
class LazyIndexComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include WorkPackages::ActivitiesTab::SharedHelpers
def initialize(work_package:, journals:, paginator:, filter: :all)
super
@work_package = work_package
@journals = journals
@paginator = paginator
@filter = filter
end
def pages
@@ -67,11 +74,30 @@ module WorkPackages
private
attr_reader :journals, :paginator
attr_reader :work_package, :journals, :paginator, :filter
def insert_target_modified?
true
end
def empty_state?
filter == :only_comments && journal_with_notes.empty?
end
def wp_journals_grouped_emoji_reactions
@wp_journals_grouped_emoji_reactions ||=
EmojiReactions::GroupedQueries.grouped_work_package_journals_emoji_reactions_by_reactable(work_package)
end
def inner_container_margin_bottom
journal_sorting.desc? ? 3 : 0
end
def journal_with_notes
work_package
.journals
.where.not(notes: "")
end
end
end
end
@@ -78,10 +78,8 @@
data: { "#{internal_comment_stimulus_controller}-target": "learnMoreLink" },
pb: 1
) do
render(Primer::Beta::Link.new(href: learn_more_static_link_url, target: "_blank")) do |link|
link.with_trailing_visual_icon(icon: "link-external", size: :small)
I18n.t("label_learn_more")
end
helpers.static_link_to(:enterprise_features, :internal_comments,
label: I18n.t(:label_learn_more))
end
end
end
@@ -0,0 +1,47 @@
<%=
component_wrapper(tag: "turbo-frame") do
flex_layout(classes: "work-packages-activities-tab-index-component") do |activities_tab_wrapper_container|
activities_tab_wrapper_container.with_row(classes: "work-packages-activities-tab-index-component--errors") do
render(
WorkPackages::ActivitiesTab::ErrorStreamComponent.new
)
end
activities_tab_wrapper_container.with_row(id: index_content_wrapper_key, data: wrapper_data_attributes) do
flex_layout do |activities_tab_container|
activities_tab_container.with_row(mb: 2) do
render(
WorkPackages::ActivitiesTab::Journals::FilterAndSortingComponent.new(
work_package:,
filter:
)
)
end
activities_tab_container.with_row(flex_layout: true, classes: "work-packages-activities-tab-index-component--content-container", mt: 3) do |journals_wrapper_container|
journals_wrapper_container.with_row(
classes: "work-packages-activities-tab-index-component--journals-container work-packages-activities-tab-index-component--journals-container_with-initial-input-compensation",
data: { "work-packages--activities-tab--index-target": "journalsContainer" }
) do
render(list_journals_component)
end
if adding_comment_allowed?
journals_wrapper_container.with_row(
id: add_comment_wrapper_key,
classes: "work-packages-activities-tab-index-component--input-container work-packages-activities-tab-index-component--input-container_sort-#{journal_sorting}",
p: 3,
bg: :subtle,
data: add_comment_wrapper_data_attributes
) do
render(
WorkPackages::ActivitiesTab::Journals::NewComponent.new(work_package:, filter:, last_server_timestamp:)
)
end
end
end
end
end
end
end
%>
@@ -30,14 +30,28 @@
module WorkPackages
module ActivitiesTab
class LazyIndexComponent < IndexComponent
def initialize(work_package:, journals:, paginator:, last_server_timestamp:, filter: :all)
super(work_package:, last_server_timestamp:, filter:, deferred: false)
class LazyIndexComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include WorkPackages::ActivitiesTab::SharedHelpers
include WorkPackages::ActivitiesTab::StimulusControllers
def initialize(work_package:, journals:, paginator:, last_server_timestamp:, filter: :all)
super
@work_package = work_package
@journals = journals
@paginator = paginator
@last_server_timestamp = last_server_timestamp
@filter = filter
end
def self.wrapper_key = "work-package-activities-tab-content"
def self.index_content_wrapper_key = WorkPackages::ActivitiesTab::StimulusControllers.index_stimulus_controller
def self.add_comment_wrapper_key = "work-packages-activities-tab-add-comment-component"
delegate :index_content_wrapper_key, :add_comment_wrapper_key, to: :class
def list_journals_component
WorkPackages::ActivitiesTab::Journals::LazyIndexComponent
.new(work_package:, journals:, filter:, paginator:)
@@ -45,7 +59,73 @@ module WorkPackages
private
attr_reader :journals, :paginator
attr_reader :work_package, :journals, :paginator, :filter, :last_server_timestamp
def wrapper_data_attributes # rubocop:disable Metrics/AbcSize
stimulus_controllers = {
controller: [
index_stimulus_controller,
polling_stimulus_controller,
editor_stimulus_controller,
auto_scrolling_stimulus_controller,
stems_stimulus_controller
].join(" ")
}
stimulus_controller_values = {
editor_stimulus_controller("-unsaved-changes-confirmation-message-value") => unsaved_changes_confirmation_message,
index_stimulus_controller("-notification-center-path-name-value") => notifications_path,
index_stimulus_controller("-sorting-value") => journal_sorting,
index_stimulus_controller("-filter-value") => filter,
index_stimulus_controller("-user-id-value") => User.current.id,
index_stimulus_controller("-work-package-id-value") => work_package.id,
polling_stimulus_controller("-last-server-timestamp-value") => last_server_timestamp,
polling_stimulus_controller("-polling-interval-in-ms-value") => polling_interval,
polling_stimulus_controller("-show-conflict-flash-message-url-value") => show_conflict_flash_message_work_packages_path,
polling_stimulus_controller("-update-streams-path-value") => update_streams_work_package_activities_path(work_package)
}
stimulus_controller_outlets = {
editor_stimulus_controller("-#{auto_scrolling_stimulus_controller}-outlet") => index_component_dom_selector,
editor_stimulus_controller("-#{polling_stimulus_controller}-outlet") => index_component_dom_selector,
editor_stimulus_controller("-#{stems_stimulus_controller}-outlet") => index_component_dom_selector,
polling_stimulus_controller("-#{auto_scrolling_stimulus_controller}-outlet") => index_component_dom_selector,
polling_stimulus_controller("-#{stems_stimulus_controller}-outlet") => index_component_dom_selector
}
{ test_selector: "op-wp-activity-tab" }
.merge(stimulus_controllers)
.merge(stimulus_controller_values)
.merge(stimulus_controller_outlets)
end
def add_comment_wrapper_data_attributes
{
test_selector: "op-work-package-journal--new-comment-component",
controller: internal_comment_stimulus_controller,
internal_comment_stimulus_controller("-target") => "formContainer",
action: editor_stimulus_controller(":onSubmit-end@window->#{internal_comment_stimulus_controller}#onSubmitEnd"),
internal_comment_stimulus_controller("-highlight-class") => "work-packages-activities-tab-journals-new-component--journal-notes-body__internal-comment", # rubocop:disable Layout/LineLength
internal_comment_stimulus_controller("-hidden-class") => "d-none",
internal_comment_stimulus_controller("-is-internal-value") => false, # Initial value
internal_comment_stimulus_controller("-#{editor_stimulus_controller}-outlet") => index_component_dom_selector
}
end
def polling_interval
# Polling interval should only be adjustable in test environment
if Rails.env.test?
ENV["WORK_PACKAGES_ACTIVITIES_TAB_POLLING_INTERVAL_IN_MS"].presence || 10000
else
10000
end
end
def adding_comment_allowed?
User.current.allowed_in_work_package?(:add_work_package_comments, @work_package)
end
def unsaved_changes_confirmation_message
I18n.t("activities.work_packages.activity_tab.unsaved_changes_confirmation_message")
end
end
end
end
+8 -8
View File
@@ -32,9 +32,14 @@ module CustomFields
class BaseContract < ::ModelContract
include RequiresAdminGuard
attribute :admin_only
attribute :allow_non_open_versions
attribute :content_right_to_left
attribute :custom_field_section_id
attribute :default_value
attribute :editable
attribute :type
attribute :field_format
attribute :formula
attribute :is_filter
attribute :is_for_all
attribute :is_required do
@@ -42,17 +47,12 @@ module CustomFields
end
attribute :max_length
attribute :min_length
attribute :multi_value
attribute :name
attribute :possible_values
attribute :regexp
attribute :formula
attribute :searchable
attribute :admin_only
attribute :default_value
attribute :multi_value
attribute :content_right_to_left
attribute :custom_field_section_id
attribute :allow_non_open_versions
attribute :type
def validate_non_true_for_some_formats
return unless %w[bool calculated_value].include?(field_format)
@@ -69,9 +69,9 @@ module Projects
end
def validate_assignee_custom_field
if project_assignee_custom_field_not_configured?
add_error :project_creation_wizard_assignee_custom_field_id, :blank
elsif not_allowed_to_read_assignee_custom_field_value?
return if project_assignee_custom_field_not_configured?
if not_allowed_to_read_assignee_custom_field_value?
add_error assignee_custom_field.attribute_name, :unauthorized
elsif missing_assignee_custom_field_value?
add_error assignee_custom_field.attribute_name, :blank
+6 -15
View File
@@ -52,7 +52,6 @@ module Projects
validate_work_package_type
validate_status_when_submitted
validate_assignee_custom_field
validate_work_package_comment
validate_notification_text
end
@@ -78,21 +77,13 @@ module Projects
end
def validate_assignee_custom_field
if model.project_creation_wizard_assignee_custom_field_id.blank?
errors.add :project_creation_wizard_assignee_custom_field_id, :blank
else
valid_custom_field = model.available_custom_fields
.where(field_format: "user", multi_value: false)
.exists?(id: model.project_creation_wizard_assignee_custom_field_id)
unless valid_custom_field
errors.add :project_creation_wizard_assignee_custom_field_id, :inclusion
end
end
end
return if model.project_creation_wizard_assignee_custom_field_id.blank?
def validate_work_package_comment
if model.project_creation_wizard_work_package_comment.blank?
errors.add :project_creation_wizard_work_package_comment, :blank
valid_custom_field = model.available_custom_fields
.where(field_format: "user", multi_value: false)
.exists?(id: model.project_creation_wizard_assignee_custom_field_id)
unless valid_custom_field
errors.add :project_creation_wizard_assignee_custom_field_id, :inclusion
end
end
+2 -1
View File
@@ -38,7 +38,8 @@ module Users
}
attribute :firstname
attribute :lastname
attribute :mail
attribute :mail,
writable: ->(*) { model.new_record? || model.id == user.id || user.admin? }
attribute :admin,
writable: ->(*) { user.admin? && model.id != user.id }
attribute :language
@@ -45,6 +45,10 @@ module WorkPackages
errors.add(:journal_internal, :enterprise_plan_required, plan_name:)
end
unless model.project.enabled_internal_comments
errors.add(:journal_internal, :feature_disabled_for_project)
end
unless allowed_in_project?(:add_internal_comments)
errors.add(:journal_internal, :error_unauthorized)
end
@@ -0,0 +1,77 @@
# 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
class McpConfigurationsController < ::ApplicationController
include OpTurbo::ComponentStream
before_action :require_admin
menu_item :mcp_configurations
layout "admin"
def index
@server_config = McpConfiguration.server_config
@tool_configs = McpConfiguration.where(identifier: McpTools.tools_by_name.keys)
@resource_configs = McpConfiguration.where(identifier: McpResources.resources_by_name.keys)
end
def update
config = McpConfiguration.find(params[:id])
if config.update(mcp_config_params)
flash[:notice] = t(".success")
else
flash[:error] = t(".failure")
end
redirect_to action: :index
end
def multi_update
updates = params[:mcp_configurations]
updates.transform_values! { |hash| hash.permit(:title, :description, :enabled) }
updates.each do |identifier, attributes|
McpConfiguration.find_by!(identifier:).update!(attributes)
end
flash[:notice] = t(".success")
redirect_to action: :index
end
private
def mcp_config_params
params.expect(mcp_configuration: %i[enabled title description])
end
end
end
@@ -34,7 +34,7 @@ module Admin::Settings
def settings_params
super.tap do |settings|
settings["apiv3_cors_origins"] = settings["apiv3_cors_origins"].split(/\r?\n/)
settings["apiv3_cors_origins"] = settings["apiv3_cors_origins"]&.split(/\r?\n/) || []
end
end
@@ -1,6 +1,6 @@
# frozen_string_literal: true
# -- copyright
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
@@ -26,11 +26,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
#++
class Projects::ExportListModalComponent < ApplicationComponent
include OpTurbo::Streamable
MODAL_ID = "op-project-list-export-dialog"
options :query
module Admin::Settings
class ExternalLinksSettingsController < ::Admin::SettingsController
menu_item :settings_external_links
end
end
@@ -45,6 +45,7 @@ module Admin::Settings
private
def validate_mail_from
return unless settings_params.key?(:mail_from)
return if EmailValidator.valid?(settings_params[:mail_from])
flash[:error] = "#{I18n.t(:setting_mail_from)} #{I18n.t('activerecord.errors.messages.email')}"
+8 -1
View File
@@ -29,12 +29,19 @@
#++
class APIDocsController < ApplicationController
before_action :require_login
before_action :require_login,
:check_if_api_docs_enabled
no_authorization_required! :index
helper API::APIDocsHelper
def index
render locals: { turbo_opt_out: true }
end
private
def check_if_api_docs_enabled
render_404 unless Setting.apiv3_docs_enabled?
end
end
@@ -0,0 +1,73 @@
# 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 ExternalLinkWarningController < ApplicationController
layout "only_logo"
skip_before_action :check_if_login_required
no_authorization_required! :show
before_action :parse_external_url, only: [:show]
before_action :verify_capture_enabled, only: [:show]
def show; end
private
def verify_capture_enabled
unless capture_enabled?
redirect_to @external_url, allow_other_host: true, status: :see_other
end
end
def capture_enabled?
Setting.capture_external_links? && EnterpriseToken.allows_to?(:capture_external_links)
end
def parse_external_url
external_url = params[:url]
@external_url = parse_url(CGI.unescape(external_url)) if external_url.present?
if @external_url.nil?
redirect_to home_path, status: :see_other
end
end
def parse_url(url)
return nil if url.blank?
uri = URI.parse(url)
return url if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
nil
rescue URI::InvalidURIError
nil
end
end
+12 -21
View File
@@ -48,9 +48,9 @@ module My
:generate_rss_key,
:revoke_rss_key,
:generate_api_key,
:remove_oauth_client_token,
:revoke_api_key,
:revoke_ical_token,
:revoke_storage_token,
:revoke_ical_meeting_token
def dialog
@@ -58,39 +58,30 @@ module My
end
def index
@ical_meeting_tokens = current_user.ical_meeting_tokens
@storage_tokens = OAuthClientToken
.preload(:oauth_client)
.joins(:oauth_client)
.where(user: @user, oauth_client: { integration_type: "Storages::Storage" })
@oauth_client_tokens = OAuthClientToken.includes(:oauth_client).where(user: @user)
end
def revoke_storage_token
token = OAuthClientToken
.preload(:oauth_client)
.joins(:oauth_client)
.where(user: @user, oauth_client: { integration_type: "Storages::Storage" }).find_by(id: params[:access_token_id])
def remove_oauth_client_token
token = OAuthClientToken.includes(:oauth_client).where(user: @user).find_by(id: params[:access_token_id])
if token&.destroy
flash[:info] = I18n.t("my_account.access_tokens.storages.removed")
flash[:info] = I18n.t("my_account.access_tokens.oauth_client.removed")
else
flash[:error] = I18n.t("my_account.access_tokens.storages.failed")
flash[:error] = I18n.t("my_account.access_tokens.oauth_client.failed")
end
redirect_to action: :index, status: :see_other
redirect_to action: :index, tab: :client, status: :see_other
end
def generate_rss_key # rubocop:disable Metrics/AbcSize
token = Token::RSS.create!(user: current_user)
flash[:info] = [
t("my.access_token.notice_reset_token", type: "RSS").html_safe,
helpers.content_tag(:strong, helpers.content_tag(:code, token.plain_value)),
t("my.access_token.token_value_warning")
]
update_via_turbo_stream(
component: My::AccessToken::APITokensSectionComponent.new(tokens: [token], token_type: Token::RSS)
)
respond_with_dialog(My::AccessToken::AccessTokenCreatedDialogComponent.new(token:))
rescue StandardError => e
Rails.logger.error "Failed to reset user ##{current_user.id} RSS key: #{e}"
flash[:error] = t("my.access_token.failed_to_reset_token", error: e.message)
ensure
redirect_to action: :index, status: :see_other
end
+5
View File
@@ -48,6 +48,7 @@ class MyController < ApplicationController
:update_settings,
:password,
:change_password,
:password_confirmation_dialog,
:notifications,
:reminders
@@ -85,6 +86,10 @@ class MyController < ApplicationController
end
end
def password_confirmation_dialog
respond_with_dialog My::PasswordConfirmationDialog.new
end
# Configure user's in app notifications
def notifications; end
@@ -0,0 +1,86 @@
# 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 OAuthMetadataController < ApplicationController
no_authorization_required! :authorization_server, :protected_resource
skip_before_action :check_if_login_required
def authorization_server
grant_types = Doorkeeper.configuration.grant_flows
grant_types += ["refresh_token"] if Doorkeeper.configuration.refresh_token_enabled?
render json: {
issuer: local_issuer,
authorization_endpoint: oauth_authorization_url,
token_endpoint: oauth_token_url,
introspection_endpoint: oauth_introspect_url,
scopes_supported: Doorkeeper.configuration.scopes.to_a,
response_types_supported: response_types(Doorkeeper.configuration.grant_flows),
grant_types_supported: grant_types,
service_documentation: OpenProject::Static::Links.url_for(:oauth_applications)
}
end
def protected_resource
render json: {
resource: resource_url,
resource_name: Setting.app_title,
authorization_servers:,
scopes_supported: OpenProject::Authentication::Scope.values,
bearer_methods_supported: ["header"],
resource_documentation: OpenProject::Static::Links.url_for(:api_docs)
}
end
private
def response_types(grant_types)
grant_types.filter_map do |grant|
case grant
when "authorization_code"
"code"
when "implicit"
"token"
end
end
end
def authorization_servers
OpenIDConnect::Provider.where(available: true).map(&:issuer) + [local_issuer]
end
def instance_base_url
"http#{'s' if request.ssl?}://#{Setting.host_name}"
end
alias resource_url instance_base_url
alias local_issuer instance_base_url
end
@@ -33,7 +33,8 @@ class Projects::Settings::CreationWizardController < Projects::SettingsControlle
menu_item :settings_creation_wizard
before_action :check_feature_flag
before_action :check_enterprise_plan, only: :toggle
before_action :check_activation_conditions, only: :toggle
def show; end
@@ -71,11 +72,10 @@ class Projects::Settings::CreationWizardController < Projects::SettingsControlle
end
def toggle_project_custom_field
mapping = ProjectCustomFieldProjectMapping.find_by(
project_id: permitted_params.project_custom_field_project_mapping[:project_id],
custom_field_id: permitted_params.project_custom_field_project_mapping[:custom_field_id]
)
if mapping&.update(creation_wizard: !mapping.creation_wizard)
cf = ProjectCustomField.find(permitted_params.project_custom_field_project_mapping[:custom_field_id])
mapping = cf.project_custom_field_project_mappings.find_by(project: @project)
if custom_field_toggleable?(cf) && toggle_mapping(mapping)
render json: {}, status: :ok
else
render json: {}, status: :unprocessable_entity
@@ -92,22 +92,65 @@ class Projects::Settings::CreationWizardController < Projects::SettingsControlle
private
def check_enterprise_plan
# Allow disabling even without enterprise plan
return if @project.project_creation_wizard_enabled
unless EnterpriseToken.allows_to?(:project_creation_wizard)
flash[:error] = I18n.t(:notice_requires_enterprise_token)
redirect_to project_settings_creation_wizard_path(@project, tab: "attributes"), status: :see_other
end
end
def check_activation_conditions
# Allow disabling even without activation conditions met
return if @project.project_creation_wizard_enabled
error = if @project.project_creation_wizard_default_work_package_type.nil?
I18n.t("projects.settings.creation_wizard.errors.no_work_package_type")
elsif @project.project_creation_wizard_default_status_when_submitted.nil?
type = @project.project_creation_wizard_default_work_package_type.name
I18n.t("projects.settings.creation_wizard.errors.no_status_when_submitted", type:)
end
if error
flash[:error] = error
redirect_to project_settings_creation_wizard_path(@project, tab: params[:tab]), status: :see_other
end
end
def update_section_mappings(value)
section_id = permitted_params.project_custom_field_project_mapping[:custom_field_section_id]
project_id = permitted_params.project_custom_field_project_mapping[:project_id]
custom_field_ids = ProjectCustomField
.where(custom_field_section_id: section_id)
.where(is_required: false)
.pluck(:id)
cf_ids_to_toggle, force_enabled_cf_ids = ProjectCustomField.toggleable_ids_in_creation_wizard_settings(@project, section_id)
ProjectCustomFieldProjectMapping
.where(project_id:, custom_field_id: custom_field_ids)
.where(project_id: @project.id, custom_field_id: cf_ids_to_toggle)
.update_all(creation_wizard: value)
enable_creation_wizard!(force_enabled_cf_ids)
redirect_to project_settings_creation_wizard_path(@project, tab: "attributes"), status: :see_other
end
def enable_creation_wizard!(custom_field_ids)
ProjectCustomFieldProjectMapping
.where(project_id: @project.id, custom_field_id: custom_field_ids)
.update_all(creation_wizard: true)
end
def custom_field_toggleable?(custom_field)
toggleable_ids = ProjectCustomField
.toggleable_ids_in_creation_wizard_settings(@project, custom_field.custom_field_section_id)
.first
toggleable_ids.include?(custom_field.id)
end
def toggle_mapping(mapping)
mapping&.update(creation_wizard: !mapping.creation_wizard)
end
def check_feature_flag
unless OpenProject::FeatureDecisions.project_initiation_active?
render_404
+3 -7
View File
@@ -34,10 +34,10 @@ class ProjectsController < ApplicationController
menu_item :overview
menu_item :roadmap, only: :roadmap
before_action :find_project, except: %i[index new create export_list_modal]
before_action :load_query_or_deny_access, only: %i[index export_list_modal]
before_action :find_project, except: %i[index new create]
before_action :load_query_or_deny_access, only: %i[index]
before_action :authorize,
only: %i[copy_form copy deactivate_work_package_attachments export_list_modal export_project_initiation_pdf]
only: %i[copy_form copy deactivate_work_package_attachments export_project_initiation_pdf]
before_action :authorize_global, only: %i[new create]
before_action :require_admin, only: %i[destroy destroy_info]
before_action :find_optional_parent, only: :new
@@ -172,10 +172,6 @@ class ProjectsController < ApplicationController
end
end
def export_list_modal
respond_with_dialog Projects::ExportListModalComponent.new(query: @query)
end
def export_project_initiation_pdf
export = Project::PDFExport::ProjectInitiation.new(@project).export!
send_data(export.content, type: export.mime_type, filename: export.title)
@@ -39,29 +39,19 @@ class WorkPackages::ActivitiesTabController < ApplicationController
before_action :find_journal, only: %i[emoji_actions item_actions edit cancel_edit update toggle_reaction]
before_action :set_filter
before_action :authorize
before_action :initialize_pagination, only: %i[page_streams]
before_action :initialize_pagination, only: %i[index page_streams]
def index
index_component =
if OpenProject::FeatureDecisions.wp_activity_tab_lazy_pagination_active?
initialize_pagination
WorkPackages::ActivitiesTab::LazyIndexComponent.new(
work_package: @work_package,
journals: @paginated_journals,
paginator: @paginator,
filter: @filter,
last_server_timestamp: get_current_server_timestamp
)
else
WorkPackages::ActivitiesTab::IndexComponent.new(
work_package: @work_package,
filter: @filter,
last_server_timestamp: get_current_server_timestamp,
deferred: ActiveRecord::Type::Boolean.new.cast(params[:deferred])
)
end
render(index_component, layout: false)
render(
WorkPackages::ActivitiesTab::LazyIndexComponent.new(
work_package: @work_package,
journals: @paginated_journals,
paginator: @paginator,
filter: @filter,
last_server_timestamp: get_current_server_timestamp
),
layout: false
)
end
def page_streams
@@ -323,37 +313,28 @@ class WorkPackages::ActivitiesTabController < ApplicationController
end
def replace_whole_tab
component =
if OpenProject::FeatureDecisions.wp_activity_tab_lazy_pagination_active?
initialize_pagination # re-initialize pagination to pick up changes to sorting/filtering
WorkPackages::ActivitiesTab::LazyIndexComponent.new(
work_package: @work_package,
journals: @paginated_journals,
paginator: @paginator,
filter: @filter,
last_server_timestamp: get_current_server_timestamp
)
else
WorkPackages::ActivitiesTab::IndexComponent.new(
work_package: @work_package,
filter: @filter,
last_server_timestamp: get_current_server_timestamp
)
end
replace_via_turbo_stream(component:)
initialize_pagination # re-initialize pagination to pick up changes to sorting/filtering
replace_via_turbo_stream(
component: WorkPackages::ActivitiesTab::LazyIndexComponent.new(
work_package: @work_package,
journals: @paginated_journals,
paginator: @paginator,
filter: @filter,
last_server_timestamp: get_current_server_timestamp
)
)
end
def update_index_component
component =
if OpenProject::FeatureDecisions.wp_activity_tab_lazy_pagination_active?
initialize_pagination # re-initialize pagination to pick up changes to sorting/filtering
WorkPackages::ActivitiesTab::Journals::LazyIndexComponent
.new(work_package: @work_package, journals: @paginated_journals, paginator: @paginator, filter: @filter)
else
WorkPackages::ActivitiesTab::Journals::IndexComponent
.new(work_package: @work_package, filter: @filter)
end
update_via_turbo_stream(component:)
initialize_pagination # re-initialize pagination to pick up changes to sorting/filtering
update_via_turbo_stream(
component: WorkPackages::ActivitiesTab::Journals::LazyIndexComponent.new(
work_package: @work_package,
journals: @paginated_journals,
paginator: @paginator,
filter: @filter
)
)
end
def create_journal_service_call
@@ -421,56 +402,27 @@ class WorkPackages::ActivitiesTabController < ApplicationController
end
def rerender_journals_with_updated_notification(journals, last_update_timestamp, grouped_emoji_reactions, editing_journals)
# Case: the user marked the journal as read somewhere else and expects the bubble to disappear
#
# below code stopped working with the introduction of the sequence_version query
# I believe it is due to the fact that the notification join does not work well with the sequence_version query
# see below comments from my debugging session
# journals
# .joins(:notifications)
# .where("notifications.updated_at > ?", last_update_timestamp)
# .find_each do |journal|
# # DEBUGGING:
# # the journal id is actually 85 but below data is logged:
# # # journal id 14 (?!)
# # # journal sequence_version 22 (correct!)
# # the update stream has a wrong target then!
# # target="work-packages-activities-tab-journals-item-component-14"
# # instead of
# # target="work-packages-activities-tab-journals-item-component-85"
# update_item_show_component(journal:, grouped_emoji_reactions: grouped_emoji_reactions.fetch(journal.id, {}))
# end
#
# alternative approach in order to bypass the notification join issue in relation with the sequence_version query
Notification
.where(journal_id: journals.pluck(:id))
.where(recipient_id: User.current.id)
.where("notifications.updated_at > ?", last_update_timestamp)
.find_each do |notification|
next if editing_journals.include?(notification.journal_id)
next if editing_journals.include?(notification.journal_id)
update_item_show_component(
journal: journals.find(notification.journal_id), # take the journal from the journals querried with sequence_version!
grouped_emoji_reactions: grouped_emoji_reactions.fetch(notification.journal_id, {})
)
end
update_item_show_component(
journal: journals.find(notification.journal_id), # take the journal from the journals querried with sequence_version!
grouped_emoji_reactions: grouped_emoji_reactions.fetch(notification.journal_id, {})
)
end
end
def insert_latest_journals_via_turbo_stream(journals, last_update_timestamp, emoji_reactions)
target_component =
if OpenProject::FeatureDecisions.wp_activity_tab_lazy_pagination_active?
WorkPackages::ActivitiesTab::Journals::LazyIndexComponent.new(
work_package: @work_package,
journals: Journal.none, # we do not need to pass any journals here since we just want the component key
paginator: nil,
filter: @filter
)
else
WorkPackages::ActivitiesTab::Journals::IndexComponent.new(
work_package: @work_package,
filter: @filter
)
end
target_component = WorkPackages::ActivitiesTab::Journals::LazyIndexComponent.new(
work_package: @work_package,
journals: Journal.none, # we do not need to pass any journals here since we just want the component key
paginator: nil,
filter: @filter
)
journals.where("created_at > ?", last_update_timestamp).find_each do |journal|
insert_via_turbo_stream(
@@ -58,6 +58,18 @@ class WorkPackages::BulkController < ApplicationController
end
end
def reassign
respond_to do |format|
format.html do
render locals: { work_packages: @work_packages,
associated: WorkPackage.associated_classes_to_address_before_destruction_of(@work_packages) }
end
format.json do
render json: { error_message: "Clean up of associated objects required" }, status: 420
end
end
end
def destroy
if WorkPackage.cleanup_associated_before_destructing_if_required(@work_packages, current_user, params[:to_do])
destroy_work_packages(@work_packages)
@@ -71,15 +83,7 @@ class WorkPackages::BulkController < ApplicationController
end
end
else
respond_to do |format|
format.html do
render locals: { work_packages: @work_packages,
associated: WorkPackage.associated_classes_to_address_before_destruction_of(@work_packages) }
end
format.json do
render json: { error_message: "Clean up of associated objects required" }, status: 420
end
end
redirect_to(action: :reassign, ids: @work_packages.map(&:id))
end
end
@@ -0,0 +1,95 @@
# 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 APISettingsForm < ApplicationForm
delegate :static_link_to, to: :@view_context
class CORSForm < ApplicationForm
settings_form do |sf|
sf.text_area(
name: :apiv3_cors_origins,
rows: 5,
disabled: !Setting.apiv3_cors_enabled?,
data: {
disable_when_checked_target: "effect",
target_name: "apiv3_cors_enabled"
}
)
end
end
settings_form do |sf|
sf.check_box(name: :rest_api_enabled)
sf.text_field(
name: :apiv3_max_page_size,
type: :number,
input_width: :xsmall,
min: 50
)
sf.check_box(name: :apiv3_write_readonly_attributes)
sf.fieldset_group(title: I18n.t("setting_apiv3_docs"), mt: 4) do |fg|
fg.check_box(
name: :apiv3_docs_enabled,
caption: I18n.t(:setting_apiv3_docs_enabled_instructions_warning)
)
end
sf.fieldset_group(title: I18n.t("setting_apiv3_cors_title")) do |fg|
fg.check_box(
name: :apiv3_cors_enabled,
data: {
target_name: "apiv3_cors_enabled",
disable_when_checked_target: "cause",
show_when_checked_target: "cause"
}
) do |apiv3_cors_check_box|
apiv3_cors_check_box.nested_form(
classes: ["mt-2", { "d-none" => !Setting.apiv3_cors_enabled? }],
data: {
target_name: "apiv3_cors_enabled",
show_when_checked_target: "effect",
show_when: "checked"
}
) do |builder|
CORSForm.new(builder)
end
end
end
sf.submit
end
end
end
end
@@ -0,0 +1,9 @@
<%= render(Primer::Beta::Text.new(tag: :p)) do %>
<%= I18n.t(:setting_apiv3_max_page_size_instructions) %>
<% end %>
<%= render(Primer::OpenProject::InlineMessage.new(scheme: :warning, size: :small)) do %>
<%= render(Primer::Beta::Text.new(tag: :p)) do %>
<%= render(Primer::Beta::Text.new(tag: :strong).with_content("#{I18n.t(:warning)}:")) %>
<%= I18n.t(:setting_apiv3_max_page_size_warning) %>
<% end %>
<% end %>
@@ -0,0 +1,17 @@
<%= render(Primer::Beta::Text.new(tag: :p)) do %>
<%= I18n.t(:setting_apiv3_write_readonly_attributes_instructions) %>
<% end %>
<%= render(Primer::OpenProject::InlineMessage.new(scheme: :warning, size: :small)) do %>
<%= render(Primer::Beta::Text.new(tag: :p)) do %>
<%= render(Primer::Beta::Text.new(tag: :strong).with_content("#{I18n.t(:warning)}:")) %>
<%= I18n.t(:setting_apiv3_write_readonly_attributes_warning) %>
<% end %>
<% end %>
<%= render(Primer::Beta::Text.new(tag: :p, mb: 0)) do %>
<%=
I18n.t(
:setting_apiv3_write_readonly_attributes_additional,
api_documentation_link: static_link_to(:api_docs)
).html_safe
%>
<% end %>
@@ -0,0 +1,10 @@
<%= render(Primer::Beta::Text.new(tag: :p)) do %>
<%= I18n.t(:text_line_separated) %>
<% end %>
<%= render(Primer::Beta::Text.new(tag: :p)) do %>
<%= I18n.t(
:setting_apiv3_cors_origins_text_html,
origin_link: ::OpenProject::Static::Links.url_for(:origin_mdn_documentation)
).html_safe %>
<% end %>

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