Merge remote-tracking branch 'origin/dev' into rails-7.2

This commit is contained in:
ulferts
2025-02-27 17:48:20 +01:00
5049 changed files with 125904 additions and 33760 deletions
-4
View File
@@ -350,10 +350,7 @@ ij_json_spaces_within_brackets = false
ij_json_wrap_long_lines = false
[{rcov,spec,rake,rails,spork,capfile,gemfile,rakefile,guardfile,isolate,vagrantfile,Puppetfile,*.jbuilder,*.rbw,*.gemspec,*.thor,*.ru,*.rb,*.rake}]
indent_size = 2
tab_width = 2
trim_trailing_whitespace=true
ij_continuation_indent_size = 2
ij_ruby_align_group_field_declarations = false
ij_ruby_align_multiline_parameters = true
ij_ruby_blank_lines_around_method = 1
@@ -364,7 +361,6 @@ ij_ruby_indent_protected_methods = false
ij_ruby_indent_public_methods = false
ij_ruby_indent_when_cases = false
ij_ruby_keep_blank_lines_in_declarations = 2
ij_ruby_keep_indents_on_empty_lines = false
ij_ruby_keep_line_breaks = true
ij_ruby_parentheses_around_method_arguments = true
ij_ruby_spaces_around_hashrocket = true
+13 -5
View File
@@ -15,6 +15,12 @@ linters:
rubocop_config:
inherit_from:
- .rubocop.yml
Layout/CommentIndentation:
Enabled: false
Layout/FirstArgumentIndentation:
EnforcedStyle: consistent
Layout/FirstMethodArgumentLineBreak:
Enabled: true
Layout/InitialIndentation:
Enabled: false
Layout/LeadingEmptyLines:
@@ -25,11 +31,13 @@ linters:
Enabled: false
Layout/TrailingWhitespace:
Enabled: false
Naming/FileName:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Lint/UselessAssignment:
Enabled: false
Rails/OutputSafety:
Naming/FileName:
Enabled: false
Rails/OutputSafety:
Enabled: true
Style/FrozenStringLiteralComment:
Enabled: false
Style/RedundantConstantBase:
Enabled: false
+4
View File
@@ -21,6 +21,10 @@ updates:
target-branch: "dev"
open-pull-requests-limit: 3
versioning-strategy: lockfile-only
groups:
aws-gems:
patterns:
- "aws-*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
+40 -3
View File
@@ -53,9 +53,13 @@ jobs:
VERSION=${TAG_REF#v}
echo "Version: $VERSION"
echo "Checkout REF: $CHECKOUT_REF"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "checkout_ref=$CHECKOUT_REF" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.extract_version.outputs.checkout_ref }}
- name: Cache NPM
uses: runs-on/cache@v4
with:
@@ -136,9 +140,9 @@ jobs:
runner: runner=4cpu-linux-x64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.setup.outputs.checkout_ref }}
uses: actions/checkout@v4
- name: Prepare docker files
run: |
cp ./docker/prod/Dockerfile ./Dockerfile
@@ -186,6 +190,16 @@ jobs:
type=semver,pattern={{version}},value=${{ needs.setup.outputs.version }}
images: |
${{ env.REGISTRY_IMAGE }}
- name: Restore vendor/bundle
id: restore-vendor-bundle
uses: actions/cache/restore@v4
with:
path: |
vendor/bundle
key: ${{ matrix.platform }}-vendor-bundle-${{ github.ref }}
- name: Include vendor/bundle in this build (so we can use it from the cache above)
run: |
sed -i 's/vendor\/bundle//g' .dockerignore
- name: Build image
id: build
uses: docker/build-push-action@v6
@@ -195,12 +209,35 @@ jobs:
target: ${{ matrix.target }}
build-args: |
BIM_SUPPORT=${{ matrix.bim_support }}
BUILDKIT_PROGRESS=plain
pull: true
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }}
cache-to: type=s3,blobs_prefix=cache/${{ github.repository }}/,manifests_prefix=cache/${{ github.repository }}/,region=${{ env.RUNS_ON_AWS_REGION }},bucket=${{ env.RUNS_ON_S3_BUCKET_CACHE }},mode=max
- name: Extract vendor/bundle from container
run: |
docker create --name bundle ${{ steps.build.outputs.imageid }}
if [ -d vendor/bundle ]; then
mv vendor/bundle vendor/bundle.bak
fi
docker cp bundle:/app/vendor/bundle vendor/bundle
docker rm bundle
- name: Save vendor/bundle
id: save-vendor-bundle
uses: actions/cache/save@v4
with:
path: |
vendor/bundle
key: ${{ steps.restore-vendor-bundle.outputs.cache-primary-key }}
- name: Restore pre-build vendor/bundle to prevent 2nd build from scratch during push
run: |
rm -rf vendor/bundle
if [ -d vendor/bundle.bak ]; then
mv vendor/bundle.bak vendor/bundle
fi
- name: Test
# We only test the native container. If that fails the builds for the others
# will be cancelled as well.
@@ -315,7 +352,7 @@ jobs:
needs: [setup, build, merge]
permissions:
contents: none
if: ${{ github.repository == 'opf/openproject' && inputs.tag != '' }}
if: ${{ github.repository == 'opf/openproject' }}
runs-on: ubuntu-latest
steps:
- name: Trigger Helm charts release
+1
View File
@@ -7,6 +7,7 @@ on:
- release/*
paths:
- 'docs/**'
- 'config/static_links.yml'
permissions:
contents: read
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send mail
uses: dawidd6/action-send-mail@v3
uses: dawidd6/action-send-mail@v4
with:
subject: ${{ inputs.subject }}
body: ${{ inputs.body }}
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '18.13'
node-version: '20.9'
cache: npm
cache-dependency-path: frontend/package-lock.json
- uses: reviewdog/action-eslint@v1
-1
View File
@@ -33,7 +33,6 @@ jobs:
echo "OPENPROJECT_FEATURE__SHOW__CHANGES__ACTIVE=true" >> .env.pullpreview
echo "OPENPROJECT_LOOKBOOK__ENABLED=true" >> .env.pullpreview
echo "OPENPROJECT_HSTS=false" >> .env.pullpreview
echo "OPENPROJECT_FEATURE_PRIMERIZED_WORK_PACKAGE_ACTIVITIES_ACTIVE=true" >> .env.pullpreview
echo "OPENPROJECT_NOTIFICATIONS_POLLING_INTERVAL=10000" >> .env.pullpreview
- name: Boot as BIM edition
if: contains(github.ref, 'bim/') || contains(github.head_ref, 'bim/')
+14 -3
View File
@@ -10,9 +10,12 @@ jobs:
name: rubocop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
- uses: reviewdog/action-rubocop@v2
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
- name: Run Rubocop
uses: reviewdog/action-rubocop@v2
with:
github_token: ${{ secrets.github_token }}
rubocop_version: gemfile
@@ -26,3 +29,11 @@ jobs:
rubocop-rspec_rails:gemfile
reporter: github-pr-check
only_changed: true
- name: Install erb_lint
run: gem install -N erb_lint erblint-github
- name: Run erb-lint
uses: tk0miya/action-erblint@v1
with:
github_token: ${{ secrets.github_token }}
reporter: github-pr-check
fail_on_error: true
+54
View File
@@ -0,0 +1,54 @@
name: "Frontend test suite"
on:
workflow_dispatch:
push:
branches:
- dev
- release/*
paths:
- '**/frontend/**/*.ts'
- '**/frontend/**/*.js'
- '**/frontend/**/*.json'
pull_request:
types: [opened, reopened, synchronize]
paths:
- '**/frontend/**/*.ts'
- '**/frontend/**/*.js'
- '**/frontend/**/*.json'
permissions:
contents: read
defaults:
run:
working-directory: ./frontend
jobs:
units:
name: Units
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20.9'
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install Dependencies
id: npm-i
run: npm i
- name: Register plugins
id: npm-run-ci-plugins-register-frontend
run: npm run ci:plugins:register_frontend
- name: Test (Angular)
id: npm-test
run: npm test -- --code-coverage
+2 -2
View File
@@ -2,7 +2,7 @@ name: Check work package version
on:
pull_request:
types: [labeled, synchronize]
types: [labeled, synchronize, ready_for_review]
permissions:
contents: read # to fetch code (actions/checkout)
@@ -10,7 +10,7 @@ permissions:
jobs:
version-check:
if: contains(github.event.pull_request.labels.*.name, 'needs review')
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
+2
View File
@@ -134,6 +134,8 @@ structure.sql
lefthook-local.yml
.rubocop-local.yml
/.lefthook-local/
frontend/package-lock.json
# Testing and nextcloud infrastructure
+1 -6
View File
@@ -21,7 +21,7 @@ targets:
<<: *debian
ubuntu-22.04:
<<: *debian
centos-8: &centos8
centos-9:
env:
- NODE_ENV=production
- NPM_CONFIG_PRODUCTION=false
@@ -30,11 +30,6 @@ targets:
- ImageMagick
- unzip
- poppler-utils
centos-9:
<<: *centos8
env:
- NODE_ENV=production
- NPM_CONFIG_PRODUCTION=false
sles-15:
build_dependencies:
- sqlite3-devel
+22 -11
View File
@@ -1,17 +1,18 @@
require:
- rubocop-openproject
- rubocop-rails
- rubocop-rspec
- rubocop-rspec_rails
- rubocop-capybara
- rubocop-factory_bot
- rubocop-performance
- ./config/initializers/inflections.rb
<% if File.exist?('.rubocop-local.yml') %>
plugins:
- rubocop-rails
- rubocop-rspec
- rubocop-performance
# A rubocop-local.yml file can be added to customized the styles
inherit_from:
- .rubocop-local.yml
<% end %>
- .rubocop-local*.yml
inherit_mode:
merge:
@@ -19,12 +20,18 @@ inherit_mode:
- Exclude
AllCops:
TargetRubyVersion: 3.3
TargetRubyVersion: 3.4
# Enable any new cops in new versions by default
NewCops: enable
Exclude:
- '**/node_modules/**/*'
# Disable it as it is deprecated
# From https://docs.rubocop.org/rubocop-capybara/cops_capybara.html#capybaraclicklinkorbuttonstyle
# "This cop is deprecated. We plan to remove this in the next major version update to 3.0."
Capybara/ClickLinkOrButtonStyle:
Enabled: false
FactoryBot/ConsistentParenthesesStyle:
Enabled: false
@@ -326,9 +333,6 @@ Style/ColonMethodCall:
Style/CommentAnnotation:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/Documentation:
Enabled: false
@@ -341,6 +345,9 @@ Style/EachWithObject:
Style/EmptyLiteral:
Enabled: false
Style/EndlessMethod:
Enabled: true
Style/EvenOdd:
Enabled: false
@@ -401,6 +408,9 @@ Style/PercentLiteralDelimiters:
Style/PerlBackrefs:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/Proc:
Enabled: false
@@ -453,7 +463,8 @@ Style/WordArray:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Enabled: true
EnforcedStyle: always_true
Style/NumericLiterals:
Enabled: false
+1 -1
View File
@@ -1 +1 @@
3.3.6
3.4.2
+38 -32
View File
@@ -36,7 +36,7 @@ ruby File.read(File.expand_path(".ruby-version", __dir__)).strip
gem "actionpack-xml_parser", "~> 2.0.0"
gem "activemodel-serializers-xml", "~> 1.0.1"
gem "activerecord-import", "~> 1.7.0"
gem "activerecord-import", "~> 2.1.0"
gem "activerecord-session_store", "~> 2.1.0"
gem "ox"
gem "rails", "~> 7.2.2"
@@ -46,7 +46,7 @@ gem "ffi", "~> 1.15"
gem "rdoc", ">= 2.4.2"
gem "doorkeeper", "~> 5.7.0"
gem "doorkeeper", "~> 5.8.0"
# Maintain our own omniauth due to relative URL root issues
# see upstream PR: https://github.com/omniauth/omniauth/pull/903
gem "omniauth", git: "https://github.com/opf/omniauth", ref: "fe862f986b2e846e291784d2caa3d90a658c67f0"
@@ -74,7 +74,7 @@ gem "addressable", "~> 2.8.0"
gem "auto_strip_attributes", "~> 2.5"
# Provide timezone info for TZInfo used by AR
gem "tzinfo-data", "~> 1.2024.1"
gem "tzinfo-data", "~> 1.2025.1"
# to generate html-diffs (e.g. for wiki comparison)
gem "htmldiff"
@@ -83,7 +83,7 @@ gem "htmldiff"
gem "stringex", "~> 2.8.5"
# CommonMark markdown parser with GFM extension
gem "commonmarker", "~> 1.1.3"
gem "commonmarker", "~> 2.0.2"
# HTML pipeline for transformations on text formatter output
# such as sanitization or additional features
@@ -93,9 +93,9 @@ 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.4.0"
gem "rouge", "~> 4.5.1"
# HTML sanitization used for html-pipeline
gem "sanitize", "~> 6.1.0"
gem "sanitize", "~> 7.0.0"
# HTML autolinking for mails and urls (replaces autolink)
gem "rinku", "~> 2.0.4", require: %w[rinku rails_rinku]
# Version parsing with semver
@@ -107,7 +107,7 @@ gem "svg-graph", "~> 2.2.0"
gem "date_validator", "~> 0.12.0"
gem "email_validator", "~> 2.2.3"
gem "json_schemer", "~> 2.3.0"
gem "json_schemer", "~> 2.4.0"
gem "ruby-duration", "~> 3.2.0"
# `config/initializers/mail_starttls_patch.rb` has also been patched to
@@ -126,7 +126,7 @@ gem "multi_json", "~> 1.15.0"
gem "oj", "~> 3.16.0"
gem "daemons"
gem "good_job", "= 3.26.2" # update should be done manually in sync with saas-openproject version.
gem "good_job", "= 3.99.1" # update should be done manually in sync with saas-openproject version.
gem "rack-protection", "~> 3.2.0"
@@ -137,10 +137,10 @@ gem "rack-protection", "~> 3.2.0"
gem "rack-attack", "~> 6.7.0"
# CSP headers
gem "secure_headers", "~> 7.0.0"
gem "secure_headers", "~> 7.1.0"
# Browser detection for incompatibility checks
gem "browser", "~> 6.0.0"
gem "browser", "~> 6.2.0"
# Providing health checks
gem "okcomputer", "~> 1.18.1"
@@ -158,7 +158,7 @@ gem "structured_warnings", "~> 0.4.0"
gem "airbrake", "~> 13.0.0", require: false
gem "markly", "~> 0.10" # 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: "fe05b4f8bae8fd46f4fa93b8e0adee6295ef7388"
gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "66893683b94a5fe8f1dc9a60600773307209cdd2"
gem "prawn", "~> 2.4"
gem "ttfunk", "~> 1.7.0" # remove after https://github.com/prawnpdf/prawn/issues/1346 resolved.
@@ -167,10 +167,13 @@ gem "matrix", "~> 0.4.2"
gem "meta-tags", "~> 2.22.0"
gem "paper_trail", "~> 15.2.0"
gem "paper_trail", "~> 16.0.0"
gem "op-clamav-client", "~> 3.4", require: "clamav"
# Recurring meeting events definition
gem "ice_cube", "~> 0.17.0"
group :production do
# we use dalli as standard memcache client
# requires memcached 1.4+
@@ -184,14 +187,11 @@ gem "rails-i18n", "~> 7.0.0"
gem "sprockets", "~> 3.7.2" # lock sprockets below 4.0
gem "sprockets-rails", "~> 3.5.1"
# waiting for a release of puma to fix an issue with current rackup update
# see https://github.com/puma/puma/pull/3532
# gem "puma", "~> 6.4"
gem "puma", github: "puma/puma", branch: "master"
gem "puma", "~> 6.5"
gem "puma-plugin-statsd", "~> 2.0"
gem "rack-timeout", "~> 0.7.0", require: "rack/timeout/base"
gem "nokogiri", "~> 1.16.0"
gem "nokogiri", "~> 1.18.1"
gem "carrierwave", "~> 1.3.4"
gem "carrierwave_direct", "~> 2.1.0"
@@ -207,7 +207,7 @@ gem "plaintext", "~> 0.3.2"
gem "ruby-progressbar", "~> 1.13.0", require: false
gem "mini_magick", "~> 5.0.1", require: false
gem "mini_magick", "~> 5.1.2", require: false
gem "validate_url"
@@ -218,7 +218,7 @@ gem "dry-monads"
gem "dry-validation"
# ActiveRecord extension which adds typecasting to store accessors
gem "store_attribute", "~> 1.0"
gem "store_attribute", "~> 2.0"
# Appsignal integration
gem "appsignal", "~> 3.10.0", require: false
@@ -232,14 +232,19 @@ gem "factory_bot", "~> 6.5.0", require: false
# require factory_bot_rails for convenience in core development
gem "factory_bot_rails", "~> 6.4.4", require: false
gem "turbo_power", "~> 0.6.2"
gem "turbo_power", "~> 0.7.0"
gem "turbo-rails", "~> 2.0.0"
gem "httpx"
# There is a problem with version 1.4.0. Do not update until you're sure there is no infinite hang
# happenning in failing tests when WebMock or VCR stub cannot be found.
gem "httpx", "~> 1.3.4"
# Brings actual deep freezing to most ruby objects
gem "ice_nine"
group :test do
gem "launchy", "~> 3.0.0"
gem "rack-test", "~> 2.1.0"
gem "launchy", "~> 3.1.0"
gem "rack-test", "~> 2.2.0"
gem "shoulda-context", "~> 2.0"
# Test prof provides factories from code
@@ -250,7 +255,7 @@ group :test do
gem "rack_session_access"
gem "rspec", "~> 3.13.0"
# also add to development group, so 'spec' rake task gets loaded
gem "rspec-rails", "~> 7.0.0", group: :development
gem "rspec-rails", "~> 7.1.0", group: :development
# Retry failures within the same environment
gem "retriable", "~> 3.1.1"
@@ -272,9 +277,10 @@ group :test do
gem "rails-controller-testing", "~> 1.0.2"
gem "capybara", "~> 3.40.0"
gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", branch: "main"
gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", tag: "v0.12.0"
gem "capybara-screenshot", "~> 1.0.17"
gem "cuprite", "~> 0.15.0"
gem "ferrum", github: "toy/ferrum", ref: "mouse-events-buttons-property-0.15"
gem "rspec-wait"
gem "selenium-devtools"
gem "selenium-webdriver", "~> 4.20"
@@ -330,11 +336,11 @@ group :development, :test do
# Output a stack trace anytime, useful when a process is stuck
gem "rbtrace"
# REPL with debug commands
gem "debug"
# REPL with debug commands, Debug changed to byebug due to the issue below
# https://github.com/puma/puma/issues/2835#issuecomment-2302133927
gem "byebug"
gem "pry-byebug", "~> 3.10.0", platforms: [:mri]
gem "pry-doc"
gem "pry-rails", "~> 0.3.6"
gem "pry-rescue", "~> 1.6.0"
@@ -353,7 +359,7 @@ group :development, :test do
gem "erblint-github", require: false
# Brakeman scanner
gem "brakeman", "~> 6.2.0"
gem "brakeman", "~> 7.0.0"
# i18n-tasks helps find and manage missing and unused translations.
gem "i18n-tasks", "~> 1.0.13", require: false
@@ -401,6 +407,6 @@ gemfiles.each do |file|
send(:eval_gemfile, file) if File.readable?(file)
end
gem "openproject-octicons", "~>19.19.0"
gem "openproject-octicons_helper", "~>19.19.0"
gem "openproject-primer_view_components", "~>0.49.1"
gem "openproject-octicons", "~>19.20.0 "
gem "openproject-octicons_helper", "~>19.20.0 "
gem "openproject-primer_view_components", "~>0.56.0"
+796 -283
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -14,7 +14,7 @@ gem 'omniauth-openid_connect-providers',
gem 'omniauth-openid-connect',
git: 'https://github.com/opf/omniauth-openid-connect.git',
ref: 'd63f5967514d10db9ddece798dadfa2ac532cbe0'
ref: '3d5fec65072fb4566fb975a9cbe401d758d22317'
group :opf_plugins do
# included so that engines can reference OpenProject::Version
@@ -23,11 +23,9 @@ group :opf_plugins do
gem 'openproject-auth_plugins', path: 'modules/auth_plugins'
gem 'openproject-auth_saml', path: 'modules/auth_saml'
gem 'openproject-openid_connect', path: 'modules/openid_connect'
gem 'openproject-documents', path: 'modules/documents'
gem 'openproject-xls_export', path: 'modules/xls_export'
gem 'costs', path: 'modules/costs'
gem 'openproject-reporting', path: 'modules/reporting'
gem 'openproject-meeting', path: 'modules/meeting'
gem "openproject-backlogs", path: 'modules/backlogs'
gem 'openproject-avatars', path: 'modules/avatars'
gem 'openproject-two_factor_authentication', path: 'modules/two_factor_authentication'
@@ -44,10 +42,12 @@ group :opf_plugins do
gem 'openproject-boards', path: 'modules/boards'
gem 'overviews', path: 'modules/overviews'
gem 'budgets', path: 'modules/budgets'
gem 'openproject-meeting', path: 'modules/meeting'
gem 'openproject-team_planner', path: 'modules/team_planner'
gem 'openproject-gantt', path: 'modules/gantt'
gem 'openproject-calendar', path: 'modules/calendar'
gem 'openproject-storages', path: 'modules/storages'
gem 'openproject-documents', path: 'modules/documents'
gem 'openproject-bim', path: 'modules/bim'
end
+2 -2
View File
@@ -26,7 +26,7 @@ More information and screenshots can be found on our [website](https://www.openp
## Start now with OpenProject
- **Free Trial**:[Start a 14-days free trial of OpenProject](https://start.openproject.com/).
- **Free Trial**: [Start a 14-days free trial of OpenProject](https://start.openproject.com/).
- **Community Edition**, free of charge: Download OpenProject and get started with the self-hosted Community edition. If you want to run an instance of OpenProject in production (or for evaluation), refer to our in-depth [installation guides](https://www.openproject.org/download-and-installation/).
- **Enterprise Edition**: Sign up for the Enterprise version, choose between cloud or on-premises and benefit from comprehensive support and Enterprise add-ons.
- **Documentation**: Explore our [comprehensive documentation](https://www.openproject.org/docs/) to help you get up and running quickly.
@@ -40,7 +40,7 @@ You found a bug? Please [report it](https://www.openproject.org/docs/development
OpenProject is supported by its Community members, both companies and individuals.
We are always looking for new members to our Community, so if you are interested in improving OpenProject we would be glad to welcome and support you getting into the code. There are guides as well, e.g. a [Quick Start for Developers](https://www.openproject.org/development/setting-up-development-environment/), but don't hesitate to simply [contact us](https://www.openproject.org/contact) if you have questions.
We are always looking for new members to our Community, so if you are interested in improving OpenProject we would be glad to welcome and support you getting into the code. There are guides as well, e.g. a [Quick Start for Developers](https://www.openproject.org/docs/development/development-environment/), but don't hesitate to simply [contact us](https://www.openproject.org/contact) if you have questions.
Working on OpenProject comes with the satisfaction of working on a widely used open source application.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+7
View File
@@ -2,10 +2,12 @@
@import "work_packages/activities_tab/journals/new_component"
@import "work_packages/activities_tab/journals/index_component"
@import "work_packages/activities_tab/journals/item_component"
@import "work_packages/activities_tab/journals/revision_component"
@import "work_packages/activities_tab/journals/item_component/details"
@import "work_packages/activities_tab/journals/item_component/add_reactions"
@import "work_packages/activities_tab/journals/item_component/reactions"
@import "shares/modal_body_component"
@import "work_packages/reminder/modal_body_component"
@import "shares/invite_user_form_component"
@import "work_packages/details/tab_component"
@import "work_packages/progress/modal_body_component"
@@ -17,3 +19,8 @@
@import "projects/row_component"
@import "op_primer/border_box_table_component"
@import "work_packages/exports/modal_dialog_component"
@import "work_package_relations_tab/index_component"
@import "work_package_relations_tab/relation_component"
@import "users/hover_card_component"
@import "enterprise_edition/banner_component"
@import "work_packages/types/pattern_input"
@@ -29,14 +29,18 @@ See COPYRIGHT and LICENSE files for more details.
<div class="op-activity-days">
<% events_by_day_sorted_by_newest_first.each do |day, events| %>
<%= content_tag(@header_tag, helpers.format_activity_day(day), class: 'op-activity-days--header') %>
<%= content_tag(@header_tag, helpers.format_activity_day(day), class: "op-activity-days--header") %>
<ul class="op-activity-list">
<%=
render(Activities::ItemComponent.with_collection(events,
current_project: @current_project,
display_user: @display_user,
activity_page: @activity_page))
render(
Activities::ItemComponent.with_collection(
events,
current_project: @current_project,
display_user: @display_user,
activity_page: @activity_page
)
)
%>
</ul>
<% end -%>
@@ -37,19 +37,21 @@ See COPYRIGHT and LICENSE files for more details.
<%= project_suffix %>
</div>
<%=
render(Activities::ItemSubtitleComponent.new(
user: display_user? && @event.event_author,
datetime: @event.event_datetime,
is_creation: initial?,
is_deletion: deletion?,
is_work_package: work_package?,
journable_type: @event.journal.journable_type)
render(
Activities::ItemSubtitleComponent.new(
user: display_user? && @event.event_author,
datetime: @event.event_datetime,
is_creation: initial?,
is_deletion: deletion?,
is_work_package: work_package?,
journable_type: @event.journal.journable_type
)
)
%>
<% if comment.present? -%>
<ul class="op-activity-list--item-details">
<li class="op-activity-list--item-detail">
<strong><%= t('label_comment_added') %></strong>
<strong><%= t("label_comment_added") %></strong>
<p class="op-uc-p"><%= helpers.truncate_formatted_text(comment) %></p>
</li>
</ul>
@@ -29,7 +29,8 @@ See COPYRIGHT and LICENSE files for more details.
<div class="op-activity-list--item-subtitle">
<%=
I18n.t(i18n_key,
I18n.t(
i18n_key,
user: user_html,
datetime: datetime_html
).html_safe
+14 -11
View File
@@ -1,11 +1,14 @@
<%= render(Primer::ButtonComponent.new(scheme: :primary,
aria: { label: aria_label },
title:,
test_selector:,
tag: :a,
id:,
href: dynamic_path) ) do |button|
button.with_leading_visual_icon(icon: :plus)
label_text
end
%>
<%= render(
Primer::ButtonComponent.new(
scheme: :primary,
aria: { label: aria_label },
title:,
test_selector:,
tag: :a,
id:,
href: dynamic_path
)
) do |button|
button.with_leading_visual_icon(icon: :plus)
label_text
end %>
@@ -30,22 +30,24 @@ See COPYRIGHT and LICENSE files for more details.
<% helpers.html_title t(:label_administration), @title %>
<%= render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t(:"attributes.attachments") }
header.with_breadcrumbs([{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_storages_path, text: t("project_module_storages") },
t(:"attributes.attachments")])
header.with_tab_nav(label: nil) do |tab_nav|
tab_nav.with_tab(selected: @selected == 1, href: admin_settings_attachments_path) do |tab|
tab.with_text { t("settings.general") }
end
tab_nav.with_tab(selected: @selected == 2, href: admin_settings_virus_scanning_path) do |tab|
tab.with_icon(icon: :"op-enterprise-addons") unless EnterpriseToken.allows_to?("virus_scanning")
tab.with_text { t(:"settings.antivirus.title") }
end
if User.current.admin? && (EnterpriseToken.allows_to?(:virus_scanning) || Attachment.status_quarantined.any?)
tab_nav.with_tab(selected: @selected == 3, href: admin_quarantined_attachments_path) do |tab|
tab.with_text { t(:"antivirus_scan.quarantined_attachments.title") }
header.with_title { t(:"attributes.attachments") }
header.with_breadcrumbs(
[{ href: admin_index_path, text: t("label_administration") },
{ href: admin_settings_storages_path, text: t("project_module_storages") },
t(:"attributes.attachments")]
)
header.with_tab_nav(label: nil) do |tab_nav|
tab_nav.with_tab(selected: @selected == 1, href: admin_settings_attachments_path) do |tab|
tab.with_text { t("settings.general") }
end
tab_nav.with_tab(selected: @selected == 2, href: admin_settings_virus_scanning_path) do |tab|
tab.with_icon(icon: :"op-enterprise-addons") unless EnterpriseToken.allows_to?("virus_scanning")
tab.with_text { t(:"settings.antivirus.title") }
end
if User.current.admin? && (EnterpriseToken.allows_to?(:virus_scanning) || Attachment.status_quarantined.any?)
tab_nav.with_tab(selected: @selected == 3, href: admin_quarantined_attachments_path) do |tab|
tab.with_text { t(:"antivirus_scan.quarantined_attachments.title") }
end
end
end
end
end
end %>
end %>
@@ -29,34 +29,40 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t(:label_backup) }
header.with_breadcrumbs([{ href: admin_index_path, text: t(:label_administration) },
t(:label_backup)])
header.with_breadcrumbs(
[{ href: admin_index_path, text: t(:label_administration) },
t(:label_backup)]
)
header.with_action_button(tag: :a,
scheme: button_scheme,
mobile_label: button_title,
mobile_icon: button_icon,
size: :medium,
href: reset_token_admin_backups_path,
aria: { label: button_title },
title: button_title) do |button|
header.with_action_button(
tag: :a,
scheme: button_scheme,
mobile_label: button_title,
mobile_icon: button_icon,
size: :medium,
href: reset_token_admin_backups_path,
aria: { label: button_title },
title: button_title
) do |button|
button.with_leading_visual_icon(icon: button_icon)
button_title
end
if @backup_token.present?
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t("backup.label_delete_token"),
size: :medium,
href: delete_token_admin_backups_path,
aria: { label: I18n.t("backup.label_delete_token") },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :post
},
title: I18n.t(:button_delete)) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t("backup.label_delete_token"),
size: :medium,
href: delete_token_admin_backups_path,
aria: { label: I18n.t("backup.label_delete_token") },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :post
},
title: I18n.t(:button_delete)
) do |button|
button.with_leading_visual_icon(icon: :trash)
t("backup.label_delete_token")
end
@@ -30,22 +30,28 @@ See COPYRIGHT and LICENSE files for more details.
<%=
component_wrapper do
primer_form_with(
model: ,
model:,
url:,
data: { turbo: true },
method: :post
) do |form|
concat(render(Primer::Alpha::Dialog::Body.new(
id: dialog_body_id, test_selector: dialog_body_id, aria: { label: title },
style: "min-height: 300px"
)) do
render(Projects::CustomFields::CustomFieldMappingForm.new(form, project_mapping: @custom_field_project_mapping))
end)
concat(
render(
Primer::Alpha::Dialog::Body.new(
id: dialog_body_id, test_selector: dialog_body_id, aria: { label: title },
classes: "Overlay-body_autocomplete_height"
)
) do
render(Projects::CustomFields::CustomFieldMappingForm.new(form, project_mapping: @custom_field_project_mapping))
end
)
concat(render(Primer::Alpha::Dialog::Footer.new(show_divider: false)) do
concat(render(Primer::ButtonComponent.new(data: { 'close-dialog-id': dialog_id })) { cancel_button_text })
concat(render(Primer::ButtonComponent.new(scheme: :primary, type: :submit)) { submit_button_text })
end)
concat(
render(Primer::Alpha::Dialog::Footer.new(show_divider: false)) do
concat(render(Primer::ButtonComponent.new(data: { "close-dialog-id": dialog_id })) { cancel_button_text })
concat(render(Primer::ButtonComponent.new(scheme: :primary, type: :submit)) { submit_button_text })
end
)
end
end
%>
@@ -28,27 +28,18 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(Primer::Alpha::Dialog.new(id: DIALOG_ID, title: "Delete item", test_selector: TEST_SELECTOR)) do |dialog|
dialog.with_header(variant: :large)
dialog.with_body do
"Are you sure you want to delete this item from the current hierarchy level?"
end
dialog.with_footer do
concat(render(Primer::ButtonComponent.new(data: { "close-dialog-id": DIALOG_ID })) do
I18n.t(:button_cancel)
end)
concat(primer_form_with(
model: @custom_field,
url: custom_field_item_path(custom_field_id: @custom_field.id, id: @hierarchy_item.id),
method: :delete,
data: { turbo: true }
) do
render(Primer::ButtonComponent.new(scheme: :danger, type: :submit, data: { "close-dialog-id": DIALOG_ID })) do
I18n.t(:button_delete)
end
end)
render(
Primer::OpenProject::DangerDialog.new(
title: I18n.t("custom_fields.admin.items.delete_dialog.title"),
form_arguments:,
size: :large,
test_selector: TEST_SELECTOR
)
) do |dialog|
dialog.with_confirmation_message do |message|
message.with_heading(tag: :h2) { I18n.t("custom_fields.admin.items.delete_dialog.heading") }
message.with_description_content(I18n.t("custom_fields.admin.items.delete_dialog.description"))
end
dialog.with_confirmation_check_box_content(I18n.t(:text_permanent_delete_confirmation_checkbox_label))
end
%>
@@ -34,7 +34,6 @@ module Admin
class DeleteItemDialogComponent < ApplicationComponent
include OpTurbo::Streamable
DIALOG_ID = "op-hierarchy-item--deletion-confirmation"
TEST_SELECTOR = "op-custom-fields--delete-item-dialog"
def initialize(custom_field:, hierarchy_item:)
@@ -42,6 +41,13 @@ module Admin
@custom_field = custom_field
@hierarchy_item = hierarchy_item
end
def form_arguments
{
action: custom_field_item_path(custom_field_id: @custom_field.id, id: @hierarchy_item.id),
method: :delete
}
end
end
end
end
@@ -58,9 +58,11 @@ See COPYRIGHT and LICENSE files for more details.
item_container.with_column do
render(Primer::Alpha::ActionMenu.new(test_selector: "op-hierarchy-item--action-menu")) do |menu|
menu.with_show_button(icon: "kebab-horizontal",
scheme: :invisible,
"aria-label": I18n.t("custom_fields.admin.items.actions"))
menu.with_show_button(
icon: "kebab-horizontal",
scheme: :invisible,
"aria-label": I18n.t("custom_fields.admin.items.actions")
)
menu_items(menu)
end
end
@@ -31,10 +31,12 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { t(:label_custom_style) }
header.with_breadcrumbs([{ href: admin_index_path, text: t(:label_administration) },
t(:label_custom_style)])
header.with_breadcrumbs(
[{ href: admin_index_path, text: t(:label_administration) },
t(:label_custom_style)]
)
header.with_description { t(:label_custom_style_description) }
if @tabs.present?
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
@@ -0,0 +1,37 @@
<%=
component_wrapper do
flex_layout(data: wrapper_data_attributes) do |flex|
flex.with_row do
render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_action_button(
scheme: :primary,
tag: :a,
href: helpers.url_for(action: :new),
test_selector: "add-enumeration-button"
) do |button|
button.with_leading_visual_icon(icon: :plus)
I18n.t(:button_add)
end
end
end
flex.with_row do
render(border_box_container(data: drop_target_config)) do |component|
component.with_header(font_weight: :bold) { enumeration_class.model_name.human(count: :other) }
if enumerations.empty?
component.with_row do
render(Primer::Beta::Text.new(color: :subtle)) { t(:no_results_title_text) }
end
else
enumerations.each do |enumeration|
component.with_row(test_selector: "enumeration-row-#{enumeration.id}", data: draggable_item_config(enumeration)) do
render(item_component_class.new(enumeration: enumeration, max_position: max_position))
end
end
end
end
end
end
end
%>
@@ -0,0 +1,78 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Admin
module Enumerations
class IndexComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
options :enumerations
private
def max_position
enumerations.map(&:position).max
end
def wrapper_data_attributes
{
controller: "generic-drag-and-drop",
"application-target": "dynamic"
}
end
def drop_target_config
{
"is-drag-and-drop-target": true,
"target-container-accessor": "& > ul",
"target-allowed-drag-type": "enumeration"
}
end
def draggable_item_config(enumeration)
{
"draggable-id": enumeration.id,
"draggable-type": "enumeration",
"drop-url": helpers.url_for(action: :move, id: enumeration.id)
}
end
def enumeration_class
enumerations.klass
end
def item_component_class
ItemComponent
end
end
end
end
@@ -0,0 +1,41 @@
<%=
component_wrapper do
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))
end
if colored?
enumeration_info.with_column(mr: 2) do
helpers.icon_for_color enumeration.color
end
end
enumeration_info.with_column(mr: 2) do
render(Primer::Beta::Link.new(href: helpers.url_for(action: :edit, id: enumeration), underline: false)) do
render(Primer::Beta::Text.new(font_weight: :bold)) { enumeration.name }
end
end
unless enumeration.active?
enumeration_info.with_column(mr: 2) do
render(Primer::Beta::Text.new(color: :subtle)) { t(:label_inactive) }
end
end
end
enumeration_container.with_column do
render(Primer::Alpha::ActionMenu.new(test_selector: "op-time-entry-acrtivity--action-menu")) do |menu|
menu.with_show_button(
icon: "kebab-horizontal",
scheme: :invisible,
"aria-label": "Actions"
)
build_enumeration_menu(menu)
end
end
end
end
%>
@@ -0,0 +1,139 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Admin
module Enumerations
class ItemComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
options :enumeration
options :max_position
delegate :colored?, to: :enumeration
private
def wrapper_uniq_by
enumeration.id
end
def first_item?
enumeration.position == 1
end
def last_item?
enumeration.position == max_position
end
def build_enumeration_menu(menu)
edit_enumeration(menu)
menu.with_divider
if !first_item?
move_to_top_enumeration(menu)
move_up_enumeration(menu)
end
if !last_item?
move_down_enumeration(menu)
move_to_bottom_enumeration(menu)
end
menu.with_divider
deletion_enumeration(menu)
end
def edit_enumeration(menu)
menu.with_item(label: I18n.t(:button_edit),
tag: :a,
href: helpers.url_for(action: :edit, id: enumeration)) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
def move_to_top_enumeration(menu)
form_inputs = [{ name: "move_to", value: "highest" }]
menu.with_item(label: I18n.t(:label_sort_highest),
tag: :button,
href: helpers.url_for(action: :move, id: enumeration),
# content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
form_arguments: { method: :put, inputs: form_inputs }) do |item|
item.with_leading_visual_icon(icon: "move-to-top")
end
end
def move_up_enumeration(menu)
form_inputs = [{ name: "move_to", value: "higher" }]
menu.with_item(label: I18n.t(:label_sort_higher),
tag: :button,
href: helpers.url_for(action: :move, id: enumeration),
# content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
form_arguments: { method: :put, inputs: form_inputs }) do |item|
item.with_leading_visual_icon(icon: "chevron-up")
end
end
def move_down_enumeration(menu)
form_inputs = [{ name: "move_to", value: "lower" }]
menu.with_item(label: I18n.t(:label_sort_lower),
tag: :button,
href: helpers.url_for(action: :move, id: enumeration),
# content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
form_arguments: { method: :put, inputs: form_inputs }) do |item|
item.with_leading_visual_icon(icon: "chevron-down")
end
end
def move_to_bottom_enumeration(menu)
form_inputs = [{ name: "move_to", value: "lowest" }]
menu.with_item(label: I18n.t(:label_sort_lowest),
tag: :button,
href: helpers.url_for(action: :move, id: enumeration),
# content_arguments: { data: { turbo_frame: ItemsComponent.wrapper_key } },
form_arguments: { method: :put, inputs: form_inputs }) do |item|
item.with_leading_visual_icon(icon: "move-to-bottom")
end
end
def deletion_enumeration(menu)
menu.with_item(label: I18n.t(:button_delete),
tag: :button,
scheme: :danger,
href: helpers.url_for(action: :destroy, id: enumeration),
form_arguments: { method: :delete }) do |item|
item.with_leading_visual_icon(icon: :trash)
end
end
end
end
end
@@ -0,0 +1,66 @@
# 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 Enumerations
class ItemForm < ApplicationForm
delegate :object, to: :@builder
form do |form|
form.text_field(
name: :name,
label: object.class.human_attribute_name(:name),
required: true,
input_width: :medium
)
if object.colored?
form.color_select_list(
label: attribute_name(:color_id),
name: :color_id,
caption: object.color_label,
input_width: :medium
)
end
form.check_box(
name: :active,
label: object.class.human_attribute_name(:active)
)
form.submit(
name: :submit,
label: I18n.t(:button_save),
scheme: :primary
)
end
end
end
end
@@ -0,0 +1,67 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Admin
module Enumerations
class ReassignForm < ApplicationForm
include OpenProject::StaticRouting::UrlHelpers
attr_reader :other_enumerations
def initialize(other_enumerations:)
super()
@other_enumerations = other_enumerations
end
form do |form|
form.select_list(
name: :reassign_to_id,
label: I18n.t(:text_enumeration_category_reassign_to),
required: true,
input_width: :large
) do |select|
other_enumerations.each do |activity|
select.option(value: activity.id, label: activity.name)
end
end
form.group(layout: :horizontal) do |button_group|
button_group.button(name: :cancel,
tag: :a,
label: I18n.t(:button_cancel),
scheme: :default,
href: admin_settings_time_entry_activities_path)
button_group.submit(name: :submit, label: I18n.t(:button_apply), scheme: :primary)
end
end
end
end
end
@@ -31,12 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
header.with_title { I18n.t(:"attribute_help_texts.label_plural") }
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: :normal)
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: currently_selected_tab == tab, href: tab[:path]) do |t|
t.with_text { I18n.t(tab[:label]) }
if @tabs.present?
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: currently_selected_tab == tab, href: tab[:path]) do |t|
t.with_text { I18n.t(tab[:label]) }
end
end
end
end if @tabs.present?
end
end
%>
@@ -39,9 +39,11 @@ class AttributeHelpTexts::IndexPageHeaderComponent < ApplicationComponent
end
def breadcrumb_items
[{ href: admin_index_path, text: t("label_administration") },
I18n.t("menus.breadcrumb.nested_element", section_header: t(:"attribute_help_texts.label_plural"),
title: I18n.t(currently_selected_tab[:label].to_s)).html_safe]
[
{ href: admin_index_path, text: t("label_administration") },
helpers.nested_breadcrumb_element(t(:"attribute_help_texts.label_plural"),
I18n.t(currently_selected_tab[:label].to_s))
]
end
def currently_selected_tab
@@ -32,18 +32,20 @@ See COPYRIGHT and LICENSE files for more details.
header.with_breadcrumbs(breadcrumb_items)
if @color.persisted?
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: color_path(@color),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: color_path(@color),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
@@ -32,7 +32,7 @@ See COPYRIGHT and LICENSE files for more details.
<div class="on-off-status-cell">
<% if enabled? %>
<span class="on-off-status -enabled">
<%= helpers.op_icon 'icon-yes' %>
<%= helpers.op_icon "icon-yes" %>
<%= model[:on_text] %>
</span>
<p>
@@ -40,7 +40,7 @@ See COPYRIGHT and LICENSE files for more details.
</p>
<% else %>
<span class="on-off-status -disabled">
<%= helpers.op_icon 'icon-not-supported' %>
<%= helpers.op_icon "icon-not-supported" %>
<%= model[:off_text] %>
</span>
<p><%= model[:off_description] %></p>
@@ -33,6 +33,8 @@ module OpTurbo
# rubocop:enable OpenProject/AddPreviewForViewComponent
INLINE_ACTIONS = %i[dialog flash].freeze
# Turbo allows the response method for these actions only:
ACTIONS_WITH_METHOD = %i[update replace].freeze
extend ActiveSupport::Concern
@@ -43,7 +45,7 @@ module OpTurbo
end
included do
def render_as_turbo_stream(view_context:, action: :update)
def render_as_turbo_stream(view_context:, action: :update, method: nil)
case action
when :update, *INLINE_ACTIONS
@inner_html_only = true
@@ -63,8 +65,13 @@ module OpTurbo
"Wrap your component in a `component_wrapper` block in order to use turbo-stream methods"
end
if method && !action.in?(ACTIONS_WITH_METHOD)
raise ArgumentError, "The #{action} action does not supports a method"
end
OpTurbo::StreamComponent.new(
action:,
method:,
target: wrapper_key,
template:
).render_in(view_context)
@@ -3,10 +3,12 @@
flex_layout do |content|
if has_no_items_or_projects?
content.with_row(mb: 3) do
render Primer::Alpha::Banner.new(scheme: :default,
icon: :info,
dismiss_scheme: :hide,
test_selector: "op-custom-fields--new-hierarchy-banner") do
render Primer::Alpha::Banner.new(
scheme: :default,
icon: :info,
dismiss_scheme: :hide,
test_selector: "op-custom-fields--new-hierarchy-banner"
) do
I18n.t("custom_fields.admin.notice.remember_items_and_projects")
end
end
@@ -31,12 +31,14 @@ See COPYRIGHT and LICENSE files for more details.
header.with_title { I18n.t(:label_custom_field_plural) }
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: :normal)
header.with_tab_nav(label: nil, test_selector: "custom-fields--tab-nav") do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: currently_selected_tab == tab, href: tab[:path]) do |t|
t.with_text { I18n.t(tab[:label]) }
if @tabs.present?
header.with_tab_nav(label: nil, test_selector: "custom-fields--tab-nav") do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: currently_selected_tab == tab, href: tab[:path]) do |t|
t.with_text { I18n.t(tab[:label]) }
end
end
end
end if @tabs.present?
end
end
%>
@@ -39,9 +39,11 @@ class CustomFields::IndexPageHeaderComponent < ApplicationComponent
end
def breadcrumb_items
[{ href: admin_index_path, text: t("label_administration") },
I18n.t("menus.breadcrumb.nested_element", section_header: t(:label_custom_field_plural),
title: I18n.t(currently_selected_tab[:label].to_s)).html_safe]
[
{ href: admin_index_path, text: t("label_administration") },
helpers.nested_breadcrumb_element(t(:label_custom_field_plural),
I18n.t(currently_selected_tab[:label].to_s))
]
end
def currently_selected_tab
@@ -0,0 +1,23 @@
<%=
grid_layout("op-ee-banner", **@system_arguments) do |grid|
grid.with_area(:"icon-container") do
content_tag :div, class: "op-ee-banner--shield" do
render(
Primer::Beta::Octicon.new(
icon: "op-enterprise-addons",
size: :medium,
classes: "op-ee-banner--icon"
)
)
end
end
grid.with_area(:"title-container") { render(Primer::Beta::Text.new) { title } }
grid.with_area(:"description-container") { render(Primer::Beta::Text.new) { description } }
grid.with_area(:"link-container") do
render(Primer::Beta::Link.new(href: href)) do |link|
link.with_trailing_visual_icon(icon: "link-external")
link_title
end
end
end
%>
@@ -0,0 +1,108 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module EnterpriseEdition
# A banner indicating that a given feature requires the enterprise edition of OpenProject.
# This component uses conventional names for translation keys or URL look-ups based on the feature_key passed in.
# It will only be rendered if necessary.
class BannerComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
# @param feature_key [Symbol, NilClass] The key of the feature to show the banner for.
# @param title [String] The title of the banner.
# @param description [String] The description of the banner.
# @param href [String] The URL to link to.
# @param skip_render [Boolean] Whether to skip rendering the banner.
# @param system_arguments [Hash] <%= link_to_system_arguments_docs %>
def initialize(feature_key,
title: nil,
description: nil,
link_title: nil,
href: nil,
skip_render: !EnterpriseToken.show_banners?,
**system_arguments)
@system_arguments = system_arguments
@system_arguments[:tag] = "div"
@system_arguments[:test_selector] = "op-ee-banner-#{feature_key.to_s.tr('_', '-')}"
super
@feature_key = feature_key
@title = title
@description = description
@link_title = link_title
@href = href
@skip_render = skip_render
end
private
attr_reader :skip_render,
:feature_key
def title
@title || I18n.t("ee.upsale.#{feature_key}.title", default: I18n.t("ee.upsale.title"))
end
def description
@description || begin
I18n.t("ee.upsale.#{feature_key}.description")
rescue StandardError
I18n.t("ee.upsale.#{feature_key}.description_html")
end
rescue I18n::MissingTranslationData => e
raise e.exception(
<<~TEXT.squish
The expected '#{I18n.locale}.ee.upsale.#{feature_key}.description' key does not exist.
Ideally, provide it in the locale file.
If that isn't applicable, a description parameter needs to be provided.
TEXT
)
end
def link_title
@link_title || I18n.t("ee.upsale.#{feature_key}.link_title", default: I18n.t("ee.upsale.link_title"))
end
def href
href_value = @href || OpenProject::Static::Links.links.dig(:enterprise_docs, feature_key, :href)
unless href_value
raise "Neither a custom href is provided nor is a value set " \
"in OpenProject::Static::Links.enterprise_docs[#{feature_key}][:href]"
end
href_value
end
def render?
!skip_render
end
end
end
@@ -0,0 +1,68 @@
/*!
/ -- copyright
/ OpenProject is an open source project management software.
/ Copyright (C) 2024 the OpenProject GmbH
/
/ This program is free software; you can redistribute it and/or
/ modify it under the terms of the GNU General Public License version 3.
/
/ OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
/ Copyright (C) 2006-2013 Jean-Philippe Lang
/ Copyright (C) 2010-2013 the ChiliProject Team
/
/ This program is free software; you can redistribute it and/or
/ modify it under the terms of the GNU General Public License
/ as published by the Free Software Foundation; either version 2
/ of the License, or (at your option) any later version.
/
/ This program is distributed in the hope that it will be useful,
/ but WITHOUT ANY WARRANTY; without even the implied warranty of
/ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/ GNU General Public License for more details.
/
/ You should have received a copy of the GNU General Public License
/ along with this program; if not, write to the Free Software
/ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
/
/ See COPYRIGHT and LICENSE files for more details.
/ ++
/
$op-ee-banner--shield-width: 32px
// This is not named op-enterprise-banner because as of now, there is still a legacy angular component that uses that block name.
.op-ee-banner
display: grid
grid-template-columns: $op-ee-banner--shield-width auto auto
grid-template-areas: "icon-container title-container" "icon-container description-container" "icon-container link-container"
grid-column-gap: 0.5rem
justify-content: left
@media screen and (min-width: $breakpoint-md)
grid-template-areas: "icon-container title-container title-container" "icon-container description-container link-container"
&--icon-container
@extend .upsale-colored
align-self: start
justify-self: center
&--shield
@extend .upsale-border-colored
width: $op-ee-banner--shield-width
height: 42px
border-width: 10px 5px 10px 5px
border-radius: 0 0 10px 10px
border-style: solid
display: flex
align-items: center
justify-content: center
&--icon
width: $op-ee-banner--shield-width
height: $op-ee-banner--shield-width
&--title-container
@extend .upsale-colored
font-weight: bold
&--link-container
align-self: end
+12 -12
View File
@@ -30,12 +30,15 @@
module Enumerations
class TableComponent < ::TableComponent
attr_reader :enumeration
def initialize(enumeration:, rows: [], **)
super(rows: rows, **)
@enumeration = enumeration
end
def columns
%i[name is_default active sort].tap do |default|
if with_colors
default.insert 3, :color
end
end
headers.map(&:first)
end
def sortable?
@@ -43,16 +46,13 @@ module Enumerations
end
def headers
[
@headers ||= [
["name", { caption: Enumeration.human_attribute_name(:name) }],
["is_default", { caption: Enumeration.human_attribute_name(:is_default) }],
enumeration.can_have_default_value? ? ["is_default", { caption: Enumeration.human_attribute_name(:is_default) }] : nil,
["active", { caption: Enumeration.human_attribute_name(:active) }],
with_colors ? ["color", { caption: Enumeration.human_attribute_name(:color) }] : nil,
["sort", { caption: I18n.t(:label_sort) }]
].tap do |default|
if with_colors
default.insert 3, ["color", { caption: Enumeration.human_attribute_name(:color) }]
end
end
].compact
end
def with_colors
@@ -1,9 +1,13 @@
<%= component_wrapper tag: "turbo-frame" do %>
<%= render(Primer::Beta::Button.new(scheme: :secondary,
disabled:,
data: { "filter--filters-form-target": "filterFormToggle",
action: "filter--filters-form#toggleDisplayFilters" },
test_selector: "filter-component-toggle")) do |button| %>
<%= render(
Primer::Beta::Button.new(
scheme: :secondary,
disabled:,
data: { "filter--filters-form-target": "filterFormToggle",
action: "filter--filters-form#toggleDisplayFilters" },
test_selector: "filter-component-toggle"
)
) do |button| %>
<% button.with_trailing_visual_counter(count: filters_count, test_selector: "filters-button-counter") %>
<%= t(:label_filter) %>
<% end %>
+46 -9
View File
@@ -67,18 +67,55 @@ module Filter
case filter
when Queries::Filters::Shared::ProjectFilter::Required,
Queries::Filters::Shared::ProjectFilter::Optional
{
autocomplete_options: {
component: "opce-project-autocompleter",
resource: "projects",
filters: [
{ name: "active", operator: "=", values: ["t"] }
]
}
}
{ autocomplete_options: project_autocomplete_options }
when Queries::Filters::Shared::CustomFields::User
{ autocomplete_options: user_autocomplete_options }
when Queries::Filters::Shared::CustomFields::ListOptional,
Queries::Projects::Filters::ProjectStatusFilter,
Queries::Projects::Filters::TypeFilter
{ autocomplete_options: list_autocomplete_options(filter) }
else
{}
end
end
def list_autocomplete_options(filter)
{
component: "opce-autocompleter",
items: filter.allowed_values.map { |name, id| { name:, id: } },
model: filter.values,
bindValue: "id",
bindLabel: "name",
hideSelected: true
}
end
def project_autocomplete_options
{
component: "opce-project-autocompleter",
resource: "projects",
filters: [
{ name: "active", operator: "=", values: ["t"] }
]
}
end
def user_autocomplete_options
{
component: "opce-user-autocompleter",
hideSelected: true,
defaultData: false,
placeholder: I18n.t(:label_user_search),
resource: "principals",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.principals,
filters: [
{ name: "type", operator: "=", values: ["User"] },
{ name: "status", operator: "!", values: [Principal.statuses["locked"].to_s] },
{ name: "member", operator: "=", values: Project.visible.pluck(:id) }
],
searchKey: "any_name_attribute",
focusDirectly: false
}
end
end
end
@@ -31,41 +31,47 @@ See COPYRIGHT and LICENSE files for more details.
header.with_title { @group.name }
header.with_breadcrumbs(breadcrumb_items)
header.with_action_button(tag: :a,
mobile_icon: :person,
mobile_label: t(:label_profile),
size: :medium,
href: show_group_path(@group),
aria: { label: I18n.t(:label_profile) },
title: I18n.t(:label_profile)) do |button|
header.with_action_button(
tag: :a,
mobile_icon: :person,
mobile_label: t(:label_profile),
size: :medium,
href: show_group_path(@group),
aria: { label: I18n.t(:label_profile) },
title: I18n.t(:label_profile)
) do |button|
button.with_leading_visual_icon(icon: :person)
t(:label_profile)
end
if @current_user.admin?
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: group_path(@group),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: t(:text_are_you_sure),
method: :delete,
},
title: I18n.t(:button_delete)) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: group_path(@group),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
end
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: selected_tab(@tabs) == tab, href: tab[:path]) do |t|
t.with_text { I18n.t("js.#{tab[:label]}") }
if @tabs.present?
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: selected_tab(@tabs) == tab, href: tab[:path]) do |t|
t.with_text { I18n.t("js.#{tab[:label]}") }
end
end
end
end if @tabs.present?
end
end
%>
@@ -28,34 +28,38 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title(test_selector: "groups--title") { @group.name }
header.with_title(test_selector: "groups--title") { @group.name }
header.with_breadcrumbs(breadcrumb_items)
if @current_user.admin?
header.with_action_button(tag: :a,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
href: edit_group_path(@group),
aria: { label: I18n.t(:button_edit) },
data: { "test-selector": "groups--edit-group-button" },
title: I18n.t(:button_edit)) do |button|
header.with_action_button(
tag: :a,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
href: edit_group_path(@group),
aria: { label: I18n.t(:button_edit) },
data: { "test-selector": "groups--edit-group-button" },
title: I18n.t(:button_edit)
) do |button|
button.with_leading_visual_icon(icon: :pencil)
t(:button_edit)
end
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: group_path(@group),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: t(:text_are_you_sure),
method: :delete,
},
title: I18n.t(:button_delete)) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: group_path(@group),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
@@ -28,11 +28,11 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%= form_tag(filter_path, method: :get) do %>
<% collapsed_class = initially_visible? ? '' : 'collapsed' %>
<% collapsed_class = initially_visible? ? "" : "collapsed" %>
<fieldset class="simple-filters--container <%= collapsed_class %>" data-members-form-target="filterContainer">
<legend><%= t(:label_filter_plural) %></legend>
<% if has_close_icon? %>
<a title="<%= t('js.close_form_title') %>"
<a title="<%= t("js.close_form_title") %>"
data-action="members-form#toggleMemberFilter"
class="toggle-member-filter-link simple-filters--close icon-context icon-close">
</a>
@@ -41,24 +41,24 @@ See COPYRIGHT and LICENSE files for more details.
<% if has_statuses? %>
<li class="simple-filters--filter">
<label class='simple-filters--filter-name' for='status'><%= User.human_attribute_name(:status) %>:</label>
<%= select_tag 'status',
<%= select_tag "status",
user_status_options,
class: 'simple-filters--filter-value',
'data-members-form-target': 'statusSelect' %>
class: "simple-filters--filter-value",
"data-members-form-target": "statusSelect" %>
</li>
<% end %>
<% if has_groups? %>
<li class="simple-filters--filter">
<label class='simple-filters--filter-name' for='group_id'><%= Group.model_name.human %>:</label>
<%= collection_select :group,
:id,
groups,
:id,
:name,
{ include_blank: true,
selected: params[:group_id].to_i },
{ name: "group_id",
class: 'simple-filters--filter-value' } %>
:id,
groups,
:id,
:name,
{ include_blank: true,
selected: params[:group_id].to_i },
{ name: "group_id",
class: "simple-filters--filter-value" } %>
</li>
<% end %>
<% if roles.present? %>
@@ -77,14 +77,15 @@ See COPYRIGHT and LICENSE files for more details.
},
{
name: "role_id",
class: 'simple-filters--filter-value'
})
%>
class: "simple-filters--filter-value"
}
)
%>
</li>
<% end %>
<% if has_shares? %>
<li class="simple-filters--filter">
<label class='simple-filters--filter-name' for='shared_role_id'><%= t('members.menu.wp_shares') %>:</label>
<label class='simple-filters--filter-name' for='shared_role_id'><%= t("members.menu.wp_shares") %>:</label>
<%=
select_tag(
:shared_role_id,
@@ -95,18 +96,19 @@ See COPYRIGHT and LICENSE files for more details.
{
include_blank: true,
name: "shared_role_id",
class: 'simple-filters--filter-value'
})
class: "simple-filters--filter-value"
}
)
%>
</li>
<% end %>
<li class="simple-filters--filter">
<label class='simple-filters--filter-name' for='name'><%= User.human_attribute_name :name %>:</label>
<%= text_field_tag 'name', params[:name], class: 'simple-filters--filter-value' %>
<%= text_field_tag "name", params[:name], class: "simple-filters--filter-value" %>
</li>
<li class="simple-filters--controls">
<%= submit_tag t(:button_apply), class: 'button -primary -small', name: nil %>
<%= link_to t(:button_clear), clear_url, class: 'button -small -with-icon icon-undo' %>
<%= submit_tag t(:button_apply), class: "button -primary -small", name: nil %>
<%= link_to t(:button_clear), clear_url, class: "button -small -with-icon icon-undo" %>
</li>
</ul>
</fieldset>
@@ -63,7 +63,7 @@ class Members::IndexPageHeaderComponent < ApplicationComponent
if @query && query_name
if menu_header.present?
I18n.t("menus.breadcrumb.nested_element", section_header: menu_header, title: query_name).html_safe
helpers.nested_breadcrumb_element(menu_header, query_name)
else
query_name
end
@@ -1,22 +1,26 @@
<%= render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_filter_button(label: I18n.t(:description_filter),
id: "filter-member-button",
aria: { label: I18n.t(:description_filter) },
class: "toggle-member-filter-link",
data: filter_button_data_attributes) do
I18n.t(:description_filter)
end
subheader.with_filter_button(
label: I18n.t(:description_filter),
id: "filter-member-button",
aria: { label: I18n.t(:description_filter) },
class: "toggle-member-filter-link",
data: filter_button_data_attributes
) do
I18n.t(:description_filter)
end
subheader.with_action_button(scheme: :primary,
aria: { label: I18n.t(:button_add_member) },
title: I18n.t(:button_add_member),
id: "add-member-button",
data: add_button_data_attributes) do |button|
button.with_leading_visual_icon(icon: :plus)
t('activerecord.models.member')
end
subheader.with_action_button(
scheme: :primary,
aria: { label: I18n.t(:button_add_member) },
title: I18n.t(:button_add_member),
id: "add-member-button",
data: add_button_data_attributes
) do |button|
button.with_leading_visual_icon(icon: :plus)
t("activerecord.models.member")
end
subheader.with_bottom_pane_component do
render ::Members::UserFilterComponent.new(params, **@members_filter_options)
end
end %>
subheader.with_bottom_pane_component do
render ::Members::UserFilterComponent.new(params, **@members_filter_options)
end
end %>
@@ -43,8 +43,7 @@ See COPYRIGHT and LICENSE files for more details.
member.roles.include?(role),
role.name,
disabled: role_disabled?(role),
no_label: true
%>
no_label: true %>
<%= role %>
</label>
<% end %>
@@ -0,0 +1,62 @@
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { @topic.subject }
header.with_breadcrumbs(breadcrumb_items)
watcher_action_button(header, @topic)
if !@topic.locked? && authorize_for("messages", "reply")
header.with_action_button(
tag: :a,
scheme: :default,
mobile_icon: :quote,
mobile_label: t(:button_quote),
size: :medium,
href: url_for({ action: "quote", id: @topic }),
aria: { label: I18n.t(:button_delete) },
data: { action: "forum-messages#quote", test_selector: "message-quote-button" },
title: t(:button_quote)
) do |button|
button.with_leading_visual_icon(icon: :quote)
t(:button_quote)
end
end
if @message.editable_by?(User.current)
header.with_action_button(
tag: :a,
scheme: :default,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
href: edit_topic_path(@topic),
aria: { label: t(:button_edit) },
data: { test_selector: "message-edit-button" },
title: t(:button_edit)
) do |button|
button.with_leading_visual_icon(icon: :pencil)
t(:button_edit)
end
end
if @message.destroyable_by?(User.current)
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: topic_path(@topic),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
end
end
%>
@@ -0,0 +1,54 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Messages
class ShowPageHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include ApplicationHelper
include WatchersHelper
def initialize(topic:, message:, forum:, project:)
super
@topic = topic
@message = message
@forum = forum
@project = project
end
def breadcrumb_items
[
{ href: project_overview_path(@project.id), text: @project.name },
{ href: project_forums_path(@project), text: t(:label_forum_plural) },
{ href: project_forum_path(@project, @forum), text: @forum.name },
@topic.subject
]
end
end
end
@@ -28,28 +28,33 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<%=
render(Primer::OpenProject::FeedbackDialog.new(
id:,
title: nil,
size: :large
)) do |dialog|
render(
Primer::OpenProject::FeedbackDialog.new(
id:,
title: I18n.t("my.access_token.create_dialog.title"),
size: :large
)
) do |dialog|
dialog.with_feedback_message do |message|
message.with_heading(tag: :h2) { I18n.t("my.access_token.create_dialog.header", type: "API") }
end
dialog.with_additional_content do
dialog.with_additional_details do
flex_layout do |flex|
flex.with_row(mb: 2) do
render(Primer::OpenProject::InputGroup.new) do |input_group|
input_group.with_text_input(name: :openproject_api_access_token,
label: Token::API.model_name.human,
visually_hide_label: false,
value: @token_value)
input_group.with_text_input(
name: :openproject_api_access_token,
label: Token::API.model_name.human,
visually_hide_label: false,
value: @token_value
)
input_group.with_trailing_action_clipboard_copy_button(
value: @token_value,
aria: {
label: I18n.t('button_copy_to_clipboard')
})
label: I18n.t("button_copy_to_clipboard")
}
)
end
end
flex.with_row do
@@ -47,7 +47,7 @@ See COPYRIGHT and LICENSE files for more details.
<div tabindex="0" class="-info op-toast">
<div role="alert" aria-atomic="true" class="op-toast--content">
<p>
<span><%= t('my_account.access_tokens.api.disabled_text') %></span>
<span><%= t("my_account.access_tokens.api.disabled_text") %></span>
</p>
</div>
</div>
@@ -36,10 +36,12 @@ See COPYRIGHT and LICENSE files for more details.
method: :post
) do |form|
component_collection do |collection|
collection.with_component(Primer::Alpha::Dialog::Body.new(
aria: { label: I18n.t("my.access_token.new_access_token_dialog_title") }
)) do
flex_layout(my: 3) do |body|
collection.with_component(
Primer::Alpha::Dialog::Body.new(
aria: { label: I18n.t("my.access_token.new_access_token_dialog_title") }
)
) do
flex_layout(mb: 3) do |body|
body.with_row do
render(Primer::Alpha::Banner.new(scheme: :warning)) do
I18n.t("my.access_token.new_access_token_dialog_attention_text")
@@ -60,7 +62,7 @@ See COPYRIGHT and LICENSE files for more details.
collection.with_component(Primer::Alpha::Dialog::Footer.new) do
component_collection do |footer|
footer.with_component(Primer::ButtonComponent.new(data: { 'close-dialog-id': "new-access-token-dialog" })) do
footer.with_component(Primer::ButtonComponent.new(data: { "close-dialog-id": "new-access-token-dialog" })) do
I18n.t("button_cancel")
end
@@ -1,15 +1,17 @@
<%= render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title { page_title }
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
header.with_title { page_title }
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
header.with_action_button(tag: :a,
mobile_icon: :gear,
mobile_label: I18n.t(:label_setting_plural),
href: my_notifications_path,
size: :medium,
target: "_blank",
aria: { label: I18n.t("js.notifications.settings.title") }) do |button|
button.with_leading_visual_icon(icon: :gear)
I18n.t(:label_setting_plural)
end
end %>
header.with_action_button(
tag: :a,
mobile_icon: :gear,
mobile_label: I18n.t(:label_setting_plural),
href: my_notifications_path,
size: :medium,
target: "_blank",
aria: { label: I18n.t("js.notifications.settings.title") }
) do |button|
button.with_leading_visual_icon(icon: :gear)
I18n.t(:label_setting_plural)
end
end %>
@@ -53,7 +53,7 @@ module Notifications
def current_breadcrumb_element
if current_section && current_section.header.present?
I18n.t("menus.breadcrumb.nested_element", section_header: current_section.header, title: page_title).html_safe
helpers.nested_breadcrumb_element(current_section.header, page_title)
else
page_title
end
@@ -1,25 +1,31 @@
<%= render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_filter_component do
render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t(:label_filter_plural))) do |control|
control.with_item(tag: :a,
href: notifications_path(facet: nil, **current_filters),
label: t("notifications.facets.unread"),
title: t("notifications.facets.unread_title"),
selected: @facet != "all")
control.with_item(tag: :a,
href: notifications_path(facet: "all", **current_filters),
label: t("notifications.facets.all"),
title: t("notifications.facets.all_title"),
selected: @facet == "all")
end
end
subheader.with_filter_component do
render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t(:label_filter_plural))) do |control|
control.with_item(
tag: :a,
href: notifications_path(facet: nil, **current_filters),
label: t("notifications.facets.unread"),
title: t("notifications.facets.unread_title"),
selected: @facet != "all"
)
control.with_item(
tag: :a,
href: notifications_path(facet: "all", **current_filters),
label: t("notifications.facets.all"),
title: t("notifications.facets.all_title"),
selected: @facet == "all"
)
end
end
subheader.with_action_button(tag: :a,
href: mark_all_read_notifications_path(**current_filters),
data: { method: :post },
size: :medium,
aria: { label: I18n.t("js.notifications.center.mark_all_read") }) do |button|
button.with_leading_visual_icon(icon: :'op-read-all')
I18n.t("js.notifications.center.mark_all_read")
end
end %>
subheader.with_action_button(
tag: :a,
href: mark_all_read_notifications_path(**current_filters),
data: { method: :post },
size: :medium,
aria: { label: I18n.t("js.notifications.center.mark_all_read") }
) do |button|
button.with_leading_visual_icon(icon: :"op-read-all")
I18n.t("js.notifications.center.mark_all_read")
end
end %>
@@ -1,5 +1,5 @@
<%=
component_wrapper(data: { 'test-selector': "op-admin-oauth--application" }) do
component_wrapper(data: { "test-selector": "op-admin-oauth--application" }) do
flex_layout(align_items: :center, justify_content: :space_between) do |oauth_application_container|
oauth_application_container.with_column(flex_layout: true) do |application_information|
application_information.with_column(mr: 2) do
@@ -29,14 +29,16 @@
# Actions
oauth_application_container.with_column do
render(Primer::Alpha::ToggleSwitch.new(
src: toggle_oauth_application_path(@application),
csrf_token: form_authenticity_token,
checked: @application.enabled?,
data: {
'test-selector': "op-admin-oauth--application-enabled-toggle-switch"
}
))
render(
Primer::Alpha::ToggleSwitch.new(
src: toggle_oauth_application_path(@application),
csrf_token: form_authenticity_token,
checked: @application.enabled?,
data: {
"test-selector": "op-admin-oauth--application-enabled-toggle-switch"
}
)
)
end
end
end
@@ -3,9 +3,13 @@
flex_layout do |index_container|
if OpenProject::FeatureDecisions.built_in_oauth_applications_active?
index_container.with_row do
render(border_box_container(mb: 4, data: {
'test-selector': "op-admin-oauth--built-in-applications"
})) do |component|
render(
border_box_container(
mb: 4, data: {
"test-selector": "op-admin-oauth--built-in-applications"
}
)
) do |component|
component.with_header(font_weight: :bold) do
render(Primer::Beta::Text.new) do
t("oauth.header.builtin_applications")
@@ -14,9 +18,13 @@
if @built_in_applications.empty?
component.with_row do
render(Primer::Beta::Text.new(data: {
'test-selector': "op-admin-oauth--built-in-applications-placeholder"
})) do
render(
Primer::Beta::Text.new(
data: {
"test-selector": "op-admin-oauth--built-in-applications-placeholder"
}
)
) do
t("oauth.empty_application_lists")
end
end
@@ -39,7 +47,7 @@
if @other_applications.empty?
component.with_row do
render(Primer::Beta::Text.new(data: { 'test-selector': "op-admin-oauth--applications-placeholder" })) do
render(Primer::Beta::Text.new(data: { "test-selector": "op-admin-oauth--applications-placeholder" })) do
t("oauth.empty_application_lists")
end
end
@@ -33,29 +33,33 @@ See COPYRIGHT and LICENSE files for more details.
header.with_breadcrumbs(breadcrumb_items)
unless @application.builtin?
header.with_action_button(tag: :a,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
href: edit_oauth_application_path(@application),
aria: { label: I18n.t(:button_edit) },
title: I18n.t(:button_edit)) do |button|
header.with_action_button(
tag: :a,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
href: edit_oauth_application_path(@application),
aria: { label: I18n.t(:button_edit) },
title: I18n.t(:button_edit)
) do |button|
button.with_leading_visual_icon(icon: :pencil)
t(:button_edit)
end
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: oauth_application_path(@application),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
href: oauth_application_path(@application),
aria: { label: I18n.t(:button_delete) },
data: {
confirm: I18n.t(:text_are_you_sure),
method: :delete
},
title: I18n.t(:button_delete)
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
@@ -31,14 +31,21 @@ See COPYRIGHT and LICENSE files for more details.
render Primer::BaseComponent.new(
tag: :div,
id: row_css_id,
classes: "#{table.grid_class} #{row_css_class}",
) do %>
classes: "#{table.grid_class} #{row_css_class}"
) do
%>
<% columns.each do |column| %>
<%= render(Primer::BaseComponent.new(tag: :div, classes: column_css_class(column), **column_args(column))) { column_value(column) } %>
<%= render(Primer::BaseComponent.new(tag: :div, classes: grid_column_classes(column), **column_args(column))) do %>
<% label = mobile_label(column) %>
<% if label.present? %>
<%= render(Primer::Beta::Text.new(classes: "op-border-box-grid--row-label")) { "#{label}: " } %>
<% end %>
<%= column_value(column) %>
<% end %>
<% end %>
<% if table.has_actions? %>
<%= flex_layout(align_items: :center, justify_content: :flex_end) do |flex| %>
<%= flex_layout(classes: "op-border-box-grid--row-action") do |flex| %>
<% button_links.each_with_index do |link, i| %>
<% args = i == (button_links.count - 1) ? {} : { mr: 1 } %>
<% flex.with_column(**args) { link } %>
@@ -32,6 +32,26 @@ module OpPrimer
class BorderBoxRowComponent < RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
include ComponentHelpers
def mobile_label(column)
return unless table.mobile_labels.include?(column)
table.column_title(column)
end
def visible_on_mobile?(column)
table.mobile_columns.include?(column)
end
def grid_column_classes(column)
classes = ["op-border-box-grid--row-item"]
classes << column_css_class(column)
classes << "op-border-box-grid--main-column" if table.main_column?(column)
classes << "ellipsis" unless table.main_column?(column)
classes << "op-border-box-grid--no-mobile" unless visible_on_mobile?(column)
classes.compact.join(" ")
end
def column_args(_column)
{}
end
@@ -31,16 +31,34 @@ See COPYRIGHT and LICENSE files for more details.
render(
border_box_container(
classes: container_class,
test_selector:,
)) do |component|
test_selector:
)
) do |component|
component.with_header(classes: grid_class, color: :muted) do
headers.each do |name, args|
caption = args.delete(:caption)
concat render(Primer::Beta::Text.new(font_weight: :semibold, **header_args(name))) { caption }
concat render(
Primer::Beta::Text.new(
classes: header_classes(name),
font_weight: :semibold,
**header_args(name)
)
) { args[:caption] }
end
concat render(
Primer::Beta::Text.new(
classes: "op-border-box-grid--mobile-heading",
font_weight: :semibold
)
) { mobile_title }
if has_actions?
concat render(Primer::BaseComponent.new(tag: :div))
concat render(
Primer::BaseComponent.new(
classes: heading_class,
tag: :div
)
)
end
end
@@ -53,6 +71,12 @@ See COPYRIGHT and LICENSE files for more details.
end
end
end
if has_footer?
component.with_footer(classes: grid_class, color: :muted) do
footer
end
end
end
%>
@@ -32,10 +32,71 @@ module OpPrimer
class BorderBoxTableComponent < TableComponent
include ComponentHelpers
class << self
# Declares columns to be shown in the mobile table
#
# Use it in subclasses like so:
#
# columns :name, :description
#
# mobile_columns :name
#
# This results in the description columns to be hidden on mobile
def mobile_columns(*names)
return Array(@mobile_columns || columns) if names.empty?
@mobile_columns = names.map(&:to_sym)
end
# Declares which columns to be rendered with a label
#
# mobile_labels :name
#
# This results in the description columns to be hidden on mobile
def mobile_labels(*names)
return Array(@mobile_labels) if names.empty?
@mobile_labels = names.map(&:to_sym)
end
# Declare main columns, that will result in a grid column span of 2 and not truncate text
#
# column_grid_span :title
#
def main_column(*names)
return Array(@main_columns) if names.empty?
@main_columns = names.map(&:to_sym)
end
end
delegate :mobile_columns, :mobile_labels,
to: :class
def main_column?(column)
self.class.main_column.include?(column)
end
def header_args(_column)
{}
end
def column_title(name)
header = headers.find { |h| h[0] == name }
header ? header[1][:caption] : nil
end
def header_classes(column)
classes = [heading_class]
classes << "op-border-box-grid--main-column" if main_column?(column)
classes.join(" ")
end
def heading_class
"op-border-box-grid--heading"
end
# Default grid class with equal weights
def grid_class
"op-border-box-grid"
@@ -45,6 +106,10 @@ module OpPrimer
false
end
def has_footer?
false
end
def sortable?
false
end
@@ -57,6 +122,10 @@ module OpPrimer
end
end
def mobile_title
raise ArgumentError, "Need to provide a mobile table title"
end
def blank_title
I18n.t(:label_nothing_display)
end
@@ -68,5 +137,9 @@ module OpPrimer
def blank_icon
nil
end
def footer
raise ArgumentError, "Need to provide footer content"
end
end
end
@@ -1,7 +1,47 @@
@import "helpers"
.op-border-box-grid
display: grid
justify-content: flex-start
align-items: center
// Distribute columns evenly by default
grid-auto-columns: minmax(0, 1fr)
grid-auto-flow: column
&--row-action
align-items: center
justify-content: flex-end
@media screen and (min-width: $breakpoint-md)
.op-border-box-grid
// Distribute columns evenly on desktop
grid-auto-columns: minmax(0, 1fr)
grid-auto-flow: column
justify-content: flex-start
align-items: center
&--heading,
&--row-item
&:not(:first-child)
padding-left: 6px
&:not(:last-child)
padding-right: 6px
&--mobile-heading,
&--row-label
display: none
&--main-column
grid-column: span 2
@media screen and (max-width: $breakpoint-md)
.op-border-box-grid
grid-template-columns: 1fr auto
grid-auto-flow: row
&--heading,
&--no-mobile
display: none
&--row-item
grid-column: 1
&--row-action
grid-column: 2
grid-row: 1
@@ -2,12 +2,14 @@
flex_layout(align_items: :center, **@system_arguments) do |flex|
if @scheme == :link
flex.with_column(classes: "ellipsis") do
render(Primer::Beta::Link.new(
id: @id,
href: value,
title: value,
target: :_blank
)) { value }
render(
Primer::Beta::Link.new(
id: @id,
href: value,
title: value,
target: :_blank
)
) { value }
end
else
flex.with_column(classes: "ellipsis") do
@@ -19,19 +19,23 @@
hidden_user_list.with_row(mt: 1) { item.to_s }
end
hidden_user_list.with_row(mt: 2) do
render(Primer::Beta::Button.new(
scheme: :link,
data: { action: "expandable-list#hideElements" }
)) { I18n.t("label_show_less") }
render(
Primer::Beta::Button.new(
scheme: :link,
data: { action: "expandable-list#hideElements" }
)
) { I18n.t("label_show_less") }
end
end
end
list.with_row(mt: 2) do
render(Primer::Beta::Button.new(
scheme: :link,
data: { "expandable-list-target": "showButton",
action: "expandable-list#showElements" }
)) { I18n.t("label_show_more") }
render(
Primer::Beta::Button.new(
scheme: :link,
data: { "expandable-list-target": "showButton",
action: "expandable-list#showElements" }
)
) { I18n.t("label_show_more") }
end
end
end
@@ -0,0 +1,44 @@
<%#-- 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::BaseComponent.new(
tag: :div,
classes: "op-primer-flash--item",
data: {
"flash-target": "item",
autohide: @autohide,
unique_key: @unique_key
}.compact
)
) do
render_parent
end
%>
@@ -38,6 +38,8 @@ module OpPrimer
system_arguments[:test_selector] ||= "op-primer-flash-message"
system_arguments[:dismiss_scheme] ||= :remove
system_arguments[:dismiss_label] ||= I18n.t(:button_close)
system_arguments[:data] ||= {}
system_arguments[:data]["flash-target"] = "flash"
@autohide = system_arguments[:scheme] == :success && system_arguments[:dismiss_scheme] != :none
@@ -0,0 +1,31 @@
<%=
if @readonly || @items.empty?
render(Primer::Beta::Button.new(**button_arguments)) do |button|
button_content(button)
end
else
render(Primer::Alpha::ActionMenu.new(**@menu_arguments)) do |menu|
menu.with_show_button(**button_arguments) do |button|
button_content(button)
end
@items.each do |option|
menu.with_item(**option.item_arguments) do |item|
item.with_leading_visual_icon(icon: option.icon) if option.icon
flex_layout(align_items: :center) do |flex|
if option.icon.nil? && option.color
flex.with_column(mr: 1) do
helpers.icon_for_color(option.color, style: "display: block")
end
end
flex.with_column do
render(Primer::Beta::Text.new) { option.name }
end
end
end
end
end
end
%>
@@ -0,0 +1,78 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpPrimer
class StatusButtonComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def initialize(current_status:, items:, readonly: false, disabled: false, button_arguments: {}, menu_arguments: {})
super
@current_status = current_status
@items = items
@readonly = readonly
@disabled = disabled
@menu_arguments = menu_arguments
@button_arguments = button_arguments
end
def default_button_title
raise NotImplementedError
end
def disabled?
@disabled
end
def readonly?
@readonly
end
def button_content(button)
button.with_leading_visual_icon(icon: @current_status.icon) if @current_status.icon
button.with_trailing_action_icon(icon: "triangle-down") if !readonly? && @items.any?
@current_status.name
end
def button_arguments
title = @button_arguments.fetch(:title) { default_button_title }
{
title:,
disabled: disabled?,
aria: {
label: title
},
style: @current_status.color&.color_styles_css
}.compact.deep_merge(@button_arguments)
end
end
end
@@ -0,0 +1,46 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module OpPrimer
class StatusButtonOption # rubocop:disable OpenProject/AddPreviewForViewComponent
attr_reader :name, :color, :icon, :item_arguments
def initialize(name:, color: nil, icon: nil, **item_arguments)
@name = name
@color = color
@icon = icon
@item_arguments = item_arguments
end
def to_s
name
end
end
end
@@ -27,11 +27,11 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
<% if options[:lazy] == true %>
<turbo-frame id="<%=turbo_frame_id%>" src="<%= options[:lazy_source] %>" loading="lazy">
<turbo-frame id="<%= turbo_frame_id %>" src="<%= options[:lazy_source] %>" loading="lazy">
<%= content %>
</turbo-frame>
<% else %>
<turbo-frame id="<%=turbo_frame_id%>">
<turbo-frame id="<%= turbo_frame_id %>">
<%= content %>
</turbo-frame>
<% end %>
@@ -5,44 +5,51 @@
class="op-long-text-attribute">
<%= render(
Primer::Beta::Text.new(tag: :div,
classes: ['op-long-text-attribute--text', PARAGRAPH_CSS_CLASS],
color: text_color,
style: "max-height: #{max_height};",
data: {
'attribute-target': "descriptionText"
})) { short_text }
%>
Primer::Beta::Text.new(
tag: :div,
classes: ["op-long-text-attribute--text", PARAGRAPH_CSS_CLASS],
color: text_color,
style: "max-height: #{max_height};",
data: {
"attribute-target": "descriptionText"
}
)
) { short_text } %>
<%= render(
Primer::Beta::Text.new(tag: :div,
display: display_expand_button_value,
classes: 'op-long-text-attribute--text-hider',
data: { 'attribute-target': 'textHider' }))
%>
Primer::Beta::Text.new(
tag: :div,
display: display_expand_button_value,
classes: "op-long-text-attribute--text-hider",
data: { "attribute-target": "textHider" }
)
) %>
<%= render(
Primer::Alpha::HiddenTextExpander.new(inline: false,
"aria-label": I18n.t('label_attribute_expand_text', attribute: name),
display: display_expand_button_value,
data: {
'attribute-target': 'expandButton',
'test-selector': 'expand-button'
},
button_arguments: { 'data-show-dialog-id': id },
classes: 'op-long-text-attribute--text-expander'
))
%>
Primer::Alpha::HiddenTextExpander.new(
inline: false,
"aria-label": I18n.t("label_attribute_expand_text", attribute: name),
display: display_expand_button_value,
data: {
"attribute-target": "expandButton",
"test-selector": "expand-button"
},
button_arguments: { "data-show-dialog-id": id },
classes: "op-long-text-attribute--text-expander"
)
) %>
<%= render(
Primer::Alpha::Dialog.new(id: id,
data: {
'test-selector': 'attribute-dialog'
},
title: name,
size: :large)) do |component|
component.with_body(mt: 2) { full_text }
component.with_header(variant: :large)
end
%>
Primer::Alpha::Dialog.new(
id: id,
data: {
"test-selector": "attribute-dialog"
},
title: name,
size: :large
)
) do |component|
component.with_body { full_text }
component.with_header(variant: :large)
end %>
</div>
@@ -1,26 +1,30 @@
<div class="op-submenu" data-controller="filter--filter-list" data-application-target="dynamic" data-test-selector="op-submenu">
<% if @searchable %>
<div class="op-submenu--search">
<%= render Primer::Alpha::TextField.new(name: "search",
label: I18n.t("label_search"),
placeholder: I18n.t("label_search_by_name"),
leading_visual: { icon: :search },
visually_hide_label: true,
classes: "op-submenu--search-input",
data: {
action: "input->filter--filter-list#filterLists",
"filter--filter-list-target": "filter",
"test-selector": "op-submenu--search-input"
},) %>
<%= render Primer::Alpha::TextField.new(
name: "search",
label: I18n.t("label_search"),
placeholder: I18n.t("label_search_by_name"),
leading_visual: { icon: :search },
visually_hide_label: true,
classes: "op-submenu--search-input",
data: {
action: "input->filter--filter-list#filterLists",
"filter--filter-list-target": "filter",
"test-selector": "op-submenu--search-input"
}
) %>
<%= render Primer::Beta::Text.new(display: :none,
classes: "op-submenu--search-no-results-container",
data: {
"test-selector": "op-submenu--search-no-results",
"filter--filter-list-target": "noResultsText",
}) do
I18n.t("js.autocompleter.notFoundText")
end %>
<%= render Primer::Beta::Text.new(
display: :none,
classes: "op-submenu--search-no-results-container",
data: {
"test-selector": "op-submenu--search-no-results",
"filter--filter-list-target": "noResultsText"
}
) do
I18n.t("js.autocompleter.notFoundText")
end %>
</div>
<% end %>
@@ -29,7 +33,7 @@
<ul class="op-submenu--items">
<% top_level_sidebar_menu_items.first.children.each do |menu_item| %>
<li class="op-submenu--item" data-filter--filter-list-target="searchItem">
<% selected = menu_item.selected ? 'selected' : '' %>
<% selected = menu_item.selected ? "selected" : "" %>
<a class="op-submenu--item-action <%= selected %>" href="<%= menu_item.href %>" data-test-selector="op-submenu--item-action">
<% if menu_item.icon %>
<%= render Primer::Beta::Octicon.new(icon: menu_item.icon, classes: "op-submenu--item-icon") %>
@@ -69,7 +73,7 @@
data-menus--submenu-target="container">
<% menu_item.children.each do |child_item| %>
<li class="op-submenu--item" data-filter--filter-list-target="searchItem">
<% selected = child_item.selected ? 'selected' : '' %>
<% selected = child_item.selected ? "selected" : "" %>
<a class="op-submenu--item-action <%= selected %>" href="<%= child_item.href %>" data-test-selector="op-submenu--item-action">
<% if child_item.icon %>
<%= render Primer::Beta::Octicon.new(icon: child_item.icon, classes: "op-submenu--item-icon") %>
@@ -99,18 +103,16 @@
<% if @create_btn_options.present? %>
<div class="op-submenu--footer">
<%= render Primer::Beta::Button.new(scheme: :primary,
tag: :a,
href: @create_btn_options[:href],
test_selector: "#{@create_btn_options[:module_key]}--create-button",
classes: "op-submenu--footer-action") do |button|
button.with_leading_visual_icon(icon: "plus")
if @create_btn_options[:btn_text].present?
@create_btn_options[:btn_text]
else
I18n.t("label_#{@create_btn_options[:module_key]}")
end
end %>
<%= render Primer::Beta::Button.new(
scheme: :primary,
tag: :a,
href: @create_btn_options[:href],
test_selector: "#{@create_btn_options[:module_key]}--create-button",
classes: "op-submenu--footer-action"
) do |button|
button.with_leading_visual_icon(icon: "plus")
@create_btn_options[:btn_text].presence || I18n.t("label_#{@create_btn_options[:module_key]}")
end %>
</div>
<% end %>
</div>
@@ -33,38 +33,44 @@ See COPYRIGHT and LICENSE files for more details.
header.with_breadcrumbs(breadcrumb_items)
unless new_record?
header.with_action_button(tag: :a,
mobile_icon: :person,
mobile_label: t(:label_profile),
size: :medium,
href: placeholder_user_path(@placeholder_user),
aria: { label: I18n.t(:label_profile) },
title: I18n.t(:label_profile)) do |button|
header.with_action_button(
tag: :a,
mobile_icon: :person,
mobile_label: t(:label_profile),
size: :medium,
href: placeholder_user_path(@placeholder_user),
aria: { label: I18n.t(:label_profile) },
title: I18n.t(:label_profile)
) do |button|
button.with_leading_visual_icon(icon: :person)
t(:label_profile)
end
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
disabled: !deletable?,
href: delete_button_href,
aria: { label: I18n.t(:button_delete) },
data: { "test-selector": "placeholder-user--delete-button" },
title: delete_button_title) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
disabled: !deletable?,
href: delete_button_href,
aria: { label: I18n.t(:button_delete) },
data: { "test-selector": "placeholder-user--delete-button" },
title: delete_button_title
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
end
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: selected_tab(@tabs) == tab, href: tab[:path]) do |t|
t.with_text { I18n.t("js.#{tab[:label]}") }
if @tabs.present?
header.with_tab_nav(label: nil) do |tab_nav|
@tabs.each do |tab|
tab_nav.with_tab(selected: selected_tab(@tabs) == tab, href: tab[:path]) do |t|
t.with_text { I18n.t("js.#{tab[:label]}") }
end
end
end
end if @tabs.present?
end
end
%>
@@ -32,29 +32,33 @@ See COPYRIGHT and LICENSE files for more details.
header.with_breadcrumbs(breadcrumb_items)
if @current_user.allowed_globally?(:manage_placeholder_user)
header.with_action_button(tag: :a,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
accesskey: accesskey(:edit),
href: edit_placeholder_user_path(@placeholder_user),
aria: { label: I18n.t(:button_edit) },
title: I18n.t(:button_edit)) do |button|
header.with_action_button(
tag: :a,
mobile_icon: :pencil,
mobile_label: t(:button_edit),
size: :medium,
accesskey: accesskey(:edit),
href: edit_placeholder_user_path(@placeholder_user),
aria: { label: I18n.t(:button_edit) },
title: I18n.t(:button_edit)
) do |button|
button.with_leading_visual_icon(icon: :pencil)
t(:button_edit)
end
end
header.with_action_button(tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
disabled: !deletable?,
href: delete_button_href,
aria: { label: I18n.t(:button_delete) },
data: { "test-selector": "placeholder-user--delete-button" },
title: delete_button_title) do |button|
header.with_action_button(
tag: :a,
scheme: :danger,
mobile_icon: :trash,
mobile_label: t(:button_delete),
size: :medium,
disabled: !deletable?,
href: delete_button_href,
aria: { label: I18n.t(:button_delete) },
data: { "test-selector": "placeholder-user--delete-button" },
title: delete_button_title
) do |button|
button.with_leading_visual_icon(icon: :trash)
t(:button_delete)
end
@@ -37,7 +37,7 @@ module Projects
# generated in the context of the component index action, instead of any turbo stream actions performing
# partial updates on the page.
#
# params[:url_for_action] is passed to the pagination_options making it's way down to any pagination links
# params[:url_for_action] is passed to the pagination_options making its way down to any pagination links
# that are generated via link_to which calls url_for which uses the params[:url_for_action] to specify
# the controller action that link_to should use.
#
@@ -1,11 +1,15 @@
<%= render(Primer::Alpha::Dialog.new(title: t(:'queries.configure_view.heading'),
size: :large,
id: MODAL_ID,
# Hack to give the draggable autcompleter (ng-select) bound to the dialog
# enough height to display all options.
# This is necessary as long as ng-select does not support popovers.
style: "min-height: 480px")) do |d| %>
<% d.with_header(variant: :large, mb: 3) %>
<%= render(
Primer::Alpha::Dialog.new(
title: t(:"queries.configure_view.heading"),
size: :large,
id: MODAL_ID,
# Hack to give the draggable autcompleter (ng-select) bound to the dialog
# enough height to display all options.
# This is necessary as long as ng-select does not support popovers.
style: "min-height: 480px"
)
) do |d| %>
<% d.with_header(variant: :large) %>
<%= render(Primer::Alpha::Dialog::Body.new) do %>
<%= primer_form_with(
url: projects_path,
@@ -14,42 +18,46 @@
method: :get
) do |form| %>
<% helpers.projects_query_params.except(:columns, :sortBy).each do |name, value| %>
<%= hidden_field_tag name, value, data: {"sort-by-config-target" => name} %>
<%= hidden_field_tag name, value, data: { "sort-by-config-target" => name } %>
<% end %>
<%= render(Primer::Alpha::TabPanels.new(label: "label")) do |tab_panel| %>
<% tab_panel.with_tab(selected: true, id: "tab-selects--columns") do |tab| %>
<% tab.with_text { I18n.t("label_columns") } %>
<% tab.with_panel do %>
<%= helpers.angular_component_tag 'opce-draggable-autocompleter',
inputs: {
options: helpers.projects_columns_options,
selected: selected_columns,
protected: helpers.protected_projects_columns_options,
name: COLUMN_HTML_NAME,
id: COLUMN_HTML_ID,
dragAreaName: "#{COLUMN_HTML_ID}_dragarea",
formControlId: "#{COLUMN_HTML_ID}_autocompleter",
inputLabel: I18n.t(:'queries.configure_view.columns.input_label'),
inputPlaceholder: I18n.t(:'queries.configure_view.columns.input_placeholder'),
dragAreaLabel: I18n.t(:'queries.configure_view.columns.drag_area_label'),
appendToComponent: true
}%>
<%= helpers.angular_component_tag "opce-draggable-autocompleter",
inputs: {
options: helpers.projects_columns_options,
selected: selected_columns,
protected: helpers.protected_projects_columns_options,
name: COLUMN_HTML_NAME,
id: COLUMN_HTML_ID,
dragAreaName: "#{COLUMN_HTML_ID}_dragarea",
formControlId: "#{COLUMN_HTML_ID}_autocompleter",
inputLabel: I18n.t(:"queries.configure_view.columns.input_label"),
inputPlaceholder: I18n.t(:"queries.configure_view.columns.input_placeholder"),
dragAreaLabel: I18n.t(:"queries.configure_view.columns.drag_area_label"),
appendToComponent: true
} %>
<% end %>
<% end %>
<% tab_panel.with_tab(id: "tab-selects--sorting") do |tab| %>
<% tab.with_text { I18n.t("label_sort") }%>
<% tab.with_text { I18n.t("label_sort") } %>
<% tab.with_panel do %>
<%= render(Queries::SortByComponent.new(query: query, selectable_columns:)) %>
<% end%>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= render(Primer::Alpha::Dialog::Footer.new) do %>
<%= render(Primer::ButtonComponent.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= render(Primer::ButtonComponent.new(scheme: :primary,
type: :submit,
data: { "test-selector": "#{MODAL_ID}-submit"},
form: QUERY_FORM_ID)) { I18n.t(:button_apply) } %>
<%= render(
Primer::ButtonComponent.new(
scheme: :primary,
type: :submit,
data: { "test-selector": "#{MODAL_ID}-submit" },
form: QUERY_FORM_ID
)
) { I18n.t(:button_apply) } %>
<% end %>
<% end %>
@@ -1,15 +1,14 @@
<%= render(Primer::Alpha::Dialog.new(title: t(:'projects.lists.delete_modal.title'),
size: :large,
id: MODAL_ID,
data: { 'test-selector': MODAL_ID })) do |d| %>
<% d.with_header(variant: :large, mb: 2) %>
<% d.with_body { t(:'projects.lists.delete_modal.text') } %>
<% d.with_footer do %>
<%= render(Primer::Beta::Button.new(data: { "close-dialog-id": MODAL_ID })) { I18n.t(:button_cancel) } %>
<%= form_with(url: project_query_path(query),
method: :delete) do %>
<%= render(Primer::Beta::Button.new(scheme: :danger,
type: :submit)) { I18n.t(:button_delete) } %>
<% end %>
<% end %>
<% end %>
<%=
render(Primer::OpenProject::DangerDialog.new(title: t(:"projects.lists.delete_modal.title"),
form_arguments: {
action: project_query_path(query),
method: :delete
},
id: MODAL_ID,
test_selector: MODAL_ID)) do |d|
d.with_confirmation_message do |message|
message.with_heading(tag: :h2) { t(:"projects.lists.delete_modal.heading") }
message.with_description_content(t(:"projects.lists.delete_modal.text"))
end
end
%>
@@ -1,6 +1,8 @@
<p class="information-section">
<%= helpers.op_icon('icon-info1') %>
<%= t(:label_projects_disk_usage_information,
<%= helpers.op_icon("icon-info1") %>
<%= t(
:label_projects_disk_usage_information,
count: Project.count,
used_disk_space: number_to_human_size(Project.total_projects_size, precision: 2)) %>
used_disk_space: number_to_human_size(Project.total_projects_size, precision: 2)
) %>
</p>
@@ -1,12 +1,16 @@
<%= render(Primer::Alpha::Dialog.new(title: t('js.label_export'),
id: MODAL_ID)) do |d| %>
<%= 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)),
class: 'op-export-options--option-link' do %>
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 %>
@@ -1,16 +1,18 @@
<%= component_wrapper(tag: 'turbo-frame') do %>
<%= component_wrapper(tag: "turbo-frame") do %>
<%=
render(Primer::OpenProject::PageHeader.new(state: @state)) do |header|
header.with_title(data: { 'test-selector': 'project-query-name' }) do |title|
title.with_editable_form(model: query,
update_path: @query.new_record? ? project_queries_path(projects_query_params) : project_query_path(@query, projects_query_params),
method: @query.new_record? ? :post : :put,
cancel_path: projects_path(**projects_query_params, **{ query_id: query.id }.compact),
input_name: "name",
label: ProjectQuery.human_attribute_name(:name),
placeholder: I18n.t(:"projects.lists.new.placeholder"),
scope: 'query',
id: 'project-save-form')
header.with_title(data: { "test-selector": "project-query-name" }) do |title|
title.with_editable_form(
model: query,
update_path: @query.new_record? ? project_queries_path(projects_query_params) : project_query_path(@query, projects_query_params),
method: @query.new_record? ? :post : :put,
cancel_path: projects_path(**projects_query_params, **{ query_id: query.id }.compact),
input_name: "name",
label: ProjectQuery.human_attribute_name(:name),
placeholder: I18n.t(:"projects.lists.new.placeholder"),
scope: "query",
id: "project-save-form"
)
page_title
end
header.with_breadcrumbs(breadcrumb_items, selected_item_font_weight: current_breadcrumb_element == page_title ? :bold : :normal)
@@ -52,7 +54,7 @@
classes: "op-primer--star-icon",
tag: :a,
href: helpers.build_favorite_path(query, format: :html),
data: { method: :delete, "test-selector": "project-query-unfavorite" },
data: { method: :delete, "test-selector": "project-query-unfavorite" }
)
else
header.with_action_icon_button(
@@ -61,7 +63,7 @@
label: t(:button_favorite),
tag: :a,
href: helpers.build_favorite_path(query, format: :html),
data: { method: :post, "test-selector": "project-query-favorite" },
data: { method: :post, "test-selector": "project-query-favorite" }
)
end
end
@@ -78,8 +80,8 @@
) do |menu|
if can_rename?
menu.with_item(
label: t('button_rename'),
href: rename_project_query_path(query),
label: t("button_rename"),
href: rename_project_query_path(query)
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
@@ -88,12 +90,12 @@
if gantt_portfolio_project_ids.any?
menu.with_item(
tag: :a,
label: t('projects.index.open_as_gantt'),
label: t("projects.index.open_as_gantt"),
href: gantt_portfolio_query_link,
id: 'projects-index-open-as-gantt',
content_arguments: { target: '_blank' }
id: "projects-index-open-as-gantt",
content_arguments: { target: "_blank" }
) do |item|
item.with_leading_visual_icon(icon: 'op-view-timeline')
item.with_leading_visual_icon(icon: "op-view-timeline")
end
end
@@ -103,13 +105,13 @@
href: activities_path,
content_arguments: { rel: "nofollow" }
) do |item|
item.with_leading_visual_icon(icon: 'tasklist')
item.with_leading_visual_icon(icon: "tasklist")
end
if can_save?
menu_save_item(
menu:,
label: t('button_save'),
label: t("button_save"),
href: project_query_path(query, projects_query_params),
method: :patch
)
@@ -118,25 +120,25 @@
if may_save_as?
menu_save_item(
menu:,
label: t('button_save_as'),
label: t("button_save_as"),
href: new_project_query_path(projects_query_params)
)
end
menu.with_item(
tag: :a,
label: t('js.label_export'),
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: 'sign-out')
item.with_leading_visual_icon(icon: "sign-out")
end
menu.with_item(
tag: :a,
label: t(:'queries.configure_view.heading'),
label: t(:"queries.configure_view.heading"),
href: configure_view_modal_project_queries_path(projects_query_params),
content_arguments: { data: { controller: "async-dialog" }}
content_arguments: { data: { controller: "async-dialog" } }
) do |item|
item.with_leading_visual_icon(icon: :gear)
end
@@ -147,18 +149,17 @@
label: t(:button_delete),
scheme: :danger,
href: destroy_confirmation_modal_project_query_path(query.id),
content_arguments: { data: { controller: "async-dialog" }}
content_arguments: { data: { controller: "async-dialog" } }
) do |item|
item.with_leading_visual_icon(icon: 'trash')
item.with_leading_visual_icon(icon: "trash")
end
end
menu.with_item(
tag: :button,
label: t('label_zen_mode'),
content_arguments: { data: { controller: "projects-zen-mode", target: "projects-zen-mode.button", action: "click->projects-zen-mode#performAction" }
}
label: t("label_zen_mode"),
content_arguments: { data: { controller: "projects-zen-mode", target: "projects-zen-mode.button", action: "click->projects-zen-mode#performAction" } }
) do |item|
item.with_leading_visual_icon(icon: 'screen-full')
item.with_leading_visual_icon(icon: "screen-full")
end
end
end
@@ -122,7 +122,7 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
return page_title if query.name.blank?
if current_section && current_section.header.present?
I18n.t("menus.breadcrumb.nested_element", section_header: current_section.header, title: query.name).html_safe
helpers.nested_breadcrumb_element(current_section.header, query.name)
else
page_title
end
@@ -1,33 +1,39 @@
<%= component_wrapper(tag: 'turbo-frame') do %>
<%= component_wrapper(tag: "turbo-frame") do %>
<%= render(Primer::OpenProject::SubHeader.new(data: sub_header_data_attributes)) do |subheader|
subheader.with_filter_input(name: "name_and_identifier",
label: t('projects.index.search.label'),
value: filter_input_value,
placeholder: t('projects.index.search.placeholder'),
leading_visual: {
icon: :search,
size: :small
},
clear_button_id: clear_button_id,
data: filter_input_data_attributes)
subheader.with_filter_input(
name: "name_and_identifier",
label: t("projects.index.search.label"),
value: filter_input_value,
placeholder: t("projects.index.search.placeholder"),
leading_visual: {
icon: :search,
size: :small
},
clear_button_id: clear_button_id,
data: filter_input_data_attributes
)
subheader.with_filter_component do
render(Projects::ProjectFilterButtonComponent.new(query: @query, disabled: @disable_buttons))
end
subheader.with_filter_component do
render(Projects::ProjectFilterButtonComponent.new(query: @query, disabled: @disable_buttons))
end
subheader.with_action_button(tag: :a,
href: new_project_path,
scheme: :primary,
disabled: @disable_buttons,
size: :medium,
aria: { label: I18n.t(:label_project_new) },
data: { 'test-selector': 'project-new-button' }) do |button|
button.with_leading_visual_icon(icon: :plus)
Project.model_name.human
end if @current_user.allowed_globally?(:add_project)
if @current_user.allowed_globally?(:add_project)
subheader.with_action_button(
tag: :a,
href: new_project_path,
scheme: :primary,
disabled: @disable_buttons,
size: :medium,
aria: { label: I18n.t(:label_project_new) },
data: { "test-selector": "project-new-button" }
) do |button|
button.with_leading_visual_icon(icon: :plus)
Project.model_name.human
end
end
subheader.with_bottom_pane_component(mt: 0) do
render(Projects::ProjectsFiltersComponent.new(query: @query))
end
end %>
subheader.with_bottom_pane_component(mt: 0) do
render(Projects::ProjectsFiltersComponent.new(query: @query))
end
end %>
<% end %>
@@ -0,0 +1,8 @@
<%= flex_layout(align_items: :center) do |type_container|
type_container.with_column(mr: 1, classes: icon_color_class) do
render Primer::Beta::Octicon.new(icon: icon)
end
type_container.with_column do
render(Primer::Beta::Text.new(**text_options)) { text }
end
end %>
@@ -0,0 +1,60 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
# ++
module Projects
class LifeCycleTypeComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
def text
model.model_name.human
end
def icon
case model
when Project::StageDefinition
:"git-commit"
when Project::GateDefinition
:diamond
else
raise NotImplementedError, "Unknown model #{model.class} to render a LifeCycleTypeComponent with"
end
end
def icon_color_class
helpers.hl_inline_class("life_cycle_step_definition", model)
end
def text_options
# The tag: :div is is a hack to fix the line height difference
# caused by font_size: :small. That line height difference
# would otherwise lead to the text being not on the same height as the icon
{ color: :muted, font_size: :small, tag: :div }.merge(options)
end
end
end
@@ -29,8 +29,7 @@ See COPYRIGHT and LICENSE files for more details.
<tr
<%= "id=\"#{row_css_id}\"".html_safe if row_css_id %>
<%= "class=\"#{row_css_class}\"".html_safe if row_css_class %>
>
<%= "class=\"#{row_css_class}\"".html_safe if row_css_class %>>
<% columns.each do |column| %>
<td class="<%= column_css_class(column) %>">
<%= column_value(column) %>
+39 -1
View File
@@ -29,8 +29,9 @@
#++
module Projects
class RowComponent < ::RowComponent
delegate :favored_project_ids, to: :table
delegate :identifier, to: :project
delegate :favored_project_ids, to: :table
delegate :project_life_cycle_step_by_definition, to: :table
def project
model.first
@@ -69,6 +70,8 @@ module Projects
def column_value(column)
if custom_field_column?(column)
custom_field_column(column)
elsif life_cycle_step_column?(column)
life_cycle_step_column(column)
else
send(column.attribute)
end
@@ -94,6 +97,16 @@ module Projects
end
end
def life_cycle_step_column(column)
return nil unless user_can_view_project_stages_and_gates?
life_cycle_step = project_life_cycle_step_by_definition(column.life_cycle_step_definition, project)
return nil if life_cycle_step.blank?
fmt_date_or_range(life_cycle_step.start_date, life_cycle_step.end_date)
end
def created_at
helpers.format_date(project.created_at)
end
@@ -370,12 +383,37 @@ module Projects
User.current.allowed_in_project?(:view_project_attributes, project)
end
def user_can_view_project_stages_and_gates?
User.current.allowed_in_project?(:view_project_stages_and_gates, project)
end
def custom_field_column?(column)
column.is_a?(::Queries::Projects::Selects::CustomField)
end
def life_cycle_step_column?(column)
column.is_a?(::Queries::Projects::Selects::LifeCycleStep)
end
def current_page
table.model.current_page.to_s
end
private
# If only the `start_date` is given, will return a formatted version of that date as string.
# When `end_date` is given as well, will return a representation of the date range from start to end.
# @example
# fmt_date_or_range(Date.new(2024, 12, 4))
# "04/12/2024"
#
# fmt_date_or_range(Date.new(2024, 12, 4), Date.new(2024, 12, 10))
# "04/12/2024 - 10/12/2024"
def fmt_date_or_range(start_date, end_date = nil)
[start_date, end_date]
.compact
.map { |d| helpers.format_date(d) }
.join(" - ")
end
end
end

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