mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Release OpenProject 17.1.0
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -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,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"
|
||||
|
||||
+4
-1
@@ -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
|
||||
+9
-23
@@ -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 %>
|
||||
+22
-22
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+43
-10
@@ -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
|
||||
|
||||
+12
-3
@@ -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
|
||||
|
||||
+45
-14
@@ -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
|
||||
|
||||
+24
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+6
-7
@@ -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')}"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
+17
@@ -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
Reference in New Issue
Block a user