mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Merge remote-tracking branch 'origin/dev' into rails-7.2
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- release/*
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'config/static_links.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -134,6 +134,8 @@ structure.sql
|
||||
lefthook-local.yml
|
||||
.rubocop-local.yml
|
||||
|
||||
/.lefthook-local/
|
||||
|
||||
frontend/package-lock.json
|
||||
|
||||
# Testing and nextcloud infrastructure
|
||||
|
||||
@@ -21,7 +21,7 @@ targets:
|
||||
<<: *debian
|
||||
ubuntu-22.04:
|
||||
<<: *debian
|
||||
centos-8: ¢os8
|
||||
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
@@ -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
@@ -1 +1 @@
|
||||
3.3.6
|
||||
3.4.2
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+17
-11
@@ -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
|
||||
%>
|
||||
|
||||
+12
-21
@@ -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
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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) %>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user