summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/actions/setup-redmine/action.yml68
-rw-r--r--.github/workflows/tests.yml87
-rw-r--r--.rubocop.yml35
-rw-r--r--.rubocop_todo.yml66
-rw-r--r--Gemfile38
-rw-r--r--app/assets/images/chevron-down.svg1
-rw-r--r--app/assets/images/chevron-right-idnt.svg1
-rw-r--r--app/assets/images/hourglass-empty.svg1
-rw-r--r--app/assets/images/icons.svg41
-rw-r--r--app/assets/javascripts/application-legacy.js (renamed from app/assets/javascripts/application.js)131
-rw-r--r--app/assets/javascripts/attachments.js14
-rw-r--r--app/assets/javascripts/quote_reply.js44
-rw-r--r--app/assets/javascripts/turndown-7.2.0.min.js8
-rw-r--r--app/assets/stylesheets/application.css498
-rw-r--r--app/assets/stylesheets/responsive.css35
-rw-r--r--app/assets/stylesheets/rtl.css39
-rw-r--r--app/assets/stylesheets/wiki_syntax.css49
-rw-r--r--app/assets/stylesheets/wiki_syntax_detailed.css58
-rw-r--r--app/controllers/application_controller.rb8
-rw-r--r--app/controllers/auto_completes_controller.rb2
-rw-r--r--app/controllers/messages_controller.rb4
-rw-r--r--app/controllers/news_controller.rb4
-rw-r--r--app/controllers/oauth2_applications_controller.rb38
-rw-r--r--app/controllers/previews_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/controllers/reactions_controller.rb65
-rw-r--r--app/controllers/repositories_controller.rb10
-rw-r--r--app/controllers/roles_controller.rb10
-rw-r--r--app/controllers/versions_controller.rb4
-rw-r--r--app/controllers/wiki_controller.rb1
-rw-r--r--app/helpers/application_helper.rb24
-rw-r--r--app/helpers/avatars_helper.rb61
-rw-r--r--app/helpers/icons_helper.rb26
-rw-r--r--app/helpers/issues_helper.rb1
-rw-r--r--app/helpers/journals_helper.rb8
-rw-r--r--app/helpers/messages_helper.rb1
-rw-r--r--app/helpers/news_helper.rb1
-rw-r--r--app/helpers/principal_memberships_helper.rb18
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/queries_helper.rb2
-rw-r--r--app/helpers/reactions_helper.rb100
-rw-r--r--app/helpers/reports_helper.rb4
-rw-r--r--app/helpers/routes_helper.rb62
-rw-r--r--app/helpers/settings_helper.rb3
-rw-r--r--app/helpers/watchers_helper.rb4
-rw-r--r--app/javascript/application.js1
-rw-r--r--app/javascript/controllers/application.js8
-rw-r--r--app/javascript/controllers/index.js3
-rw-r--r--app/javascript/controllers/quote_reply_controller.js224
-rw-r--r--app/javascript/controllers/sticky_issue_header_controller.js22
-rw-r--r--app/models/comment.rb8
-rw-r--r--app/models/custom_field.rb7
-rw-r--r--app/models/email_address.rb6
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/issue.rb23
-rw-r--r--app/models/issue_relation.rb4
-rw-r--r--app/models/journal.rb5
-rw-r--r--app/models/mail_handler.rb4
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/message.rb2
-rw-r--r--app/models/news.rb2
-rw-r--r--app/models/principal.rb7
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/reaction.rb60
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/repository/bazaar.rb4
-rw-r--r--app/models/repository/cvs.rb4
-rw-r--r--app/models/repository/filesystem.rb4
-rw-r--r--app/models/repository/git.rb4
-rw-r--r--app/models/repository/mercurial.rb4
-rw-r--r--app/models/role.rb26
-rw-r--r--app/models/time_entry_query.rb19
-rw-r--r--app/models/user.rb50
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/version.rb10
-rw-r--r--app/views/activities/_activities.html.erb2
-rw-r--r--app/views/attachments/_form.html.erb9
-rw-r--r--app/views/attachments/other.html.erb1
-rw-r--r--app/views/calendars/show.html.erb2
-rw-r--r--app/views/context_menus/issues.html.erb18
-rw-r--r--app/views/context_menus/time_entries.html.erb4
-rw-r--r--app/views/custom_fields/formats/_progressbar.html.erb6
-rw-r--r--app/views/custom_fields/index.api.rsb1
-rw-r--r--app/views/doorkeeper/applications/_form.html.erb39
-rw-r--r--app/views/doorkeeper/applications/edit.html.erb6
-rw-r--r--app/views/doorkeeper/applications/index.html.erb33
-rw-r--r--app/views/doorkeeper/applications/new.html.erb6
-rw-r--r--app/views/doorkeeper/applications/show.html.erb54
-rw-r--r--app/views/doorkeeper/authorizations/error.html.erb6
-rw-r--r--app/views/doorkeeper/authorizations/new.html.erb48
-rw-r--r--app/views/doorkeeper/authorizations/show.html.erb8
-rw-r--r--app/views/doorkeeper/authorized_applications/index.html.erb31
-rw-r--r--app/views/gantts/show.html.erb8
-rw-r--r--app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb8
-rw-r--r--app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb52
-rw-r--r--app/views/imports/_issues_mapping.html.erb2
-rw-r--r--app/views/imports/_issues_relations_mapping.html.erb20
-rw-r--r--app/views/imports/show.html.erb4
-rw-r--r--app/views/issues/_list.html.erb4
-rw-r--r--app/views/issues/index.html.erb2
-rw-r--r--app/views/issues/show.html.erb32
-rw-r--r--app/views/issues/tabs/_changesets.html.erb44
-rw-r--r--app/views/issues/tabs/_history.html.erb55
-rw-r--r--app/views/issues/tabs/_time_entries.html.erb52
-rw-r--r--app/views/journals/update.js.erb4
-rw-r--r--app/views/layouts/base.html.erb13
-rw-r--r--app/views/messages/show.html.erb143
-rw-r--r--app/views/my/account.html.erb1
-rw-r--r--app/views/news/show.html.erb45
-rw-r--r--app/views/projects/index.html.erb2
-rw-r--r--app/views/queries/_filters.html.erb1
-rw-r--r--app/views/queries/_query_form.html.erb4
-rw-r--r--app/views/reactions/_replace_button.js.erb7
-rw-r--r--app/views/reactions/create.js.erb1
-rw-r--r--app/views/reactions/destroy.js.erb1
-rw-r--r--app/views/repositories/_breadcrumbs.html.erb2
-rw-r--r--app/views/repositories/_dir_list_content.html.erb2
-rw-r--r--app/views/roles/permissions.html.erb2
-rw-r--r--app/views/search/index.html.erb2
-rw-r--r--app/views/settings/_display.html.erb22
-rw-r--r--app/views/settings/_general.html.erb2
-rw-r--r--app/views/timelog/_list.html.erb4
-rw-r--r--app/views/timelog/index.html.erb2
-rw-r--r--app/views/users/show.api.rsb2
-rw-r--r--app/views/versions/_sidebar.html.erb2
-rw-r--r--app/views/versions/index.html.erb2
-rw-r--r--app/views/versions/show.html.erb2
-rw-r--r--app/views/wiki/date_index.html.erb2
-rw-r--r--app/views/wiki/index.html.erb8
-rw-r--r--app/views/wiki/show.html.erb2
-rw-r--r--app/views/workflows/edit.html.erb4
-rw-r--r--bin/importmap4
-rw-r--r--config/application.rb1
-rw-r--r--config/boot.rb16
-rw-r--r--config/database.yml.example4
-rw-r--r--config/environments/development.rb6
-rw-r--r--config/icon_source.yml16
-rw-r--r--config/importmap.rb9
-rw-r--r--config/initializers/30-redmine.rb78
-rw-r--r--config/initializers/doorkeeper.rb9
-rw-r--r--config/locales/ar.yml36
-rw-r--r--config/locales/az.yml37
-rw-r--r--config/locales/bg.yml42
-rw-r--r--config/locales/bs.yml36
-rw-r--r--config/locales/ca.yml36
-rw-r--r--config/locales/cs.yml65
-rw-r--r--config/locales/da.yml36
-rw-r--r--config/locales/de.yml27
-rw-r--r--config/locales/el.yml36
-rw-r--r--config/locales/en-GB.yml36
-rw-r--r--config/locales/en.yml29
-rw-r--r--config/locales/es-PA.yml35
-rw-r--r--config/locales/es.yml35
-rw-r--r--config/locales/et.yml36
-rw-r--r--config/locales/eu.yml36
-rw-r--r--config/locales/fa.yml108
-rw-r--r--config/locales/fi.yml33
-rw-r--r--config/locales/fr.yml33
-rw-r--r--config/locales/gl.yml53
-rw-r--r--config/locales/he.yml36
-rw-r--r--config/locales/hr.yml36
-rw-r--r--config/locales/hu.yml33
-rw-r--r--config/locales/id.yml36
-rw-r--r--config/locales/it.yml46
-rw-r--r--config/locales/ja.yml42
-rw-r--r--config/locales/ko.yml41
-rw-r--r--config/locales/lt.yml36
-rw-r--r--config/locales/lv.yml36
-rw-r--r--config/locales/mk.yml36
-rw-r--r--config/locales/mn.yml36
-rw-r--r--config/locales/nl.yml36
-rw-r--r--config/locales/no.yml35
-rw-r--r--config/locales/pl.yml36
-rw-r--r--config/locales/pt-BR.yml36
-rw-r--r--config/locales/pt.yml36
-rw-r--r--config/locales/ro.yml36
-rw-r--r--config/locales/ru.yml37
-rw-r--r--config/locales/sk.yml36
-rw-r--r--config/locales/sl.yml36
-rw-r--r--config/locales/sq.yml36
-rw-r--r--config/locales/sr-YU.yml38
-rw-r--r--config/locales/sr.yml36
-rw-r--r--config/locales/sv.yml707
-rw-r--r--config/locales/ta-IN.yml36
-rw-r--r--config/locales/th.yml36
-rw-r--r--config/locales/tr.yml36
-rw-r--r--config/locales/uk.yml36
-rw-r--r--config/locales/vi.yml31
-rw-r--r--config/locales/zh-TW.yml33
-rw-r--r--config/locales/zh.yml36
-rw-r--r--config/routes.rb7
-rw-r--r--config/settings.yml2
-rw-r--r--db/migrate/017_create_settings.rb1
-rw-r--r--db/migrate/20250423065135_create_reactions.rb11
-rw-r--r--db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb8
-rw-r--r--db/migrate/20250611092155_create_doorkeeper_tables.rb68
-rw-r--r--db/migrate/20250611092227_enable_pkce.rb8
-rw-r--r--doc/CHANGELOG155
-rw-r--r--doc/INSTALL20
-rw-r--r--doc/UPGRADING4
-rw-r--r--lib/plugins/gravatar/lib/gravatar.rb4
-rw-r--r--lib/redmine.rb5
-rw-r--r--lib/redmine/activity.rb15
-rw-r--r--lib/redmine/core_ext/string/conversions.rb2
-rw-r--r--lib/redmine/database.rb2
-rw-r--r--lib/redmine/diff.rb8
-rw-r--r--lib/redmine/field_format.rb29
-rw-r--r--lib/redmine/helpers/gantt.rb36
-rw-r--r--lib/redmine/i18n.rb25
-rw-r--r--lib/redmine/plugin.rb10
-rw-r--r--lib/redmine/preparation.rb9
-rw-r--r--lib/redmine/quote_reply.rb25
-rw-r--r--lib/redmine/reaction.rb70
-rw-r--r--lib/redmine/scm/adapters/mercurial_adapter.rb22
-rw-r--r--lib/redmine/sort_criteria.rb4
-rw-r--r--lib/redmine/subclass_factory.rb4
-rw-r--r--lib/redmine/syntax_highlighting.rb1
-rw-r--r--lib/redmine/version.rb2
-rw-r--r--lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb63
-rw-r--r--lib/redmine/wiki_formatting/common_mark/formatter.rb9
-rw-r--r--lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb70
-rw-r--r--lib/redmine/wiki_formatting/macros.rb2
-rw-r--r--lib/tasks/icons.rake2
-rw-r--r--test/application_system_test_case.rb9
-rw-r--r--test/fixtures/changesets.yml12
-rw-r--r--test/fixtures/reactions.yml51
-rw-r--r--test/functional/attachments_controller_test.rb6
-rw-r--r--test/functional/calendars_controller_test.rb2
-rw-r--r--test/functional/custom_fields_controller_test.rb17
-rw-r--r--test/functional/documents_controller_test.rb4
-rw-r--r--test/functional/gantts_controller_test.rb2
-rw-r--r--test/functional/issues_controller_test.rb97
-rw-r--r--test/functional/issues_custom_fields_visibility_test.rb4
-rw-r--r--test/functional/messages_controller_test.rb23
-rw-r--r--test/functional/news_controller_test.rb19
-rw-r--r--test/functional/reactions_controller_test.rb394
-rw-r--r--test/functional/repositories_bazaar_controller_test.rb1
-rw-r--r--test/functional/repositories_controller_test.rb2
-rw-r--r--test/functional/repositories_cvs_controller_test.rb1
-rw-r--r--test/functional/repositories_git_controller_test.rb1
-rw-r--r--test/functional/repositories_mercurial_controller_test.rb2
-rw-r--r--test/functional/repositories_subversion_controller_test.rb22
-rw-r--r--test/functional/roles_controller_test.rb32
-rw-r--r--test/functional/search_controller_test.rb10
-rw-r--r--test/functional/workflows_controller_test.rb39
-rw-r--r--test/generators/controller_generator_test.rb69
-rw-r--r--test/generators/model_generator_test.rb69
-rw-r--r--test/helpers/application_helper_test.rb62
-rw-r--r--test/helpers/avatars_helper_test.rb22
-rw-r--r--test/helpers/icons_helper_test.rb7
-rw-r--r--test/helpers/journals_helper_test.rb19
-rw-r--r--test/helpers/reactions_helper_test.rb216
-rw-r--r--test/integration/api_test/authentication_test.rb8
-rw-r--r--test/integration/api_test/custom_fields_test.rb2
-rw-r--r--test/integration/api_test/news_test.rb2
-rw-r--r--test/integration/issues_test.rb2
-rw-r--r--test/integration/repositories_git_test.rb1
-rw-r--r--test/system/copy_pre_content_to_clipboard_test.rb71
-rw-r--r--test/system/issues_reply_test.rb2
-rw-r--r--test/system/issues_test.rb11
-rw-r--r--test/system/messages_test.rb4
-rw-r--r--test/system/oauth_provider_test.rb137
-rw-r--r--test/system/reactions_test.rb173
-rw-r--r--test/system/sticky_issue_header_test.rb38
-rw-r--r--test/system/sudo_mode_test.rb3
-rw-r--r--test/system/timelog_test.rb3
-rw-r--r--test/unit/changeset_test.rb2
-rw-r--r--test/unit/email_address_test.rb6
-rw-r--r--test/unit/lib/redmine/field_format/progressbar_format_test.rb49
-rw-r--r--test/unit/lib/redmine/quote_reply_helper_test.rb14
-rw-r--r--test/unit/lib/redmine/reaction_test.rb189
-rw-r--r--test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb1
-rw-r--r--test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb1
-rw-r--r--test/unit/lib/redmine/scm/adapters/git_adapter_test.rb9
-rw-r--r--test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb8
-rw-r--r--test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb1
-rw-r--r--test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb129
-rw-r--r--test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb38
-rw-r--r--test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb20
-rw-r--r--test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb12
-rw-r--r--test/unit/member_test.rb6
-rw-r--r--test/unit/principal_test.rb4
-rw-r--r--test/unit/project_admin_query_test.rb1
-rw-r--r--test/unit/query_test.rb6
-rw-r--r--test/unit/reaction_test.rb118
-rw-r--r--test/unit/repository_bazaar_test.rb1
-rw-r--r--test/unit/repository_cvs_test.rb1
-rw-r--r--test/unit/repository_git_test.rb1
-rw-r--r--test/unit/repository_mercurial_test.rb1
-rw-r--r--test/unit/repository_subversion_test.rb1
-rw-r--r--test/unit/repository_test.rb10
-rw-r--r--test/unit/role_test.rb26
-rw-r--r--test/unit/setting_test.rb4
-rw-r--r--test/unit/user_test.rb94
-rw-r--r--vendor/javascript/turndown.js110
295 files changed, 7503 insertions, 1526 deletions
diff --git a/.github/actions/setup-redmine/action.yml b/.github/actions/setup-redmine/action.yml
new file mode 100644
index 000000000..d637914f0
--- /dev/null
+++ b/.github/actions/setup-redmine/action.yml
@@ -0,0 +1,68 @@
+name: Setup Redmine Test Environment
+description: Composite action for setting up Redmine test environment
+
+inputs:
+ db-type:
+ description: 'Database type: postgresql, mysql2, or sqlite3. Note: postgresql and mysql2 require service containers to be defined in the workflow.'
+ required: true
+ ruby-version:
+ description: 'Ruby version to use'
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Install dependencies and configure environment
+ shell: bash
+ run: |
+ sudo apt-get update
+ sudo apt-get install --yes --quiet ghostscript gsfonts locales bzr cvs
+ sudo locale-gen en_US # for bazaar non ascii test
+
+ - name: Allow imagemagick to read PDF files
+ shell: bash
+ run: |
+ echo '<policymap>' > policy.xml
+ echo '<policy domain="coder" rights="read | write" pattern="PDF" />' >> policy.xml
+ echo '</policymap>' >> policy.xml
+ sudo rm /etc/ImageMagick-6/policy.xml
+ sudo mv policy.xml /etc/ImageMagick-6/policy.xml
+
+ - if: ${{ inputs.db-type == 'sqlite3' }}
+ name: Prepare test database for sqlite3
+ shell: bash
+ run: |
+ cat > config/database.yml <<EOF
+ test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ EOF
+
+ - if: ${{ inputs.db-type == 'mysql2' || inputs.db-type == 'postgresql' }}
+ name: Prepare test database for mysql2 and postgresql
+ shell: bash
+ run: |
+ cat > config/database.yml <<EOF
+ test:
+ adapter: ${{ inputs.db-type }}
+ database: redmine_test
+ username: root
+ password: root
+ host: 127.0.0.1
+ EOF
+
+ - name: Install Ruby and gems
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ inputs.ruby-version }}
+ bundler-cache: true
+
+ - name: Run prepare test environment
+ shell: bash
+ env:
+ RAILS_ENV: test
+ SCMS: subversion,git,git_utf8,filesystem,bazaar,cvs
+ run: |
+ bundle exec rake ci:about
+ bundle exec rake ci:setup
+ bundle exec rake db:environment:set
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 12b507b0e..fa8b61666 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
- ruby: ['3.1', '3.2', '3.3']
+ ruby: ['3.2', '3.3', '3.4']
db: ['postgresql', 'mysql2', 'sqlite3']
fail-fast: false
@@ -46,55 +46,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- - name: Install dependencies and configure environment
- run: |
- sudo apt-get update
- sudo apt-get install --yes --quiet ghostscript gsfonts locales bzr cvs
- sudo locale-gen en_US # for bazaar non ascii test
-
- - name: Allow imagemagick to read PDF files
- run: |
- echo '<policymap>' > policy.xml
- echo '<policy domain="coder" rights="read | write" pattern="PDF" />' >> policy.xml
- echo '</policymap>' >> policy.xml
- sudo rm /etc/ImageMagick-6/policy.xml
- sudo mv policy.xml /etc/ImageMagick-6/policy.xml
-
- - if: ${{ matrix.db == 'sqlite3' }}
- name: Prepare test database for sqlite3
- run: |
- cat > config/database.yml <<EOF
- test:
- adapter: sqlite3
- database: db/test.sqlite3
- EOF
-
- - if: ${{ matrix.db == 'mysql2' || matrix.db == 'postgresql' }}
- name: Prepare test database for mysql2 and postgresql
- run: |
- cat > config/database.yml <<EOF
- test:
- adapter: ${{ matrix.db }}
- database: redmine_test
- username: root
- password: root
- host: 127.0.0.1
- EOF
-
- - name: Install Ruby and gems
- uses: ruby/setup-ruby@v1
+ - name: Setup Redmine test environment
+ uses: ./.github/actions/setup-redmine
with:
+ db-type: ${{ matrix.db }}
ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
-
- - name: Run prepare test environment
- env:
- RAILS_ENV: test
- SCMS: subversion,git,git_utf8,filesystem,bazaar,cvs
- run: |
- bundle exec rake ci:about
- bundle exec rake ci:setup
- bundle exec rake db:environment:set
- name: Run tests
run: |
@@ -110,3 +66,38 @@ jobs:
- name: Run autoload test
run: |
bin/rails test:autoload
+
+ system-tests:
+ name: system test
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Redmine test environment
+ uses: ./.github/actions/setup-redmine
+ with:
+ db-type: sqlite3
+ ruby-version: '3.4'
+
+ # System tests use Chrome and ChromeDriver installed on the GitHub Actions Ubuntu image.
+ # They are generally updated to the latest stable versions.
+ - name: Display Chrome version
+ run: google-chrome --version
+
+ - name: Run system tests
+ run: bin/rails test:system
+ env:
+ GOOGLE_CHROME_OPTS_ARGS: headless,disable-gpu,no-sandbox,disable-dev-shm-usage
+ # System tests might still be a bit unstable, so for now, even if a system test fails,
+ # output the results and consider the overall test as successful.
+ continue-on-error: true
+
+ - name: Upload system test screenshots
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: system-test-screenshots
+ path: tmp/screenshots
+ if-no-files-found: ignore
diff --git a/.rubocop.yml b/.rubocop.yml
index 3a2831c05..77857c9cd 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,7 +1,7 @@
inherit_from: .rubocop_todo.yml
AllCops:
- TargetRubyVersion: 3.1
+ TargetRubyVersion: 3.2
TargetRailsVersion: 7.2
NewCops: enable
@@ -17,9 +17,7 @@ AllCops:
- '**/files/**/*'
- 'db/schema.rb'
-# Enable extensions
-
-require:
+plugins:
- rubocop-performance
- rubocop-rails
@@ -89,6 +87,14 @@ Lint/MissingSuper:
Lint/SuppressedException:
AllowComments: true
+Lint/UselessConstantScoping:
+ Exclude:
+ - 'app/controllers/repositories_controller.rb'
+ - 'app/models/mail_handler.rb'
+ - 'lib/redmine/acts/mentionable.rb'
+ - 'lib/redmine/asset_path.rb'
+ - 'lib/redmine/wiki_formatting/textile/redcloth3.rb'
+
Layout/LineContinuationLeadingSpace:
Enabled: false
@@ -108,7 +114,10 @@ Naming/VariableNumber:
Naming/BinaryOperatorParameterName:
Enabled: false
-Naming/PredicateName:
+Naming/PredicateMethod:
+ Enabled: false
+
+Naming/PredicatePrefix:
Enabled: false
Performance/CollectionLiteralInLoop:
@@ -160,6 +169,13 @@ Rails/FindEach:
Rails/HelperInstanceVariable:
Enabled: false
+Rails/Output:
+ Exclude:
+ - 'config/routes.rb'
+ - 'lib/redmine/diff.rb'
+ - 'lib/redmine/diff_table.rb'
+ - 'test/unit/lib/redmine/scm/adapters/*.rb'
+
Rails/Pluck:
Exclude:
# `pluck` is not available in Gemfile
@@ -194,6 +210,9 @@ Style/AsciiComments:
# We can not change nor remove it.
- 'app/models/repository/git.rb'
+Style/BarePercentLiterals:
+ Enabled: false
+
Style/BlockComments:
Exclude:
- 'lib/redmine/string_array_diff/diff.rb'
@@ -205,6 +224,9 @@ Style/BlockDelimiters:
- 'lib/redmine/string_array_diff/diff.rb'
- 'lib/redmine/string_array_diff/diffable.rb'
+Style/EmptyStringInsideInterpolation:
+ Enabled: false
+
Style/FetchEnvVar:
Enabled: false
@@ -262,6 +284,9 @@ Style/PerlBackrefs:
Style/RaiseArgs:
Enabled: false
+Style/RedundantCondition:
+ Enabled: false
+
Style/RedundantConstantBase:
Exclude:
- 'config/environments/production.rb'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index aee767d54..1aafe4925 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 20 --no-offense-counts --no-auto-gen-timestamp`
-# using RuboCop version 1.69.0.
+# using RuboCop version 1.76.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -220,9 +220,6 @@ Lint/ParenthesesAsGroupedExpression:
- 'test/unit/attachment_test.rb'
- 'test/unit/lib/redmine/export/pdf_test.rb'
-Lint/ShadowingOuterLocalVariable:
- Enabled: false
-
# Configuration parameters: AllowComments, AllowNil.
Lint/SuppressedException:
Exclude:
@@ -289,8 +286,9 @@ Naming/MemoizedInstanceVariableName:
- 'lib/redmine/field_format.rb'
- 'lib/redmine/search.rb'
-# Configuration parameters: EnforcedStyle, AllowedPatterns.
+# Configuration parameters: EnforcedStyle, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns.
# SupportedStyles: snake_case, camelCase
+# ForbiddenIdentifiers: __id__, __send__
Naming/MethodName:
Exclude:
- 'lib/redmine/export/pdf.rb'
@@ -310,7 +308,7 @@ Naming/RescuedExceptionsVariableName:
- 'lib/redmine/scm/adapters/abstract_adapter.rb'
- 'lib/redmine/scm/adapters/filesystem_adapter.rb'
-# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns.
+# Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns.
# SupportedStyles: snake_case, camelCase
Naming/VariableName:
Exclude:
@@ -319,7 +317,7 @@ Naming/VariableName:
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer
-# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
+# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
Naming/VariableNumber:
Exclude:
- 'test/functional/versions_controller_test.rb'
@@ -330,7 +328,7 @@ Naming/VariableNumber:
- 'test/unit/project_test.rb'
# Configuration parameters: Severity, Include.
-# Include: app/models/**/*.rb
+# Include: **/app/models/**/*.rb
Rails/ActiveRecordOverride:
Exclude:
- 'app/models/email_address.rb'
@@ -401,7 +399,7 @@ Rails/EagerEvaluationLogMessage:
- 'app/controllers/application_controller.rb'
# Configuration parameters: Include.
-# Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb
+# Include: **/app/**/*.rb, **/config/**/*.rb, **/lib/**/*.rb
Rails/Exit:
Exclude:
- 'config/environment.rb'
@@ -409,7 +407,7 @@ Rails/Exit:
- 'config/routes.rb'
# Configuration parameters: Include.
-# Include: app/models/**/*.rb
+# Include: **/app/models/**/*.rb
Rails/HasAndBelongsToMany:
Exclude:
- 'app/models/changeset.rb'
@@ -424,7 +422,7 @@ Rails/HasAndBelongsToMany:
- 'app/models/user.rb'
# Configuration parameters: Include.
-# Include: app/models/**/*.rb
+# Include: **/app/models/**/*.rb
Rails/HasManyOrHasOneDependent:
Exclude:
- 'app/models/auth_source.rb'
@@ -458,7 +456,7 @@ Rails/I18nLocaleTexts:
- 'app/models/mailer.rb'
# Configuration parameters: IgnoreScopes, Include.
-# Include: app/models/**/*.rb
+# Include: **/app/models/**/*.rb
Rails/InverseOf:
Exclude:
- 'app/models/board.rb'
@@ -482,7 +480,7 @@ Rails/InverseOf:
- 'app/models/wiki_page.rb'
# Configuration parameters: Include.
-# Include: app/controllers/**/*.rb, app/mailers/**/*.rb
+# Include: **/app/controllers/**/*.rb, **/app/mailers/**/*.rb
Rails/LexicallyScopedActionFilter:
Exclude:
- 'app/controllers/projects_controller.rb'
@@ -494,7 +492,7 @@ Rails/LinkToBlank:
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Include.
-# Include: config/routes.rb, config/routes/**/*.rb
+# Include: **/config/routes.rb, **/config/routes/**/*.rb
Rails/MatchRoute:
Exclude:
- 'config/routes.rb'
@@ -503,15 +501,6 @@ Rails/MatchRoute:
Rails/NegateInclude:
Enabled: false
-# This cop supports unsafe autocorrection (--autocorrect-all).
-# Configuration parameters: Include.
-# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
-Rails/Output:
- Exclude:
- - 'config/routes.rb'
- - 'lib/redmine/diff.rb'
- - 'lib/redmine/diff_table.rb'
-
Rails/OutputSafety:
Enabled: false
@@ -540,7 +529,7 @@ Rails/Present:
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Include.
-# Include: app/models/**/*.rb
+# Include: **/app/models/**/*.rb
Rails/ReadWriteAttribute:
Exclude:
- 'app/models/auth_source_ldap.rb'
@@ -564,12 +553,6 @@ Rails/RedundantTravelBack:
- 'test/integration/sudo_mode_test.rb'
- 'test/system/sudo_mode_test.rb'
-# This cop supports unsafe autocorrection (--autocorrect-all).
-Rails/ReflectionClassName:
- Exclude:
- - 'lib/redmine/nested_set/issue_nested_set.rb'
- - 'lib/redmine/nested_set/project_nested_set.rb'
-
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, Include.
# SupportedStyles: assert_not, refute
@@ -605,7 +588,7 @@ Rails/TimeZone:
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Include.
-# Include: app/models/**/*.rb
+# Include: **/app/models/**/*.rb
Rails/Validation:
Enabled: false
@@ -692,7 +675,6 @@ Style/AndOr:
- 'db/migrate/022_serialize_possibles_values.rb'
- 'lib/redmine/export/pdf.rb'
- 'lib/redmine/field_format.rb'
- - 'lib/redmine/helpers/gantt.rb'
- 'lib/redmine/scm/adapters/abstract_adapter.rb'
- 'lib/redmine/scm/adapters/bazaar_adapter.rb'
- 'lib/redmine/scm/adapters/filesystem_adapter.rb'
@@ -700,13 +682,6 @@ Style/AndOr:
- 'lib/redmine/scm/adapters/subversion_adapter.rb'
- 'lib/redmine/wiki_formatting/textile/redcloth3.rb'
-# This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: percent_q, bare_percent
-Style/BarePercentLiterals:
- Exclude:
- - 'test/integration/api_test/api_test.rb'
-
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: MinBranchesCount.
Style/CaseLikeIf:
@@ -719,8 +694,10 @@ Style/CaseLikeIf:
- 'lib/redmine/scm/adapters/git_adapter.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
-# Configuration parameters: EnforcedStyle.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
# SupportedStyles: nested, compact
+# SupportedStylesForClasses: ~, nested, compact
+# SupportedStylesForModules: ~, nested, compact
Style/ClassAndModuleChildren:
Enabled: false
@@ -986,7 +963,7 @@ Style/NestedTernaryOperator:
- 'app/models/journal.rb'
# This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: EnforcedStyle, MinBodyLength.
+# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Enabled: false
@@ -1107,13 +1084,6 @@ Style/RedundantBegin:
- 'test/unit/query_test.rb'
# This cop supports safe autocorrection (--autocorrect).
-Style/RedundantCondition:
- Exclude:
- - 'app/controllers/messages_controller.rb'
- - 'app/controllers/previews_controller.rb'
- - 'app/models/issue.rb'
-
-# This cop supports safe autocorrection (--autocorrect).
Style/RedundantConditional:
Exclude:
- 'app/controllers/workflows_controller.rb'
diff --git a/Gemfile b/Gemfile
index 32f710c54..1344fb57b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-ruby '>= 3.1.0', '< 3.4.0'
+ruby '>= 3.2.0', '< 3.5.0'
gem 'rails', '7.2.2.1'
gem 'rouge', '~> 4.5'
@@ -11,17 +11,24 @@ gem 'marcel'
gem 'mail', '~> 2.8.1'
gem 'nokogiri', '~> 1.18.3'
gem 'i18n', '~> 1.14.1'
-gem 'rbpdf', '~> 1.21.3'
+gem 'rbpdf', '~> 1.21.4'
gem 'addressable'
gem 'rubyzip', '~> 2.4.0'
gem 'propshaft', '~> 1.1.0'
gem 'rack', '>= 3.1.3'
+gem "stimulus-rails", "~> 1.3"
+gem "importmap-rails", "~> 2.0"
+gem 'commonmarker', '~> 2.3.0'
+gem "doorkeeper", "~> 5.8.2"
+gem "bcrypt", require: false
+gem "doorkeeper-i18n", "~> 5.2"
+gem "requestjs-rails", "~> 0.0.13"
# Ruby Standard Gems
-gem 'csv', '~> 3.2.8'
-gem 'net-imap', '~> 0.4.8'
+gem 'csv', '~> 3.3.2'
+gem 'net-imap', '~> 0.5.7'
gem 'net-pop', '~> 0.1.2'
-gem 'net-smtp', '~> 0.4.0'
+gem 'net-smtp', '~> 0.5.0'
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin]
@@ -44,12 +51,6 @@ group :minimagick do
gem 'mini_magick', '~> 5.2.0'
end
-# Optional CommonMark support, not for JRuby
-group :common_mark do
- gem "commonmarker", '~> 1.1.0'
- gem 'deckar01-task_list', '2.3.2'
-end
-
# Include database gems for the adapters found in the database
# configuration file
database_file = File.join(File.dirname(__FILE__), "config/database.yml")
@@ -72,6 +73,9 @@ if File.exist?(database_file)
when /mysql2/
gem 'mysql2', '~> 0.5.0'
gem "with_advisory_lock"
+ when /trilogy/
+ gem 'trilogy', '~> 2.9.0'
+ gem "with_advisory_lock"
when /postgresql/
gem 'pg', '~> 1.5.3'
when /sqlite3/
@@ -98,10 +102,11 @@ group :development do
gem 'listen', '~> 3.3'
gem 'yard', require: false
gem 'svg_sprite', require: false
+ gem 'bullet'
end
group :test do
- gem "rails-dom-testing"
+ gem "rails-dom-testing", '>= 2.3.0'
gem 'mocha', '>= 2.0.1'
gem 'simplecov', '~> 0.22.0', :require => false
gem "ffi", platforms: [:mingw, :x64_mingw, :mswin]
@@ -110,10 +115,13 @@ group :test do
gem "capybara", ">= 3.39"
gem 'selenium-webdriver', '>= 4.11.0'
# RuboCop
- gem 'rubocop', '~> 1.69.0', require: false
- gem 'rubocop-performance', '~> 1.23.0', require: false
- gem 'rubocop-rails', '~> 2.29.0', require: false
+ gem 'rubocop', '~> 1.76.0', require: false
+ gem 'rubocop-performance', '~> 1.25.0', require: false
+ gem 'rubocop-rails', '~> 2.32.0', require: false
gem 'bundle-audit', require: false
+ # for testing oauth provider capabilities
+ gem 'oauth2'
+ gem 'rest-client'
end
local_gemfile = File.join(File.dirname(__FILE__), "Gemfile.local")
diff --git a/app/assets/images/chevron-down.svg b/app/assets/images/chevron-down.svg
new file mode 100644
index 000000000..7dfc75f54
--- /dev/null
+++ b/app/assets/images/chevron-down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ccd" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 9l6 6l6 -6" /></svg> \ No newline at end of file
diff --git a/app/assets/images/chevron-right-idnt.svg b/app/assets/images/chevron-right-idnt.svg
new file mode 100644
index 000000000..c15529e90
--- /dev/null
+++ b/app/assets/images/chevron-right-idnt.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ccd" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg> \ No newline at end of file
diff --git a/app/assets/images/hourglass-empty.svg b/app/assets/images/hourglass-empty.svg
new file mode 100644
index 000000000..789eb193c
--- /dev/null
+++ b/app/assets/images/hourglass-empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#169" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-hourglass-empty"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6 20v-2a6 6 0 1 1 12 0v2a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1z" /><path d="M6 4v2a6 6 0 1 0 12 0v-2a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1z" /></svg> \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index df09ffd6e..6283537ce 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -11,6 +11,11 @@
<path d="M9 12h6"/>
<path d="M12 9v6"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--alert-circle">
+ <path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/>
+ <path d="M12 8v4"/>
+ <path d="M12 16h.01"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--angle-down">
<path d="M6 9l6 6l6 -6"/>
</symbol>
@@ -54,6 +59,13 @@
<path d="M12 15v6"/>
<path d="M5 15h3l-3 6h3"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--apps">
+ <path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
+ <path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
+ <path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
+ <path d="M14 7l6 0"/>
+ <path d="M17 4l0 6"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--arrow-right">
<path d="M4 9h8v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-8a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1z"/>
</symbol>
@@ -72,6 +84,11 @@
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bookmarked">
<path d="M18 7v14l-6 -4l-6 4v-14a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4z"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bulb">
+ <path d="M3 12h1m8 -9v1m8 8h1m-15.4 -6.4l.7 .7m12.1 -.7l-.7 .7"/>
+ <path d="M9 16a5 5 0 1 1 6 0a3.5 3.5 0 0 0 -1 3a2 2 0 0 1 -4 0a3.5 3.5 0 0 0 -1 -3"/>
+ <path d="M9.7 17l4.6 0"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--bullet-end">
<path d="M12 21a9 9 0 1 0 0 -18a9 9 0 0 0 0 18"/>
<path d="M8 12l4 4"/>
@@ -141,6 +158,10 @@
<path d="M13 17v-1a1 1 0 0 1 1 -1h1m3 0h1a1 1 0 0 1 1 1v1m0 3v1a1 1 0 0 1 -1 1h-1m-3 0h-1a1 1 0 0 1 -1 -1v-1"/>
<path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--copy-pre-content">
+ <path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"/>
+ <path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--custom-fields">
<path d="M20 13v-4a2 2 0 0 0 -2 -2h-12a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h6"/>
<path d="M15 19l2 2l4 -4"/>
@@ -290,6 +311,11 @@
<path d="M8 13h6"/>
<path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--message-report">
+ <path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z"/>
+ <path d="M12 8v3"/>
+ <path d="M12 14v.01"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--move">
<path d="M15 14l4 -4l-4 -4"/>
<path d="M19 10h-11a4 4 0 1 0 0 8h1"/>
@@ -332,6 +358,10 @@
<path d="M7 5.03v5.455"/>
<path d="M12 8l5 -3"/>
</symbol>
+ <symbol viewBox="0 0 24 24" id="icon--quote-filled">
+ <path d="M9 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/>
+ <path d="M18 5a2 2 0 0 1 2 2v6c0 3.13 -1.65 5.193 -4.757 5.97a1 1 0 1 1 -.486 -1.94c2.227 -.557 3.243 -1.827 3.243 -4.03v-1h-3a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-3a2 2 0 0 1 2 -2z"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--reload">
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"/>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/>
@@ -375,6 +405,10 @@
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"/>
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--shield-check">
+ <path d="M11.46 20.846a12 12 0 0 1 -7.96 -14.846a12 12 0 0 0 8.5 -3a12 12 0 0 0 8.5 3a12 12 0 0 1 -.09 7.06"/>
+ <path d="M15 19l2 2l4 -4"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--stats">
<path d="M3 13a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
<path d="M15 9a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
@@ -455,6 +489,13 @@
<path d="M19 15v6h3"/>
<path d="M11 21v-6l2.5 3l2.5 -3v6"/>
</symbol>
+ <symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--thumb-up">
+ <path d="M7 11v8a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1h3a4 4 0 0 0 4 -4v-1a2 2 0 0 1 4 0v5h3a2 2 0 0 1 2 2l-1 5a2 3 0 0 1 -2 2h-7a3 3 0 0 1 -3 -3"/>
+ </symbol>
+ <symbol viewBox="0 0 24 24" id="icon--thumb-up-filled">
+ <path d="M13 3a3 3 0 0 1 2.995 2.824l.005 .176v4h2a3 3 0 0 1 2.98 2.65l.015 .174l.005 .176l-.02 .196l-1.006 5.032c-.381 1.626 -1.502 2.796 -2.81 2.78l-.164 -.008h-8a1 1 0 0 1 -.993 -.883l-.007 -.117l.001 -9.536a1 1 0 0 1 .5 -.865a2.998 2.998 0 0 0 1.492 -2.397l.007 -.202v-1a3 3 0 0 1 3 -3z"/>
+ <path d="M5 10a1 1 0 0 1 .993 .883l.007 .117v9a1 1 0 0 1 -.883 .993l-.117 .007h-1a2 2 0 0 1 -1.995 -1.85l-.005 -.15v-7a2 2 0 0 1 1.85 -1.995l.15 -.005h1z"/>
+ </symbol>
<symbol viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" id="icon--time">
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"/>
<path d="M12 7v5l3 3"/>
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application-legacy.js
index 19578bee8..1219e1ef8 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application-legacy.js
@@ -43,10 +43,18 @@ function toggleRowGroup(el) {
}
function toggleExpendCollapseIcon(el) {
+ const svg = el.getElementsByTagName('svg').item(0)
+
+ if (svg === null) {
+ return false;
+ }
+
if (el.classList.contains('icon-expanded')) {
- updateSVGIcon(el, 'angle-down')
+ updateSVGIcon(svg, 'angle-down')
+ svg.classList.remove('icon-rtl')
} else {
- updateSVGIcon(el, 'angle-right')
+ updateSVGIcon(svg, 'angle-right')
+ svg.classList.add('icon-rtl')
}
}
@@ -61,13 +69,20 @@ function updateSVGIcon(element, icon) {
iconElement.setAttribute('href', iconPath.replace(/#.*$/g, "#icon--" + icon))
}
+function createSVGIcon(icon) {
+ const clonedIcon = document.querySelector('#icon-copy-source svg').cloneNode(true);
+ updateSVGIcon(clonedIcon, icon);
+ return clonedIcon
+}
+
function collapseAllRowGroups(el) {
var tbody = $(el).parents('tbody').first();
tbody.children('tr').each(function(index) {
if ($(this).hasClass('group')) {
$(this).removeClass('open');
- $(this).find('.expander').switchClass('icon-expanded', 'icon-collapsed');
- updateSVGIcon($(this).find('.expander')[0], 'angle-right')
+ var expander = $(this).find('.expander');
+ expander.switchClass('icon-expanded', 'icon-collapsed');
+ toggleExpendCollapseIcon(expander[0]);
} else {
$(this).hide();
}
@@ -79,8 +94,9 @@ function expandAllRowGroups(el) {
tbody.children('tr').each(function(index) {
if ($(this).hasClass('group')) {
$(this).addClass('open');
- $(this).find('.expander').switchClass('icon-collapsed', 'icon-expanded');
- updateSVGIcon($(this).find('.expander')[0], 'angle-down')
+ var expander = $(this).find('.expander');
+ expander.switchClass('icon-collapsed', 'icon-expanded');
+ toggleExpendCollapseIcon(expander[0]);
} else {
$(this).show();
}
@@ -212,8 +228,7 @@ function buildFilterRow(field, operator, values) {
case "list_status":
case "list_subprojects":
const iconType = values.length > 1 ? 'toggle-minus' : 'toggle-plus';
- const clonedIcon = document.querySelector('#icon-copy-source svg').cloneNode(true);
- updateSVGIcon(clonedIcon, iconType);
+ const iconSvg = createSVGIcon(iconType)
tr.find('.values').append(
$('<span>', { style: 'display:none;' }).append(
@@ -223,7 +238,7 @@ function buildFilterRow(field, operator, values) {
name: `v[${field}][]`,
}),
'\n',
- $('<span>', { class: `toggle-multiselect icon-only icon-${iconType}` }).append(clonedIcon)
+ $('<span>', { class: `toggle-multiselect icon-only icon-${iconType}` }).append(iconSvg)
)
);
select = tr.find('.values select');
@@ -411,7 +426,7 @@ function showIssueHistory(journal, url) {
tab_content.find('.journal').show();
tab_content.find('.journal:not(.has-notes)').hide();
tab_content.find('.journal .wiki').show();
- tab_content.find('.journal .contextual .journal-actions').show();
+ tab_content.find('.journal .journal-actions > *').show();
// always show thumbnails in notes tab
var thumbnails = tab_content.find('.journal .thumbnails');
@@ -424,13 +439,15 @@ function showIssueHistory(journal, url) {
tab_content.find('.journal:not(.has-details)').hide();
tab_content.find('.journal .wiki').hide();
tab_content.find('.journal .thumbnails').hide();
- tab_content.find('.journal .contextual .journal-actions').hide();
+ tab_content.find('.journal .journal-actions > *').hide();
+ // Show reaction button in properties tab
+ tab_content.find('.journal .journal-actions .reaction-button-wrapper').show();
break;
default:
tab_content.find('.journal').show();
tab_content.find('.journal .wiki').show();
tab_content.find('.journal .thumbnails').show();
- tab_content.find('.journal .contextual .journal-actions').show();
+ tab_content.find('.journal .journal-actions > *').show();
}
return false;
@@ -586,19 +603,23 @@ function expandScmEntry(id) {
function scmEntryClick(id, url) {
var el = $('#'+id);
+ var expander = el.find('.expander');
+ var folder = el.find('.icon-folder');
if (el.hasClass('open')) {
collapseScmEntry(id);
el.find('.expander').switchClass('icon-expanded', 'icon-collapsed');
el.addClass('collapsed');
- updateSVGIcon(el.find('.icon-folder')[0], 'folder')
+ updateSVGIcon(folder[0], 'folder')
+ toggleExpendCollapseIcon(expander[0]);
return false;
} else if (el.hasClass('loaded')) {
expandScmEntry(id);
el.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
el.removeClass('collapsed');
- updateSVGIcon(el.find('.icon-folder-open')[0], 'folder-open')
+ updateSVGIcon(folder[0], 'folder-open')
+ toggleExpendCollapseIcon(expander[0]);
return false;
}
@@ -611,8 +632,9 @@ function scmEntryClick(id, url) {
success: function(data) {
el.after(data);
el.addClass('open').addClass('loaded').removeClass('loading');
- updateSVGIcon(el.find('.icon-folder')[0], 'folder-open')
el.find('.expander').switchClass('icon-collapsed', 'icon-expanded');
+ updateSVGIcon(folder[0], 'folder-open')
+ toggleExpendCollapseIcon(expander[0]);
}
});
return true;
@@ -627,23 +649,65 @@ function randomKey(size) {
return key;
}
-function copyTextToClipboard(target) {
- if (target) {
- var temp = document.createElement('textarea');
- temp.value = target.getAttribute('data-clipboard-text');
- document.body.appendChild(temp);
- temp.select();
- document.execCommand('copy');
- if (temp.parentNode) {
- temp.parentNode.removeChild(temp);
- }
- if ($(target).closest('.drdn.expanded').length) {
- $(target).closest('.drdn.expanded').removeClass("expanded");
- }
+function copyToClipboard(text) {
+ if (navigator.clipboard) {
+ return navigator.clipboard.writeText(text).catch(() => {
+ return fallbackClipboardCopy(text);
+ });
+ } else {
+ return fallbackClipboardCopy(text);
+ }
+}
+
+function fallbackClipboardCopy(text) {
+ const temp = document.createElement('textarea');
+ temp.value = text;
+ temp.style.position = 'fixed';
+ temp.style.left = '-9999px';
+ document.body.appendChild(temp);
+ temp.select();
+ document.execCommand('copy');
+ document.body.removeChild(temp);
+ return Promise.resolve();
+}
+
+function copyDataClipboardTextToClipboard(target) {
+ copyToClipboard(target.getAttribute('data-clipboard-text'));
+
+ if ($(target).closest('.drdn.expanded').length) {
+ $(target).closest('.drdn.expanded').removeClass("expanded");
}
return false;
}
+function setupCopyButtonsToPreElements() {
+ document.querySelectorAll('.wiki pre:not(.pre-wrapper pre)').forEach((pre) => {
+ // Wrap the <pre> element with a container and add a copy button
+ const wrapper = document.createElement("div");
+ wrapper.classList.add("pre-wrapper");
+
+ const copyButton = document.createElement("a");
+ copyButton.title = rm.I18n.buttonCopy;
+ copyButton.classList.add("copy-pre-content-link", "icon-only");
+ copyButton.append(createSVGIcon("copy-pre-content"));
+
+ wrapper.appendChild(copyButton);
+ wrapper.append(pre.cloneNode(true));
+ pre.replaceWith(wrapper);
+
+ // Copy the contents of the pre tag when copyButton is clicked
+ copyButton.addEventListener("click", (event) => {
+ event.preventDefault();
+ let textToCopy = (pre.querySelector("code") || pre).textContent.replace(/\n$/, '');
+ if (pre.querySelector("code.syntaxhl")) { textToCopy = textToCopy.replace(/ $/, ''); } // Workaround for half-width space issue in Textile's highlighted code
+ copyToClipboard(textToCopy).then(() => {
+ updateSVGIcon(copyButton, "checked");
+ setTimeout(() => updateSVGIcon(copyButton, "copy-pre-content"), 2000);
+ });
+ });
+ });
+}
+
function updateIssueFrom(url, el) {
$('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
$(this).data('valuebeforeupdate', $(this).val());
@@ -1160,8 +1224,8 @@ function setupWikiTableSortableHeader() {
});
}
-$(function () {
- $("[title]:not(.no-tooltip)").tooltip({
+function setupHoverTooltips(container) {
+ $(container || 'body').find("[title]:not(.no-tooltip)").tooltip({
show: {
delay: 400
},
@@ -1170,7 +1234,11 @@ $(function () {
at: "center top"
}
});
-});
+}
+function removeHoverTooltips(container) {
+ $(container || 'body').find("[title]:not(.no-tooltip)").tooltip('destroy')
+}
+$(function() { setupHoverTooltips(); });
function inlineAutoComplete(element) {
'use strict';
@@ -1364,3 +1432,4 @@ $(document).ready(setupWikiTableSortableHeader);
$(document).on('focus', '[data-auto-complete=true]', function(event) {
inlineAutoComplete(event.target);
});
+document.addEventListener("DOMContentLoaded", () => { setupCopyButtonsToPreElements(); });
diff --git a/app/assets/javascripts/attachments.js b/app/assets/javascripts/attachments.js
index df9c7090e..4c74b2d74 100644
--- a/app/assets/javascripts/attachments.js
+++ b/app/assets/javascripts/attachments.js
@@ -5,9 +5,13 @@
*/
function addFile(inputEl, file, eagerUpload) {
- var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
- var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
+ var attachmentsForm = $(inputEl).closest('.attachments_form')
+ var attachmentsFields = attachmentsForm.find('.attachments_fields');
+ var attachmentsIcons = attachmentsForm.find('.attachments_icons');
+ var addAttachment = attachmentsForm.find('.add_attachment');
var maxFiles = ($(inputEl).attr('multiple') == 'multiple' ? 10 : 1);
+ var delIcon = attachmentsIcons.find('svg.svg-del').clone();
+ var attachmentIcon = attachmentsIcons.find('svg.svg-attachment').clone();
if (attachmentsFields.children().length < maxFiles) {
var attachmentId = addFile.nextAttachmentId++;
@@ -16,12 +20,14 @@ function addFile(inputEl, file, eagerUpload) {
if (!param) {param = 'attachments'};
fileSpan.append(
+ attachmentIcon,
$('<input>', { type: 'text', 'class': 'icon icon-attachment filename readonly', name: param +'[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
$('<input>', { type: 'text', 'class': 'description', name: param + '[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
$('<input>', { type: 'hidden', 'class': 'token', name: param + '[' + attachmentId + '][token]'} ),
- $('<a>&nbsp</a>').attr({ href: "#", 'class': 'icon-only icon-del remove-upload' }).click(removeFile).toggle(!eagerUpload)
+ $('<a>', { href: "#", 'class': 'icon-only icon-del remove-upload' }).append(delIcon).click(removeFile).toggle(!eagerUpload)
).appendTo(attachmentsFields);
+
if ($(inputEl).data('description') == 0) {
fileSpan.find('input.description').remove();
}
@@ -63,7 +69,7 @@ function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
.done(function(result) {
addInlineAttachmentMarkup(file);
progressSpan.progressbar( 'value', 100 ).remove();
- fileSpan.find('input.description, a').css('display', 'inline-block');
+ fileSpan.find('input.description, a').css('display', 'inline-flex');
})
.fail(function(result) {
progressSpan.text(result.statusText);
diff --git a/app/assets/javascripts/quote_reply.js b/app/assets/javascripts/quote_reply.js
index 7649f5125..dd05d27fe 100644
--- a/app/assets/javascripts/quote_reply.js
+++ b/app/assets/javascripts/quote_reply.js
@@ -1,21 +1,6 @@
-function quoteReply(path, selectorForContentElement, textFormatting) {
- const contentElement = $(selectorForContentElement).get(0);
- const selectedRange = QuoteExtractor.extract(contentElement);
-
- let formatter;
-
- if (textFormatting === 'common_mark') {
- formatter = new QuoteCommonMarkFormatter();
- } else {
- formatter = new QuoteTextFormatter();
- }
-
- $.ajax({
- url: path,
- type: 'post',
- data: { quote: formatter.format(selectedRange) }
- });
-}
+import { Controller } from '@hotwired/stimulus'
+import TurndownService from 'turndown'
+import { post } from '@rails/request.js'
class QuoteExtractor {
static extract(targetElement) {
@@ -214,3 +199,26 @@ class QuoteCommonMarkFormatter {
return htmlFragment.innerHTML;
}
}
+
+export default class extends Controller {
+ static targets = [ 'content' ];
+
+ quote(event) {
+ const { url, textFormatting } = event.params;
+ const selectedRange = QuoteExtractor.extract(this.contentTarget);
+
+ let formatter;
+
+ if (textFormatting === 'common_mark') {
+ formatter = new QuoteCommonMarkFormatter();
+ } else {
+ formatter = new QuoteTextFormatter();
+ }
+
+ post(url, {
+ body: JSON.stringify({ quote: formatter.format(selectedRange) }),
+ contentType: 'application/json',
+ responseKind: 'script'
+ });
+ }
+}
diff --git a/app/assets/javascripts/turndown-7.2.0.min.js b/app/assets/javascripts/turndown-7.2.0.min.js
index f3fb4b1e6..e69de29bb 100644
--- a/app/assets/javascripts/turndown-7.2.0.min.js
+++ b/app/assets/javascripts/turndown-7.2.0.min.js
@@ -1,8 +0,0 @@
-/*
- * Turndown v7.2.0
- * https://github.com/mixmark-io/turndown
- * Copyright (c) 2017 Dom Christie
- * Released under the MIT license
- * https://github.com/mixmark-io/turndown/blob/master/LICENSE
- */
-var TurndownService=(()=>{function u(e,n){return Array(n+1).join(e)}var n=["ADDRESS","ARTICLE","ASIDE","AUDIO","BLOCKQUOTE","BODY","CANVAS","CENTER","DD","DIR","DIV","DL","DT","FIELDSET","FIGCAPTION","FIGURE","FOOTER","FORM","FRAMESET","H1","H2","H3","H4","H5","H6","HEADER","HGROUP","HR","HTML","ISINDEX","LI","MAIN","MENU","NAV","NOFRAMES","NOSCRIPT","OL","OUTPUT","P","PRE","SECTION","TABLE","TBODY","TD","TFOOT","TH","THEAD","TR","UL"];function f(e){return o(e,n)}var r=["AREA","BASE","BR","COL","COMMAND","EMBED","HR","IMG","INPUT","KEYGEN","LINK","META","PARAM","SOURCE","TRACK","WBR"];function d(e){return o(e,r)}var i=["A","TABLE","THEAD","TBODY","TFOOT","TH","TD","IFRAME","SCRIPT","AUDIO","VIDEO"];function o(e,n){return 0<=n.indexOf(e.nodeName)}function a(n,e){return n.getElementsByTagName&&e.some(function(e){return n.getElementsByTagName(e).length})}var t={};function c(e){return e?e.replace(/(\n+\s*)+/g,"\n"):""}function l(e){for(var n in this.options=e,this._keep=[],this._remove=[],this.blankRule={replacement:e.blankReplacement},this.keepReplacement=e.keepReplacement,this.defaultRule={replacement:e.defaultReplacement},this.array=[],e.rules)this.array.push(e.rules[n])}function s(e,n,t){for(var r=0;r<e.length;r++){var i=e[r];if(((e,n,t)=>{var r=e.filter;if("string"==typeof r)return r===n.nodeName.toLowerCase();if(Array.isArray(r))return-1<r.indexOf(n.nodeName.toLowerCase());if("function"==typeof r)return!!r.call(e,n,t);throw new TypeError("`filter` needs to be a string, array, or function")})(i,n,t))return i}}function p(e){var n=e.nextSibling||e.parentNode;return e.parentNode.removeChild(e),n}function h(e,n,t){return e&&e.parentNode===n||t(n)?n.nextSibling||n.parentNode:n.firstChild||n.nextSibling||n.parentNode}t.paragraph={filter:"p",replacement:function(e){return"\n\n"+e+"\n\n"}},t.lineBreak={filter:"br",replacement:function(e,n,t){return t.br+"\n"}},t.heading={filter:["h1","h2","h3","h4","h5","h6"],replacement:function(e,n,t){n=Number(n.nodeName.charAt(1));return"setext"===t.headingStyle&&n<3?"\n\n"+e+"\n"+u(1===n?"=":"-",e.length)+"\n\n":"\n\n"+u("#",n)+" "+e+"\n\n"}},t.blockquote={filter:"blockquote",replacement:function(e){return"\n\n"+(e=(e=e.replace(/^\n+|\n+$/g,"")).replace(/^/gm,"> "))+"\n\n"}},t.list={filter:["ul","ol"],replacement:function(e,n){var t=n.parentNode;return"LI"===t.nodeName&&t.lastElementChild===n?"\n"+e:"\n\n"+e+"\n\n"}},t.listItem={filter:"li",replacement:function(e,n,t){e=e.replace(/^\n+/,"").replace(/\n+$/,"\n").replace(/\n/gm,"\n ");var r,t=t.bulletListMarker+" ",i=n.parentNode;return"OL"===i.nodeName&&(r=i.getAttribute("start"),i=Array.prototype.indexOf.call(i.children,n),t=(r?Number(r)+i:i+1)+". "),t+e+(n.nextSibling&&!/\n$/.test(e)?"\n":"")}},t.indentedCodeBlock={filter:function(e,n){return"indented"===n.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,n,t){return"\n\n "+n.firstChild.textContent.replace(/\n/g,"\n ")+"\n\n"}},t.fencedCodeBlock={filter:function(e,n){return"fenced"===n.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,n,t){for(var r,i=((n.firstChild.getAttribute("class")||"").match(/language-(\S+)/)||[null,""])[1],o=n.firstChild.textContent,n=t.fence.charAt(0),a=3,l=new RegExp("^"+n+"{3,}","gm");r=l.exec(o);)r[0].length>=a&&(a=r[0].length+1);t=u(n,a);return"\n\n"+t+i+"\n"+o.replace(/\n$/,"")+"\n"+t+"\n\n"}},t.horizontalRule={filter:"hr",replacement:function(e,n,t){return"\n\n"+t.hr+"\n\n"}},t.inlineLink={filter:function(e,n){return"inlined"===n.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,n){var t=(t=n.getAttribute("href"))&&t.replace(/([()])/g,"\\$1"),n=c(n.getAttribute("title"));return"["+e+"]("+t+(n=n&&' "'+n.replace(/"/g,'\\"')+'"')+")"}},t.referenceLink={filter:function(e,n){return"referenced"===n.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,n,t){var r=n.getAttribute("href"),i=(i=c(n.getAttribute("title")))&&' "'+i+'"';switch(t.linkReferenceStyle){case"collapsed":a="["+e+"][]",l="["+e+"]: "+r+i;break;case"shortcut":a="["+e+"]",l="["+e+"]: "+r+i;break;default:var o=this.references.length+1,a="["+e+"]["+o+"]",l="["+o+"]: "+r+i}return this.references.push(l),a},references:[],append:function(e){var n="";return this.references.length&&(n="\n\n"+this.references.join("\n")+"\n\n",this.references=[]),n}},t.emphasis={filter:["em","i"],replacement:function(e,n,t){return e.trim()?t.emDelimiter+e+t.emDelimiter:""}},t.strong={filter:["strong","b"],replacement:function(e,n,t){return e.trim()?t.strongDelimiter+e+t.strongDelimiter:""}},t.code={filter:function(e){var n=e.previousSibling||e.nextSibling,n="PRE"===e.parentNode.nodeName&&!n;return"CODE"===e.nodeName&&!n},replacement:function(e){if(!e)return"";e=e.replace(/\r?\n|\r/g," ");for(var n=/^`|^ .*?[^ ].* $|`$/.test(e)?" ":"",t="`",r=e.match(/`+/gm)||[];-1!==r.indexOf(t);)t+="`";return t+n+e+n+t}},t.image={filter:"img",replacement:function(e,n){var t=c(n.getAttribute("alt")),r=n.getAttribute("src")||"",n=c(n.getAttribute("title"));return r?"!["+t+"]("+r+(n?' "'+n+'"':"")+")":""}},l.prototype={add:function(e,n){this.array.unshift(n)},keep:function(e){this._keep.unshift({filter:e,replacement:this.keepReplacement})},remove:function(e){this._remove.unshift({filter:e,replacement:function(){return""}})},forNode:function(e){return e.isBlank?this.blankRule:s(this.array,e,this.options)||s(this._keep,e,this.options)||s(this._remove,e,this.options)||this.defaultRule},forEach:function(e){for(var n=0;n<this.array.length;n++)e(this.array[n],n)}};var g,m="undefined"!=typeof window?window:{},A=(()=>{var e=m.DOMParser,n=!1;try{(new e).parseFromString("","text/html")&&(n=!0)}catch(e){}return n})()?m.DOMParser:((()=>{var n=!1;try{document.implementation.createHTMLDocument("").open()}catch(e){m.ActiveXObject&&(n=!0)}return n})()?e.prototype.parseFromString=function(e){var n=new window.ActiveXObject("htmlfile");return n.designMode="on",n.open(),n.write(e),n.close(),n}:e.prototype.parseFromString=function(e){var n=document.implementation.createHTMLDocument("");return n.open(),n.write(e),n.close(),n},e);function e(){}function y(e,n){var n={element:e="string"==typeof e?(g=g||new A).parseFromString('<x-turndown id="turndown-root">'+e+"</x-turndown>","text/html").getElementById("turndown-root"):e.cloneNode(!0),isBlock:f,isVoid:d,isPre:n.preformattedCode?v:null},t=n.element,r=n.isBlock,i=n.isVoid,o=n.isPre||function(e){return"PRE"===e.nodeName};if(t.firstChild&&!o(t)){for(var a=null,l=!1,u=h(s=null,t,o);u!==t;){if(3===u.nodeType||4===u.nodeType){var c=u.data.replace(/[ \r\n\t]+/g," ");if(!(c=a&&!/ $/.test(a.data)||l||" "!==c[0]?c:c.substr(1))){u=p(u);continue}u.data=c,a=u}else{if(1!==u.nodeType){u=p(u);continue}r(u)||"BR"===u.nodeName?(a&&(a.data=a.data.replace(/ $/,"")),a=null,l=!1):i(u)||o(u)?l=!(a=null):a&&(l=!1)}var c=h(s,u,o),s=u,u=c}a&&(a.data=a.data.replace(/ $/,""),a.data||p(a))}return e}function v(e){return"PRE"===e.nodeName||"CODE"===e.nodeName}function N(e,n){var t;return e.isBlock=f(e),e.isCode="CODE"===e.nodeName||e.parentNode.isCode,e.isBlank=!d(t=e)&&!(e=>o(e,i))(t)&&/^\s*$/i.test(t.textContent)&&!(e=>a(e,r))(t)&&!(e=>a(e,i))(t),e.flankingWhitespace=((e,n)=>{var t;return e.isBlock||n.preformattedCode&&e.isCode?{leading:"",trailing:""}:((t=(e=>({leading:(e=e.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/))[1],leadingAscii:e[2],leadingNonAscii:e[3],trailing:e[4],trailingNonAscii:e[5],trailingAscii:e[6]}))(e.textContent)).leadingAscii&&E("left",e,n)&&(t.leading=t.leadingNonAscii),t.trailingAscii&&E("right",e,n)&&(t.trailing=t.trailingNonAscii),{leading:t.leading,trailing:t.trailing})})(e,n),e}function E(e,n,t){var r,i,e="left"===e?(r=n.previousSibling,/ $/):(r=n.nextSibling,/^ /);return r&&(3===r.nodeType?i=e.test(r.nodeValue):t.preformattedCode&&"CODE"===r.nodeName?i=!1:1!==r.nodeType||f(r)||(i=e.test(r.textContent))),i}var T=Array.prototype.reduce,R=[[/\\/g,"\\\\"],[/\*/g,"\\*"],[/^-/g,"\\-"],[/^\+ /g,"\\+ "],[/^(=+)/g,"\\$1"],[/^(#{1,6}) /g,"\\$1 "],[/`/g,"\\`"],[/^~~~/g,"\\~~~"],[/\[/g,"\\["],[/\]/g,"\\]"],[/^>/g,"\\>"],[/_/g,"\\_"],[/^(\d+)\. /g,"$1\\. "]];function C(e){if(!(this instanceof C))return new C(e);this.options=function(e){for(var n=1;n<arguments.length;n++){var t,r=arguments[n];for(t in r)r.hasOwnProperty(t)&&(e[t]=r[t])}return e}({},{rules:t,headingStyle:"setext",hr:"* * *",bulletListMarker:"*",codeBlockStyle:"indented",fence:"```",emDelimiter:"_",strongDelimiter:"**",linkStyle:"inlined",linkReferenceStyle:"full",br:" ",preformattedCode:!1,blankReplacement:function(e,n){return n.isBlock?"\n\n":""},keepReplacement:function(e,n){return n.isBlock?"\n\n"+n.outerHTML+"\n\n":n.outerHTML},defaultReplacement:function(e,n){return n.isBlock?"\n\n"+e+"\n\n":e}},e),this.rules=new l(this.options)}function k(e){var r=this;return T.call(e.childNodes,function(e,n){var t="";return 3===(n=new N(n,r.options)).nodeType?t=n.isCode?n.nodeValue:r.escape(n.nodeValue):1===n.nodeType&&(t=function(e){var n=this.rules.forNode(e),t=k.call(this,e),r=e.flankingWhitespace;(r.leading||r.trailing)&&(t=t.trim());return r.leading+n.replacement(t,e,this.options)+r.trailing}.call(r,n)),b(e,t)},"")}function b(e,n){var t=(e=>{for(var n=e.length;0<n&&"\n"===e[n-1];)n--;return e.substring(0,n)})(e),r=n.replace(/^\n*/,""),e=Math.max(e.length-t.length,n.length-r.length);return t+"\n\n".substring(0,e)+r}return C.prototype={turndown:function(e){if(null==(n=e)||"string"!=typeof n&&(!n.nodeType||1!==n.nodeType&&9!==n.nodeType&&11!==n.nodeType))throw new TypeError(e+" is not a string, or an element/document/fragment node.");var n;return""===e?"":(n=k.call(this,new y(e,this.options)),function(n){var t=this;return this.rules.forEach(function(e){"function"==typeof e.append&&(n=b(n,e.append(t.options)))}),n.replace(/^[\t\r\n]+/,"").replace(/[\t\r\n\s]+$/,"")}.call(this,n))},use:function(e){if(Array.isArray(e))for(var n=0;n<e.length;n++)this.use(e[n]);else{if("function"!=typeof e)throw new TypeError("plugin must be a Function or an Array of Functions");e(this)}return this},addRule:function(e,n){return this.rules.add(e,n),this},keep:function(e){return this.rules.keep(e),this},remove:function(e){return this.rules.remove(e),this},escape:function(e){return R.reduce(function(e,n){return e.replace(n[0],n[1])},e)}},C})();
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index dab8cc4bb..568bed2bc 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -236,6 +236,7 @@ a.user.user-mention {
#sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
#sidebar a.selected:hover {text-decoration:none;}
+#sidebar a.selected svg.icon-svg { stroke: #fff !important; }
#sidebar .query.default {font-weight: bold;}
#admin-menu a {line-height:1.7em;}
#admin-menu a.selected:not(:has(svg)) {padding-left: 20px !important; background-position: 2px 40%;}
@@ -305,24 +306,44 @@ div + .drdn-items {border-top:1px solid #ccc;}
}
.drdn-items>span {color:#999;}
-.contextual .drdn-content {top:18px;}
-.contextual .drdn-items {padding:2px; min-width: 160px;}
-.contextual .drdn-items>a {display: flex; padding: 5px 8px;}
-.contextual .drdn-items>a.icon:not(:has(svg)) {padding-left: 24px; background-position-x: 4px;}
-.contextual .drdn-items>a:hover {color:#2A5685; border:1px solid #628db6; background-color:#eef5fd; border-radius:3px;}
+.contextual .drdn-content, .journal-actions .drdn-content {
+ top: 18px;
+}
+
+.contextual .drdn-items, .journal-actions .drdn-items {
+ padding: 2px;
+ min-width: 160px;
+}
+
+.contextual .drdn-items > a, .journal-actions .drdn-items > a {
+ display: flex;
+ padding: 5px 8px;
+}
+
+.contextual .drdn-items > a.icon:not(:has(svg)), .journal-actions .drdn-items > a.icon:not(:has(svg)) {
+ padding-left: 24px;
+ background-position-x: 4px;
+}
+
+.contextual .drdn-items > a:hover, .journal-actions .drdn-items > a:hover {
+ color: #2A5685;
+ border: 1px solid #628db6;
+ background-color: #eef5fd;
+ border-radius: 3px;
+}
#project-jump.drdn {width:200px;display:inline-block;}
#project-jump .drdn-trigger {
width:100%;
height:24px;
display:inline-block;
- padding:3px 18px 3px 6px;
+ padding:1.5px 18px 3px 6px;
border-radius:3px;
border:1px solid #ccc;
margin:0 !important;
vertical-align:middle;
color:#555;
- background:#fff url(/arrow_down.png) no-repeat 97% 50%;
+ background:#fff url(/chevron-down.svg) no-repeat 98% 50%;
}
#project-jump .drdn.expanded .drdn-trigger {background-image:url(/arrow_up.png);}
#project-jump .drdn-content {width:280px;}
@@ -358,11 +379,14 @@ table.list td.buttons a, div.buttons a, table.list td.buttons span.icon-only { m
table.list td.buttons a:last-child, div.buttons a:last-child { margin-right: 0; }
table.list td.buttons img, div.buttons img {vertical-align:middle;}
table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
-table.list table.progress td {padding-right:0px;}
+table.list table.progress td {padding-right:0; border-top: none;}
table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
table.list tr.overdue td.due_date { color: #c22; }
table.list thead.related-issues th { background-color: inherit; font-size: 11px; border: none; }
#role-permissions-trackers table.list th {white-space:normal;}
+table.list div.wiki p {
+ margin: 0;
+}
.table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
.table-list div.buttons {width: 15%;}
@@ -384,16 +408,16 @@ table.issues td.block_column {color:#777; font-size:90%; padding:4px 4px 4px 24p
table.issues td.block_column>span {font-weight: bold; display: block; margin-bottom: 4px;}
table.issues td.block_column>pre {white-space:normal;}
-tr.idnt td.subject, tr.idnt td.name {background: url(/arrow_right.png) no-repeat 2px 50%;}
-tr.idnt-1 td.subject, tr.idnt-1 td.name {padding-left: 24px; background-position: 8px 50%;}
-tr.idnt-2 td.subject, tr.idnt-2 td.name {padding-left: 40px; background-position: 24px 50%;}
-tr.idnt-3 td.subject, tr.idnt-3 td.name {padding-left: 56px; background-position: 40px 50%;}
-tr.idnt-4 td.subject, tr.idnt-4 td.name {padding-left: 72px; background-position: 56px 50%;}
-tr.idnt-5 td.subject, tr.idnt-5 td.name {padding-left: 88px; background-position: 72px 50%;}
-tr.idnt-6 td.subject, tr.idnt-6 td.name {padding-left: 104px; background-position: 88px 50%;}
-tr.idnt-7 td.subject, tr.idnt-7 td.name {padding-left: 120px; background-position: 104px 50%;}
-tr.idnt-8 td.subject, tr.idnt-8 td.name {padding-left: 136px; background-position: 120px 50%;}
-tr.idnt-9 td.subject, tr.idnt-9 td.name {padding-left: 152px; background-position: 136px 50%;}
+tr.idnt td.subject, tr.idnt td.name {background: url(/chevron-right-idnt.svg) no-repeat 2px 50%;}
+tr.idnt-1 td.subject, tr.idnt-1 td.name {padding-left: 24px; background-position: 4px 50%;}
+tr.idnt-2 td.subject, tr.idnt-2 td.name {padding-left: 40px; background-position: 20px 50%;}
+tr.idnt-3 td.subject, tr.idnt-3 td.name {padding-left: 56px; background-position: 36px 50%;}
+tr.idnt-4 td.subject, tr.idnt-4 td.name {padding-left: 72px; background-position: 52px 50%;}
+tr.idnt-5 td.subject, tr.idnt-5 td.name {padding-left: 88px; background-position: 68px 50%;}
+tr.idnt-6 td.subject, tr.idnt-6 td.name {padding-left: 104px; background-position: 84px 50%;}
+tr.idnt-7 td.subject, tr.idnt-7 td.name {padding-left: 120px; background-position: 100px 50%;}
+tr.idnt-8 td.subject, tr.idnt-8 td.name {padding-left: 136px; background-position: 116px 50%;}
+tr.idnt-9 td.subject, tr.idnt-9 td.name {padding-left: 152px; background-position: 132px 50%;}
table.issue-report {table-layout:fixed;}
table.issue-report tr.total, table.issue-report-detailed tr.total { font-weight: bold; border-top:2px solid #d0d7de;}
@@ -409,7 +433,7 @@ tr.entry.file td.filename a { margin-left: 26px; }
tr.entry.file td.filename_no_report a { margin-left: 16px; }
tr span.expander, .gantt_subjects div > span.expander {margin-left: 0; cursor: pointer;}
-.gantt_subjects div > span .icon-gravatar {float: none;}
+.gantt_subjects .avatar {margin-right: 4px;}
.gantt_subjects div.project-name a, .gantt_subjects div.version-name a {margin-left: 4px;}
tr.changeset { height: 20px }
@@ -432,16 +456,12 @@ tr.message td.last_message { font-size: 93%; white-space: nowrap; }
tr.message.sticky td.subject { font-weight: bold; }
tr.message td.subject:not(:has(.icon)) { padding-left: 20px; }
-body.avatars-on #replies .message.reply {padding-left: 32px;}
-#replies .reply:target h4.reply-header {background-color:#DDEEFF;}
-#replies h4 img.gravatar {margin-left:-32px;}
-
tr.version.closed, tr.version.closed a { color: #999; }
tr.version:not(.shared) td.name { padding-left: 20px; }
tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
-#principals_for_new_member .icon-user {background:transparent;}
-#principals_for_new_member svg, #principals_for_new_member img {margin-right: 4px;}
+#principals_for_new_member .icon-user, #users_for_watcher .icon-user {background:transparent;}
+#principals_for_new_member svg, #principals_for_new_member .avatar {margin-right: 4px;}
tr.user td {width:13%;white-space: nowrap;}
td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
@@ -545,9 +565,9 @@ body.controller-gantts fieldset#options > div > div {
td.center {text-align:center;}
#watchers select {width: 95%; display: block;}
-#watchers img.gravatar {margin: 0 4px 2px 0;}
+#watchers .avatar {margin: 0 4px 2px 0;}
#watchers svg.icon-svg {margin: 0 2px 2px 0;}
-#users_for_watcher img.gravatar {padding-bottom: 2px; margin-right: 4px;}
+#users_for_watcher .avatar {padding-bottom: 2px; margin-right: 4px;}
#users_for_watcher svg {margin-right: 4px;}
#users_for_watcher span.icon-user {display: inline;}
@@ -589,7 +609,6 @@ div.square {
}
.contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;}
.contextual input, .contextual select {font-size:0.9em;}
-.message .contextual, #comments .contextual { margin-top: 0; }
.splitcontent {overflow: auto; display: flex; flex-wrap: wrap;}
.splitcontentleft {flex: 1; margin-right: 5px;}
@@ -605,9 +624,9 @@ select {
-o-appearance: none;
appearance: none;
background-color: #fff;
- background-image: url(/arrow_down.png);
+ background-image: url(/chevron-down.svg);
background-repeat: no-repeat;
- background-position: calc(100% - 7px) 50%;
+ background-position: calc(100% - 2px) 50%;
padding-right: 20px;
}
input[type="file"] {border: 0; padding-left: 0; padding-right: 0; height: initial; background-color: initial; }
@@ -679,6 +698,36 @@ div.issue .attribute.string_cf .value .wiki p {margin-top: 0; margin-bottom: 0;}
div.issue .attribute.text_cf .value .wiki p:first-of-type {margin-top: 0;}
div.issue.overdue .due-date .value { color: #c22; }
body.controller-issues h2.inline-flex {padding-right: 0}
+div#sticky-issue-header {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ background-color: white;
+ border-bottom: 1px solid #d0d7de;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
+ font-size: 0.8125rem;
+ align-items: center;
+ z-index: 1000;
+ padding: 10px 6px;
+ border-radius: 0px;
+}
+div#sticky-issue-header.is-visible {
+ display: flex;
+}
+div#sticky-issue-header .issue-heading {
+ flex-shrink: 0;
+ white-space: nowrap;
+ margin-right: 6px;
+}
+div#sticky-issue-header .subject {
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-grow: 1;
+}
#issue_tree table.issues, #relations table.issues {border: 0;}
#issue_tree table.issues td, #relations table.issues td {border: 0;}
@@ -741,29 +790,15 @@ div#issue-changesets div.changeset {border-bottom: 1px solid #ddd; padding: 4px;
div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
.changeset-comments {margin-bottom:1em;}
-div.journal .contextual {margin-top: 0;}
-div.journal.private-notes .wiki {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
-div.journal ul.details, ul.revision-info {color:#959595; margin-bottom: 1.5em;}
-div.journal ul.details a, ul.revision-info a {color:#70A7CD;}
-div.journal ul.details a:hover, ul.revision-info a:hover {color:#D14848;}
-body.avatars-on div.journal {padding-left:32px;}
-div.journal h4 img.gravatar {margin-left:-32px;}
-div.journal span.update-info {color: #666; font-size: 0.9em;}
-
#update {margin-bottom: 1.4em;}
-#history .tab-content {
- padding: 0 8px;
- margin-bottom: 10px;
- border-right: 1px solid #d0d7de;
- border-bottom: 1px solid #d0d7de;
- border-left: 1px solid #d0d7de;
- border-radius: 0 0 3px 3px / 0 0 3px 3px;
- box-shadow: 0 1px 2px rgba(0,0,0,0.05);
-}
-
-#history div:target h4.note-header {background-color:#DDEEFF;}
#history p.nodata {display: none;}
+/* Prevent content from being hidden behind a #sticky-issue-header when scrolling via anchor links. */
+.controller-issues.action-show div.wiki a[name],
+.controller-issues.action-show #history div[id^="note-"],
+.controller-issues.action-show #history div[id^="change-"] {
+ scroll-margin-top: 50px;
+}
div#activity dl, #search-results { margin-left: 2em; }
div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 22px; font-size: 0.8125rem;}
@@ -885,7 +920,11 @@ ul.projects div.description ul li {list-style-type:initial;}
background-image: none;
padding-left: 0;
}
-#projects-index ul.projects div.root svg {
+#projects-index ul.projects .icon-bookmarked-project svg,
+#projects-index ul.projects .my-project svg {
+ margin-left: 4px;
+}
+#projects-index ul.projects div.root .icon-bookmarked-project svg, #projects-index ul.projects div.root .my-project svg {
stroke-width: 2;
margin-bottom: 10px;
}
@@ -896,7 +935,12 @@ ul.projects div.description ul li {list-style-type:initial;}
background-image: none;
padding-left: 0;
}
-#projects-index a.project ~ svg, table.projects tr.project td.name svg {
+#projects-index div.wiki p {
+ margin-top: 0px;
+}
+
+table.projects td.name .icon-bookmarked-project svg,
+table.projects td.name .my-project svg {
margin-left: 4px;
}
@@ -1060,17 +1104,14 @@ input#months { width: 46px; }
.jstBlock .jstTabs { padding-right: 6px; }
.jstBlock .wiki-preview { padding: 2px; }
-.jstBlock .wiki-preview p:first-child { padding-top: 0 !important; margin-top: 0 !important;}
-.jstBlock .wiki-preview p:last-child { padding-bottom: 0 !important; margin-bottom: 0 !important;}
+.jstBlock .wiki-preview > p:first-child { padding-top: 0 !important; margin-top: 0 !important;}
+.jstBlock .wiki-preview > p:last-child { padding-bottom: 0 !important; margin-bottom: 0 !important;}
.tabular .wiki-preview, .tabular .jstTabs {width: 95%;}
.tabular.settings .wiki-preview, .tabular.settings .jstTabs { width: 99%; }
.tabular.settings .wiki-preview p {padding-left: 0 !important}
.tabular .wiki-preview p {
min-height: initial;
- padding: 0;
- padding-top: 1em !important;
- padding-bottom: 1em !important;
overflow: initial;
}
@@ -1111,13 +1152,29 @@ span.required {color: #bb0000;}
.attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
.attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
/* ToDo: delete this line when legacy icons are deleted */
-.attachments_fields , #existing-attachments .icon-attachment {background-image: none; padding-left: 0}
+.attachments_fields .icon-attachment, #existing-attachments .icon-attachment {background-image: none; padding-left: 0}
.attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; }
.tabular input.filename {max-width:75% !important;}
-.attachments_fields input.filename {height:1.8em;}
-.attachments_fields .ajax-waiting input.filename {background:url(/hourglass.png) no-repeat 0px 50%;}
-.attachments_fields .ajax-loading input.filename {background:url(/loading.gif) no-repeat 0px 50%;}
.attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
+.attachments_fields input.filename {
+ height:1.8em;
+ padding-left: 3px;
+ padding-right: 0;
+}
+.attachments_fields .ajax-waiting {
+ padding-left: 16px;
+ background:url(/hourglass-empty.svg) no-repeat 0px 50%;
+}
+.attachments_fields .ajax-waiting .svg-attachment {
+ display: none;
+}
+.attachments_fields .ajax-loading {
+ padding-left: 16px;
+ background: url(/loading.gif) no-repeat 0px 50%;
+}
+.attachments_fields .ajax-loading .svg-attachment {
+ display: none;
+}
a.remove-upload:hover {text-decoration:none !important;}
.existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
@@ -1131,13 +1188,10 @@ div.attachments span.author { font-size: 0.9em; color: #888; }
div.thumbnails {margin:0.6em;}
div.thumbnail {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
div.thumbnail img {margin: 3px; vertical-align: middle;}
-#history div.thumbnails {margin-left: 2em;}
p.other-formats { text-align: right; font-size:0.9em; color: #666; }
.other-formats span + span:before { content: "| "; }
-a.atom { background: url(/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
-
em.info {font-style:normal;display:block;font-size:90%;color:#888;}
em.info.error {padding-left:20px; background:url(/exclamation.png) no-repeat 0 50%;}
@@ -1256,11 +1310,45 @@ div.flash.warning svg.icon-svg, .conflict svg.icon-svg {
color: #A6750C;
}
+.warning .oauth-permissions { display:inline-block;text-align:left; }
+.warning .oauth-permissions p { margin-top:0;-webkit-margin-before:0;}
+
#errorExplanation ul { font-size: 0.9em;}
#errorExplanation h2, #errorExplanation p { display: none; }
.conflict-details {font-size:93%;}
+/***** CommonMark Alerts *****/
+.markdown-alert {
+ border-left: 4px solid;
+ padding-left: 0.6em;
+ margin: 1em 0;
+}
+
+.markdown-alert-title {
+ font-weight: bold;
+}
+
+.markdown-alert-tip { border-color: #5db651; }
+.markdown-alert-tip .markdown-alert-title { color: #005f00; }
+.markdown-alert-tip .markdown-alert-title svg {stroke: #005f00; }
+
+.markdown-alert-important { border-color: #800080; }
+.markdown-alert-important .markdown-alert-title { color: #4b006e; }
+.markdown-alert-important .markdown-alert-title svg { stroke: #4b006e; }
+
+.markdown-alert-caution { border-color: #c22; }
+.markdown-alert-caution .markdown-alert-title { color: #880000; }
+.markdown-alert-caution .markdown-alert-title svg { stroke: #880000; }
+
+.markdown-alert-warning { border-color: #e4bc4b; }
+.markdown-alert-warning .markdown-alert-title { color: #a7760c; }
+.markdown-alert-warning .markdown-alert-title svg { stroke: #a7760c; }
+
+.markdown-alert-note { border-color: #169; }
+.markdown-alert-note .markdown-alert-title { color: #1e40af; }
+.markdown-alert-note .markdown-alert-title svg { stroke: #1e40af; }
+
/***** Ajax indicator ******/
#ajax-indicator {
position: absolute; /* fixed not supported by IE */
@@ -1351,7 +1439,7 @@ p.cal.legend span {display:flex;}
.tooltip span.tip{display: none; text-align:left;}
.tooltip span.tip a { color: #169 !important; }
-.tooltip span.tip img.gravatar {
+.tooltip span.tip .avatar {
float: none;
margin: 0;
}
@@ -1535,11 +1623,20 @@ div.wiki .external {
div.wiki a {word-wrap: break-word;}
div.wiki a.new {color: #b73535;}
-div.wiki p {line-height: 1.6;}
+div.wiki p {
+ line-height: 1.6;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ padding: 0;
+}
div.wiki ul, div.wiki ol {margin-bottom:1em;}
div.wiki li {line-height: 1.6; margin-bottom: 0.125rem;}
div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
+div.wiki div.pre-wrapper {
+ position: relative;
+}
+
div.wiki pre {
margin: 1em 1em 1em 1.6em;
padding: 8px;
@@ -1557,6 +1654,22 @@ div.wiki *:not(pre)>code, div.wiki>code {
border-radius: 0.1em;
}
+div.pre-wrapper a.copy-pre-content-link {
+ position: absolute;
+ top: 3px;
+ right: calc(1em + 3px);
+ cursor: pointer;
+ display: none;
+ border-radius: 3px;
+ background: #fff;
+ border: 1px solid #ccc;
+ padding: 0px 3px 3px 3px;
+}
+
+div.pre-wrapper:hover a.copy-pre-content-link {
+ display: block;
+}
+
div.wiki ul.toc {
background-color: #ffffdd;
border: 1px solid #e4e4e4;
@@ -1590,10 +1703,11 @@ a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor, h4:hover a.wiki-anchor, h5:hover a.wiki-anchor, h6:hover a.wiki-anchor { display: inline; color: #ddd; }
div.wiki img {vertical-align:middle; max-width:100%;}
-div.wiki>.task-list {
- padding-left: 0px;
+
+div.wiki>.contains-task-list {
+ padding-left: 0;
}
-div.wiki .task-list {
+div.wiki .contains-task-list {
list-style-type: none;
}
div.wiki .task-list input.task-list-item-checkbox {
@@ -1625,6 +1739,7 @@ div.wiki .task-list input.task-list-item-checkbox {
.handle {cursor: move;}
#my-page .list th.checkbox, #my-page .list td.checkbox {display:none;}
+
/***** Gantt chart *****/
table.gantt-table {
width: 100%;
@@ -1678,10 +1793,7 @@ table.gantt-table td {
width: 100%;
}
.gantt_subjects div.issue-subject:hover { background-color:#ffffdd; }
-.gantt_selected_column_content { padding-left: 3px; padding-right: 3px;}
-.gantt_subjects .issue-subject img.icon-gravatar {
- margin: 2px 5px 0px 2px;
-}
+.gantt_selected_column_content > div { padding-left: 3px; box-sizing: border-box; }
.gantt_hdr_selected_column_name {
position: absolute;
@@ -1702,6 +1814,21 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
width: 49px;
}
+td.gantt_watcher_users_column div.issue_watcher_users ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+td.gantt_watcher_users_column div.issue_watcher_users ul li {
+ display: inline;
+}
+
+td.gantt_watcher_users_column div.issue_watcher_users ul li:not(:last-child)::after {
+ content: ', ';
+ white-space: pre;
+}
+
.task {
position: absolute;
height:8px;
@@ -1738,6 +1865,68 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
+/***** User events (ex: journal, notes, replies, comments) *****/
+.journals h4.journal-header {
+ background-color: #f6f7f8;
+ border-bottom: 0;
+ padding: 8px;
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+}
+
+.journals h4.journal-header .update-info {
+ color: #666;
+ font-size: 0.9em;
+}
+
+.journals h4.journal-header .badge {
+ position: static;
+}
+
+.journals div:target h4.journal-header {
+ background-color:#DDEEFF;
+}
+
+.journals .journal-content {
+ padding-left: 8px;
+ margin-bottom: 1.2em;
+}
+
+.journals .journal .journal-content .wiki {
+ margin-left: 0.6em;
+}
+
+.journals .private-notes {
+ border-left: 2px solid #d22;
+}
+
+.journals .journal-meta, .journals .journal-actions {
+ display: inline-flex;
+ gap: 10px;
+}
+
+.journals .journal-meta .journal-link {
+ color: #555;
+}
+
+.journals .journal-actions .reaction-button-wrapper {
+ display: inline-flex;
+}
+
+.journals .journal-details, ul.revision-info {
+ color: #959595;
+ margin-bottom: 1.5em;
+}
+
+.journals .journal-details a, ul.revision-info a {
+ color: #70A7CD;
+}
+
+.journals .journal-details a:hover, ul.revision-info a:hover {
+ color: #D14848;
+}
+
/***** Badges *****/
.badge {
position:relative;
@@ -1799,10 +1988,15 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
flex-shrink: 0;
}
-a.icon:hover svg, a.icon-only:hover svg {
+a.icon:hover .icon-svg, a.icon-only:hover .icon-svg, span.icon-actions:hover .icon-svg {
stroke: #c61a1a;
}
+a.icon:hover .icon-svg-filled, a.icon-only:hover .icon-svg-filled {
+ stroke: none;
+ fill: #c61a1a;
+}
+
svg.icon-ok {
stroke: #5db651;
}
@@ -1826,6 +2020,11 @@ svg.icon-svg {
vertical-align: middle;
}
+svg.icon-svg-filled {
+ fill: #169;
+ stroke: none;
+}
+
svg.s20 {
width: 1.25rem;
height: 1.25rem;
@@ -1836,6 +2035,11 @@ svg.s18 {
height: 1.125rem;
}
+svg.s16 {
+ width: 1rem;
+ height: 1rem;
+}
+
svg.s14 {
width: 0.875rem;
height: 0.875rem;
@@ -1977,21 +2181,16 @@ tr.ui-sortable-helper { border:1px solid #e4e4e4; }
.contextual>*:not(:first-child), .buttons>.icon:not(:first-child), .contextual .journal-actions>*:not(:first-child) { margin-left: 5px; }
-img.gravatar {
- vertical-align: middle;
- border-radius: 20%;
-}
-
-div.issue img.gravatar {
+div.issue .avatar {
float: left;
margin: 0 12px 6px 0;
}
-div.gravatar-with-child {
+div.avatar-with-child {
position: relative;
}
-div.gravatar-with-child > img.gravatar:nth-child(2) {
+div.avatar-with-child > .avatar:nth-child(2) {
position: absolute;
top: 30px;
left: 30px;
@@ -1999,12 +2198,11 @@ div.gravatar-with-child > img.gravatar:nth-child(2) {
border: 2px solid rgba(255, 255, 255, 0.9);
}
-h2 img.gravatar, h3 img.gravatar {margin-right: 4px;}
+h2 .avatar, h3 .avatar {margin-right: 4px;}
h4 img.gravatar {margin: -2px 4px -4px 0;}
+/*# TODO: check where this rule is still used*/
td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
-#activity dt img.gravatar {margin: 0 1em 0 0;}
-/* Used on 12px Gravatar img tags without the icon background */
-.icon-gravatar {float: left; margin-right: 4px;}
+#activity dt .avatar {margin: 0 1em 0 0;}
#activity dt, .journal {clear: left;}
@@ -2027,6 +2225,134 @@ color: #555; text-shadow: 1px 1px 0 #fff;
img.filecontent.image {background-image: url(/transparent.png);}
+/* Avatar styles */
+.avatar {
+ border-radius: 20%;
+ display: inline-flex;
+ vertical-align: middle;
+}
+
+span[role="img"].avatar {
+ font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ align-items: center;
+ display: inline-flex;
+ font-size: calc(24px * .4);
+ justify-content: center;
+ user-select: none;
+ font-weight: 700;
+}
+.avatar.s13 {
+ block-size: 13px;
+ inline-size: 13px;
+}
+span[role="img"].avatar.s13 {
+ font-size: calc(16px * .3);
+}
+.avatar.s16 {
+ block-size: 16px;
+ inline-size: 16px;
+}
+span[role="img"].avatar.s16 {
+ font-size: calc(16px * .4);
+}
+.avatar.s22 {
+ block-size: 22px;
+ inline-size: 22px;
+}
+span[role="img"].avatar.s22 {
+ font-size: calc(22px * .4);
+}
+.avatar.s24 {
+ block-size: 24px;
+ inline-size: 24px;
+}
+span[role="img"].avatar.s24 {
+ font-size: calc(24px * .4);
+}
+.avatar.s40 {
+ block-size: 40px;
+ inline-size: 40px;
+}
+span[role="img"].avatar.s40 {
+ font-size: calc(40px * .4);
+}
+.avatar.s50 {
+ block-size: 50px;
+ inline-size: 50px;
+}
+span[role="img"].avatar.s50 {
+ font-size: calc(50px * .4);
+}
+
+.avatar-color-0 {
+ background-color: #880000;
+ color: #FFFFFF;
+}
+.avatar-color-1 {
+ background-color: #ff0000;
+ color: #000000;
+}
+.avatar-color-2 {
+ background-color: #00ff00;
+ color: #000000;
+}
+.avatar-color-3 {
+ background-color: #008800;
+ color: #FFFFFF;
+}
+.avatar-color-4 {
+ background-color: #0000ff;
+ color: #FFFFFF;
+}
+.avatar-color-5 {
+ background-color: #000088;
+ color: #FFFFFF;
+}
+.avatar-color-6 {
+ background-color: #ff8800;
+ color: #000000;
+}
+.avatar-color-7 {
+ background-color: #ff0088;
+ color: #000000;
+}
+
+/* Reaction styles */
+.reaction-button:hover, .reaction-button:active {
+ text-decoration: none;
+}
+.reaction-button .icon-label {
+ margin-left: 3px;
+ margin-bottom: -1px;
+ font-weight: bold;
+}
+.reaction-button.readonly {
+ cursor: default;
+}
+.reaction-button.readonly .icon-svg {
+ stroke: #999;
+}
+.reaction-button.readonly .icon-label {
+ color: #999;
+}
+div.issue.details .reaction {
+ float: right;
+ font-size: 0.9em;
+ margin-top: 0.5em;
+ margin-left: 10px;
+ clear: right;
+}
+div.message .reaction {
+ float: right;
+ font-size: 0.9em;
+ margin-left: 10px;
+}
+div.news .reaction {
+ float: right;
+ font-size: 0.9em;
+ margin-left: 10px;
+}
+
/* Custom JQuery styles */
.ui-autocomplete, .ui-menu {
border-radius: 2px;
diff --git a/app/assets/stylesheets/responsive.css b/app/assets/stylesheets/responsive.css
index ec580037c..b3e8bddd8 100644
--- a/app/assets/stylesheets/responsive.css
+++ b/app/assets/stylesheets/responsive.css
@@ -385,7 +385,7 @@
list-style: none;
}
- .flyout-menu #watchers {
+ .flyout-menu #watchers, .flyout-menu .queries {
display: -webkit-flex;
display: -webkit-box;
display: flex;
@@ -402,11 +402,11 @@
order: 3;
}
- .flyout-menu #watchers h3 {
+ #sidebar-wrapper {
margin-left: -8px;
}
- .flyout-menu #watchers ul li {
+ .flyout-menu #watchers ul li, .flyout-menu ul.queries li {
display: -webkit-flex;
display: -webkit-box;
display: flex;
@@ -418,6 +418,16 @@
-webkit-align-items: center;
-webkit-box-align: center;
align-items: center;
+ border-top: 1px solid rgba(255,255,255,.1);
+ }
+
+ .flyout-menu #watchers ul li a, .flyout-menu ul.queries li a {
+ border-top: none;
+ }
+
+ .flyout-menu ul.queries li a.icon-clear-query {
+ flex-shrink: 0;
+ padding-right: 8px;
}
.flyout-menu ul li a {
@@ -440,7 +450,7 @@
color: white;
}
- .flyout-menu .icon svg {
+ .flyout-menu .icon svg, .flyout-menu .icon-only svg {
stroke: white;
}
@@ -771,6 +781,10 @@
width: 100%; /* let subject have one full width column */
}
+ #issue_tree .issue:has(.buttons a) > td.subject, #relations .issue:has(.buttons a) > td.subject {
+ padding-right: 40px;
+ }
+
#issue_tree .issue > td:not(.subject), #relations .issue > td:not(.subject) {
width: 20%; /* three columns for all cells that are not subject */
}
@@ -844,6 +858,19 @@
font-size: 1.1em;
text-align: left;
}
+
+ /* Sticky issue header */
+ /* When project-jump.drdn is visible in mobile layout, offset the sticky header by its height to prevent it from being hidden. */
+ div#sticky-issue-header {
+ top: 64px;
+ }
+
+ /* Prevent content from being hidden behind #sticky-issue-header and project-jump when scrolling via anchor links. */
+ .controller-issues.action-show div.wiki a[name],
+ .controller-issues.action-show #history div[id^="note-"],
+ .controller-issues.action-show #history div[id^="change-"] {
+ scroll-margin-top: 114px;
+ }
}
@media all and (max-width: 599px) {
diff --git a/app/assets/stylesheets/rtl.css b/app/assets/stylesheets/rtl.css
index 550071462..20a2a73dc 100644
--- a/app/assets/stylesheets/rtl.css
+++ b/app/assets/stylesheets/rtl.css
@@ -21,12 +21,29 @@ h1, h2, h3, h4 {padding:2px 00px 1px 10px;}
#main-menu {left:auto;right:6px;margin-right:0;margin-left:-500px;}
#main-menu li {float:right;margin:0px 0px 0px 2px;}
-#admin-menu a {padding-left:0;padding-right:20px;}
+#admin-menu a:not(:has(svg)) {padding-left:0;padding-right:20px;}
+
+#sidebar {float:left; padding-right: 20px; padding-left: 8px; border-left: 0; border-right: 1px solid #d0d7de;}
+* html #sidebar hr {left: auto; right: -6px;}
+
+#main.collapsedsidebar #sidebar {
+ padding-left: 0;
+ padding-right: 20px;
+}
-#sidebar {float:left;}
-* html #sidebar hr{ left: auto; right: -6px; }
#sidebar .contextual { margin-right: 0; margin-left: 1em;}
-#sidebar ul li {margin: 0px 0px 0px 2px;}
+#sidebar ul li {margin: 0 0 0 2px;}
+#sidebar #sidebar-switch-panel {
+ margin-left: 0;
+ margin-right: -20px;
+ padding-left: 28px;
+ padding-right: 0;
+}
+
+#sidebar #sidebar-switch-panel #sidebar-switch-button {
+ padding-right: 0;
+ padding-left: 28px;
+}
#content {border-right:0 solid #ddd; border-left:1px solid #ddd;}
* html #content{padding-right:0;}
@@ -38,7 +55,7 @@ div.modal p.buttons {text-align:left;}
/***** Links *****/
#sidebar a.selected {padding:1px 2px 2px 3px; margin-left:0px; margin-right:-2px;}
-#admin-menu a.selected {padding-left:0!important; padding-right:20px!important; background-position:right 2px 40%;}
+#admin-menu a.selected:not(:has(svg)) {padding-left:0!important; padding-right:20px!important; background-position:right 2px 40%;}
a.collapsible:not(:has(svg)) {padding-left:0px; padding-right:12px; background: url(/arrow_down.png) no-repeat right 0px top 50%;}
a.collapsible.collapsed:not(:has(svg)) {background-image: url(/arrow_left.png);}
@@ -122,7 +139,7 @@ div.projects h3 {padding-left:0px; padding-right:20px;}
#watchers li {margin: 0px 0px 0px 2px; padding: 0px 0px 0px 0px;}
#watchers img.gravatar {margin: 0 0 2px 4px;}
-span.search_for_watchers a, span.add_attachment a {padding-left:px; padding-right:16px; background: url(/bullet_add.png) no-repeat right 50%; }
+span.search_for_watchers a:not(:has(svg)), span.add_attachment a:not(:has(svg)) {padding-left:0px; padding-right:16px; background: url(/bullet_add.png) no-repeat right 50%; }
div.square {float:right;}
.contextual {float:left; padding-left:0px; padding-right:10px;}
@@ -213,7 +230,7 @@ fieldset#notified_events .parent {padding-left:0px; padding-right:20px; }
.attachments_fields input.description {margin-left:0px; margin-right:4px;}
.attachments_fields input.filename {background:url(/attachment.png) no-repeat right 1px top 50%; padding-left:0px; padding-right:18px;}
-.attachments_fields .ajax-waiting input.filename {background:url(/hourglass.png) no-repeat right top 50%;}
+.attachments_fields .ajax-waiting input.filename {background:url(/hourglass-empty.svg) no-repeat right top 50%;}
.attachments_fields .ajax-loading input.filename {background:url(/loading.gif) no-repeat right top 50%;}
.attachments_fields div.ui-progressbar {margin: 2px 8px -5px 0;}
@@ -221,7 +238,7 @@ a.remove-upload {background: url(/delete.png) no-repeat right 1px top 50%; paddi
div.thumbnails div {margin-right:0px; margin-left:2px;}
-p.other-formats { text-align:left; }
+p.other-formats, p.query-totals { text-align:left; }
a.atom { background: url(/feed.png) no-repeat right 1px top 50%; padding: 2px 16px 3px 0; }
@@ -231,7 +248,7 @@ table.members td.name {padding-right: 20px; padding-left:0; }
table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(/group.png) no-repeat right 50%;}
input.autocomplete {
- background: #fff url(/magnifier.png) no-repeat right 2px top 50%; padding-left:0px !important; padding-right:20px !important;
+ background: #fff url(/search.svg) no-repeat right 2px top 50%; padding-left:0px !important; padding-right:20px !important;
}
.role-visibility {padding-right:2em; padding-left:0;}
@@ -355,6 +372,10 @@ a.wiki-anchor {margin-left:0px; margin-right:6px;}
padding-left:0; padding-right:20px;
}
+svg.icon-svg.icon-rtl {
+ transform: scaleX(-1);
+}
+
div.issue img.gravatar {
float: right;
margin: 0 0 0 6px;
diff --git a/app/assets/stylesheets/wiki_syntax.css b/app/assets/stylesheets/wiki_syntax.css
index d326a3293..89b117419 100644
--- a/app/assets/stylesheets/wiki_syntax.css
+++ b/app/assets/stylesheets/wiki_syntax.css
@@ -1,19 +1,33 @@
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: normal;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
}
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-Italic-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: italic;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Italic.woff2") format("woff2");
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-BoldItalic.woff2") format("woff2");
+ font-weight: 700;
+ font-style: italic;
+ font-display: swap;
}
:root {
@@ -58,3 +72,14 @@ a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
.syntaxhl .s1 { background-color: #fff0f0 }
span.more_info { font-weight: normal; }
+
+.markdown-alert {
+ border-left: 4px solid;
+ padding-left: 10px;
+ margin-left: 10px;
+}
+.markdown-alert-title {
+ font-weight: bold;
+}
+.markdown-alert-note { border-color: #169; }
+.markdown-alert-note .markdown-alert-title { color: #1e40af; } \ No newline at end of file
diff --git a/app/assets/stylesheets/wiki_syntax_detailed.css b/app/assets/stylesheets/wiki_syntax_detailed.css
index 7d7c30f53..ad3c8c65f 100644
--- a/app/assets/stylesheets/wiki_syntax_detailed.css
+++ b/app/assets/stylesheets/wiki_syntax_detailed.css
@@ -1,19 +1,33 @@
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: normal;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
}
@font-face {
- font-family: "Noto Sans";
- src: url("/NotoSans-Italic-VariableFont_wdth,wght.woff2") format("woff2");
- font-weight: 100 900;
- font-stretch: 75% 125%;
- font-style: italic;
- font-display: swap;
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-Italic.woff2") format("woff2");
+ font-weight: 400;
+ font-style: italic;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: "Noto Sans";
+ src: url("/NotoSans-BoldItalic.woff2") format("woff2");
+ font-weight: 700;
+ font-style: italic;
+ font-display: swap;
}
:root {
@@ -49,3 +63,23 @@ table.list td { background-color: #f5f5f5; vertical-align: middle; padding: 0.3e
.syntaxhl .o { color: #333333 }
.syntaxhl .s2 { background-color: #fff0f0 }
.syntaxhl .si { background-color: #eeeeee }
+
+
+.markdown-alert {
+ border-left: 4px solid;
+ padding-left: 10px;
+ margin-left: 20px;
+}
+.markdown-alert-title {
+ font-weight: bold;
+}
+.markdown-alert-tip { border-color: #5db651; }
+.markdown-alert-tip .markdown-alert-title { color: #005f00; }
+.markdown-alert-important { border-color: #800080; }
+.markdown-alert-important .markdown-alert-title { color: #4b006e; }
+.markdown-alert-caution { border-color: #c22; }
+.markdown-alert-caution .markdown-alert-title { color: #880000; }
+.markdown-alert-warning { border-color: #e4bc4b; }
+.markdown-alert-warning .markdown-alert-title { color: #a7760c; }
+.markdown-alert-note { border-color: #169; }
+.markdown-alert-note .markdown-alert-title { color: #1e40af; } \ No newline at end of file
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 074392709..a01d5c75f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -131,6 +131,14 @@ class ApplicationController < ActionController::Base
if (key = api_key_from_request)
# Use API key
user = User.find_by_api_key(key)
+ elsif access_token = Doorkeeper.authenticate(request)
+ # Oauth
+ if access_token.accessible?
+ user = User.active.find_by_id(access_token.resource_owner_id)
+ user.oauth_scope = access_token.scopes.all.map(&:to_sym)
+ else
+ doorkeeper_render_error
+ end
elsif /\ABasic /i.match?(request.authorization.to_s)
# HTTP Basic, either username/password or API key/random
authenticate_with_http_basic do |username, password|
diff --git a/app/controllers/auto_completes_controller.rb b/app/controllers/auto_completes_controller.rb
index 2982447e9..77105c8e8 100644
--- a/app/controllers/auto_completes_controller.rb
+++ b/app/controllers/auto_completes_controller.rb
@@ -26,7 +26,7 @@ class AutoCompletesController < ApplicationController
status = params[:status].to_s
issue_id = params[:issue_id].to_s
- scope = Issue.cross_project_scope(@project, params[:scope]).visible
+ scope = Issue.cross_project_scope(@project, params[:scope]).includes(:tracker).visible
scope = scope.open(status == 'o') if status.present?
scope = scope.where.not(:id => issue_id.to_i) if issue_id.present?
if q.present?
diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb
index 5159bf540..8b26bee73 100644
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb
@@ -51,6 +51,8 @@ class MessagesController < ApplicationController
offset(@reply_pages.offset).
to_a
+ Message.preload_reaction_details(@replies)
+
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => false if request.xhr?
end
@@ -134,7 +136,7 @@ class MessagesController < ApplicationController
def preview
message = @board.messages.find_by_id(params[:id])
- @text = params[:text] ? params[:text] : nil
+ @text = params[:text] || nil
@previewed = message
render :partial => 'common/preview'
end
diff --git a/app/controllers/news_controller.rb b/app/controllers/news_controller.rb
index 06240e359..dd6bade24 100644
--- a/app/controllers/news_controller.rb
+++ b/app/controllers/news_controller.rb
@@ -67,8 +67,10 @@ class NewsController < ApplicationController
end
def show
- @comments = @news.comments.to_a
+ @comments = @news.comments.preload(:commented).to_a
@comments.reverse! if User.current.wants_comments_in_reverse_order?
+
+ Comment.preload_reaction_details(@comments)
end
def new
diff --git a/app/controllers/oauth2_applications_controller.rb b/app/controllers/oauth2_applications_controller.rb
new file mode 100644
index 000000000..107af2ec0
--- /dev/null
+++ b/app/controllers/oauth2_applications_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+#
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+#
+class Oauth2ApplicationsController < Doorkeeper::ApplicationsController
+ private
+
+ def application_params
+ params[:doorkeeper_application] ||= {}
+ params[:doorkeeper_application][:scopes] ||= []
+
+ scopes = Redmine::AccessControl.public_permissions.map{|p| p.name.to_s}
+
+ if params[:doorkeeper_application][:scopes].is_a?(Array)
+ scopes |= params[:doorkeeper_application][:scopes]
+ else
+ scopes |= params[:doorkeeper_application][:scopes].split(/\s+/)
+ end
+ params[:doorkeeper_application][:scopes] = scopes.join(' ')
+ super
+ end
+end
diff --git a/app/controllers/previews_controller.rb b/app/controllers/previews_controller.rb
index 9dd228a3d..744daa7c8 100644
--- a/app/controllers/previews_controller.rb
+++ b/app/controllers/previews_controller.rb
@@ -26,7 +26,7 @@ class PreviewsController < ApplicationController
if @issue
@previewed = @issue
end
- @text = params[:text] ? params[:text] : nil
+ @text = params[:text] || nil
render :partial => 'common/preview'
end
@@ -34,12 +34,12 @@ class PreviewsController < ApplicationController
if params[:id].present? && news = News.visible.find_by_id(params[:id])
@previewed = news
end
- @text = params[:text] ? params[:text] : nil
+ @text = params[:text] || nil
render :partial => 'common/preview'
end
def text
- @text = params[:text] ? params[:text] : nil
+ @text = params[:text] || nil
render :partial => 'common/preview'
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index f9a390c58..2a42c99ed 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -176,7 +176,7 @@ class ProjectsController < ApplicationController
respond_to do |format|
format.html do
@principals_by_role = @project.principals_by_role
- @subprojects = @project.children.visible.to_a
+ @subprojects = @project.leaf? ? [] : @project.children.visible.to_a
@news = @project.news.limit(5).includes(:author, :project).reorder("#{News.table_name}.created_on DESC").to_a
with_subprojects = Setting.display_subprojects_issues?
@trackers = @project.rolled_up_trackers(with_subprojects).visible
diff --git a/app/controllers/reactions_controller.rb b/app/controllers/reactions_controller.rb
new file mode 100644
index 000000000..71b37e5f8
--- /dev/null
+++ b/app/controllers/reactions_controller.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+class ReactionsController < ApplicationController
+ before_action :require_login
+
+ before_action :check_enabled
+ before_action :set_object, :authorize_reactable
+
+ def create
+ respond_to do |format|
+ format.js do
+ @object.reactions.find_or_create_by!(user: User.current)
+ end
+ format.any { head :not_found }
+ end
+ end
+
+ def destroy
+ respond_to do |format|
+ format.js do
+ reaction = @object.reactions.by(User.current).find_by(id: params[:id])
+ reaction&.destroy
+ end
+ format.any { head :not_found }
+ end
+ end
+
+ private
+
+ def check_enabled
+ render_403 unless Setting.reactions_enabled?
+ end
+
+ def set_object
+ object_type = params[:object_type]
+
+ unless Redmine::Reaction::REACTABLE_TYPES.include?(object_type)
+ render_403
+ return
+ end
+
+ @object = object_type.constantize.find(params[:object_id])
+ end
+
+ def authorize_reactable
+ render_403 unless Redmine::Reaction.editable?(@object, User.current)
+ end
+end
diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb
index 9be7878ce..d6a13daf2 100644
--- a/app/controllers/repositories_controller.rb
+++ b/app/controllers/repositories_controller.rb
@@ -160,7 +160,15 @@ class RepositoriesController < ApplicationController
# Force the download
send_opt = {:filename => filename_for_content_disposition(@path.split('/').last)}
send_type = Redmine::MimeType.of(@path)
- send_opt[:type] = send_type.to_s if send_type
+ case send_type
+ when nil
+ # No MIME type detected. Let Rails use the default type.
+ when 'application/javascript'
+ # Avoid ActionController::InvalidCrossOriginRequest exception by setting non-JS content type
+ send_opt[:type] = 'text/plain'
+ else
+ send_opt[:type] = send_type
+ end
send_opt[:disposition] = disposition(@path)
send_data @repository.cat(@path, @rev), send_opt
else
diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb
index dfe7c2b8f..89f9ee497 100644
--- a/app/controllers/roles_controller.rb
+++ b/app/controllers/roles_controller.rb
@@ -99,7 +99,15 @@ class RolesController < ApplicationController
begin
@role.destroy
rescue
- flash[:error] = l(:error_can_not_remove_role)
+ flash[:error] = l(:error_can_not_remove_role)
+
+ if @role.members.present?
+ projects = Project.joins(members: :member_roles).where(member_roles: { role_id: @role.id }).distinct.sorted
+ links = projects.map do |p|
+ view_context.link_to(p, settings_project_path(p, tab: 'members'))
+ end.join(', ')
+ flash[:error] += l(:error_can_not_remove_role_reason_members_html, projects: links)
+ end
end
redirect_to roles_path
end
diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb
index d52b43ba3..328d3e56e 100644
--- a/app/controllers/versions_controller.rb
+++ b/app/controllers/versions_controller.rb
@@ -51,7 +51,7 @@ class VersionsController < ApplicationController
if @selected_tracker_ids.any? && @versions.any?
issues = Issue.visible.
includes(:project, :tracker).
- preload(:status, :priority, :fixed_version).
+ preload(:status, :priority, :fixed_version, {:assigned_to => :email_address}).
where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
@issues_by_version = issues.group_by(&:fixed_version)
@@ -69,7 +69,7 @@ class VersionsController < ApplicationController
format.html do
@issues = @version.fixed_issues.visible.
includes(:status, :tracker, :priority).
- preload(:project).
+ preload(:project, {:assigned_to => :email_address}).
reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
to_a
end
diff --git a/app/controllers/wiki_controller.rb b/app/controllers/wiki_controller.rb
index 36b90da77..bcb3b0891 100644
--- a/app/controllers/wiki_controller.rb
+++ b/app/controllers/wiki_controller.rb
@@ -240,6 +240,7 @@ class WikiController < ApplicationController
# don't load text
@versions = @page.content.versions.
select("id, author_id, comments, updated_on, version").
+ preload(:author).
reorder('version DESC').
limit(@version_pages.per_page + 1).
offset(@version_pages.offset).
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 9bb26bdec..ab418fb38 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -128,7 +128,7 @@ module ApplicationHelper
# * :download - Force download (default: false)
def link_to_attachment(attachment, options={})
text = options.delete(:text) || attachment.filename
- icon = options.fetch(:icon, false)
+ icon = options.delete(:icon)
if options.delete(:download)
route_method = :download_named_attachment_url
@@ -436,7 +436,7 @@ module ApplicationHelper
def format_changeset_comments(changeset, options={})
method = options[:short] ? :short_comments : :comments
- textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
+ textilizable changeset, method, project: changeset.project, formatting: Setting.commit_logs_formatting?
end
def due_date_distance_in_words(date)
@@ -518,6 +518,8 @@ module ApplicationHelper
def render_flash_messages
s = +''
flash.each do |k, v|
+ next unless v.is_a?(String)
+
s << content_tag('div', notice_icon(k) + v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
end
s.html_safe
@@ -789,7 +791,7 @@ module ApplicationHelper
end
def other_formats_links(&)
- concat('<p class="other-formats">'.html_safe + l(:label_export_to))
+ concat('<p class="other-formats hide-when-print">'.html_safe + l(:label_export_to))
yield Redmine::Views::OtherFormatsBuilder.new(self)
concat('</p>'.html_safe)
end
@@ -1386,7 +1388,7 @@ module ApplicationHelper
<|
$)
}x
- HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
+ HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/im unless const_defined?(:HEADING_RE)
def parse_sections(text, project, obj, attr, only_path, options)
return unless options[:edit_section_links]
@@ -1607,7 +1609,7 @@ module ApplicationHelper
# Helper to render JSON in views
def raw_json(arg)
- arg.to_json.to_s.gsub('/', '\/').html_safe
+ arg.to_json.gsub('/', '\/').html_safe
end
def back_url_hidden_field_tag
@@ -1805,7 +1807,7 @@ module ApplicationHelper
if Setting.wiki_tablesort_enabled?
tags << javascript_include_tag('tablesort-5.2.1.min.js', 'tablesort-5.2.1.number.min.js')
end
- tags << javascript_include_tag('application', 'responsive')
+ tags << javascript_include_tag('application-legacy', 'responsive')
unless User.current.pref.warn_on_leaving_unsaved == '0'
warn_text = escape_javascript(l(:text_warn_on_leaving_unsaved))
tags <<
@@ -1917,6 +1919,14 @@ module ApplicationHelper
end
end
+ def heads_for_i18n
+ javascript_tag(
+ "rm = window.rm || {};" \
+ "rm.I18n = rm.I18n || {};" \
+ "rm.I18n = Object.freeze({buttonCopy: '#{l(:button_copy)}'});"
+ )
+ end
+
def heads_for_auto_complete(project)
data_sources = autocomplete_data_sources(project)
javascript_tag(
@@ -1934,7 +1944,7 @@ module ApplicationHelper
def copy_object_url_link(url)
link_to_function(
- sprite_icon('copy-link', l(:button_copy_link)), 'copyTextToClipboard(this);',
+ sprite_icon('copy-link', l(:button_copy_link)), 'copyDataClipboardTextToClipboard(this);',
class: 'icon icon-copy-link',
data: {'clipboard-text' => url}
)
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index b39427bda..67567fd8d 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -38,24 +38,9 @@ module AvatarsHelper
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
def avatar(user, options = {})
if Setting.gravatar_enabled?
- options[:default] = Setting.gravatar_default
- options[:class] = GravatarHelper::DEFAULT_OPTIONS[:class] + " " + options[:class] if options[:class]
- email = nil
- if user.respond_to?(:mail)
- email = user.mail
- options[:title] = user.name unless options[:title]
- elsif user.to_s =~ %r{<(.+?)>}
- email = $1
- end
- if email.present?
- gravatar(email.to_s.downcase, options) rescue nil
- elsif user.is_a?(AnonymousUser)
- anonymous_avatar(options)
- elsif user.is_a?(Group)
- group_avatar(options)
- else
- nil
- end
+ gravatar_avatar_tag(user, options)
+ elsif user.respond_to?(:initials)
+ initials_avatar_tag(user, options)
else
''
end
@@ -69,8 +54,6 @@ module AvatarsHelper
end
end
- private
-
def anonymous_avatar(options={})
image_tag 'anonymous.png', GravatarHelper::DEFAULT_OPTIONS.except(:default, :rating, :ssl).merge(options)
end
@@ -78,4 +61,42 @@ module AvatarsHelper
def group_avatar(options={})
image_tag 'group.png', GravatarHelper::DEFAULT_OPTIONS.except(:default, :rating, :ssl).merge(options)
end
+
+ private
+
+ def gravatar_avatar_tag(user, options)
+ options[:default] = Setting.gravatar_default
+ options[:class] = [GravatarHelper::DEFAULT_OPTIONS[:class], options[:class]].compact.join(' ')
+
+ email = extract_email_from_user(user)
+
+ if user.respond_to?(:mail)
+ options[:title] ||= user.name
+ options[:initials] = user.initials if options[:default] == "initials" && user.initials.present?
+ end
+
+ if email.present?
+ gravatar(email.to_s.downcase, options) rescue nil
+ elsif user.is_a?(AnonymousUser)
+ anonymous_avatar(options)
+ elsif user.is_a?(Group)
+ group_avatar(options)
+ end
+ end
+
+ def initials_avatar_tag(user, options)
+ size = (options.delete(:size) || GravatarHelper::DEFAULT_OPTIONS[:size]).to_i
+
+ css_class = ["avatar-color-#{user.id % 8}", 'avatar', "s#{size}", options[:class]].compact.join(' ')
+
+ content_tag('span', user.initials, role: 'img', class: css_class, title: options[:title])
+ end
+
+ def extract_email_from_user(user)
+ if user.respond_to?(:mail)
+ user.mail
+ elsif user.to_s =~ %r{<(.+?)>}
+ $1
+ end
+ end
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 948f68752..6afb84537 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -21,10 +21,10 @@ module IconsHelper
DEFAULT_ICON_SIZE = "18"
DEFAULT_SPRITE = "icons"
- def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil)
+ def sprite_icon(icon_name, label = nil, icon_only: false, size: DEFAULT_ICON_SIZE, style: :outline, css_class: nil, sprite: DEFAULT_SPRITE, plugin: nil, rtl: false)
sprite = plugin ? "plugin_assets/#{plugin}/#{sprite}.svg" : "#{sprite}.svg"
- svg_icon = svg_sprite_icon(icon_name, size: size, css_class: css_class, sprite: sprite)
+ svg_icon = svg_sprite_icon(icon_name, size: size, style: style, css_class: css_class, sprite: sprite, rtl: rtl)
if label
label_classes = ["icon-label"]
@@ -36,23 +36,23 @@ module IconsHelper
end
end
- def file_icon(entry, name, **options)
+ def file_icon(entry, name, **)
if entry.is_dir?
- sprite_icon("folder", name, **options)
+ sprite_icon("folder", name, **)
else
icon_name = icon_for_mime_type(Redmine::MimeType.css_class_of(name))
- sprite_icon(icon_name, name, **options)
+ sprite_icon(icon_name, name, **)
end
end
- def principal_icon(principal, **options)
+ def principal_icon(principal, **)
raise ArgumentError, "First argument has to be a Principal, was #{principal.inspect}" unless principal.is_a?(Principal)
principal_class = principal.class.name.downcase
- sprite_icon('group', **options) if ['groupanonymous', 'groupnonmember', 'group'].include?(principal_class)
+ sprite_icon('group', **) if ['groupanonymous', 'groupnonmember', 'group'].include?(principal_class)
end
- def activity_event_type_icon(event_type, **options)
+ def activity_event_type_icon(event_type, **)
icon_name = case event_type
when 'reply'
'comments'
@@ -64,7 +64,7 @@ module IconsHelper
event_type
end
- sprite_icon(icon_name, **options)
+ sprite_icon(icon_name, **)
end
def scm_change_icon(action, name, **options)
@@ -79,7 +79,7 @@ module IconsHelper
sprite_icon(icon_name, name, size: 14)
end
- def notice_icon(type, **options)
+ def notice_icon(type, **)
icon_name = case type
when 'notice'
'checked'
@@ -87,14 +87,16 @@ module IconsHelper
'warning'
end
- sprite_icon(icon_name, **options)
+ sprite_icon(icon_name, **)
end
private
- def svg_sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, sprite: DEFAULT_SPRITE, css_class: nil)
+ def svg_sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, style: :outline, sprite: DEFAULT_SPRITE, css_class: nil, rtl: false)
css_classes = "s#{size} icon-svg"
+ css_classes += " icon-svg-filled" if style == :filled
css_classes += " #{css_class}" unless css_class.nil?
+ css_classes += " icon-rtl" if rtl
content_tag(
:svg,
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 6586a1b7e..ce3607a5d 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -22,6 +22,7 @@ module IssuesHelper
include Redmine::Export::PDF::IssuesPdfHelper
include IssueStatusesHelper
include QueriesHelper
+ include ReactionsHelper
def issue_list(issues, &)
ancestors = []
diff --git a/app/helpers/journals_helper.rb b/app/helpers/journals_helper.rb
index 6c22fc4ca..0ddbc34b8 100644
--- a/app/helpers/journals_helper.rb
+++ b/app/helpers/journals_helper.rb
@@ -19,6 +19,7 @@
module JournalsHelper
include Redmine::QuoteReply::Helper
+ include ReactionsHelper
# Returns the attachments of a journal that are displayed as thumbnails
def journal_thumbnail_attachments(journal)
@@ -40,10 +41,12 @@ module JournalsHelper
)
end
+ links << reaction_button(journal)
+
if journal.notes.present?
if options[:reply_links]
url = quoted_issue_path(issue, :journal_id => journal, :journal_indice => indice)
- links << quote_reply(url, "#journal-#{journal.id}-notes", icon_only: true)
+ links << quote_reply_button(url: url, icon_only: true)
end
if journal.editable_by?(User.current)
links << link_to(sprite_icon('edit', l(:button_edit)),
@@ -66,7 +69,8 @@ module JournalsHelper
end
def render_notes(issue, journal, options={})
- content_tag('div', textilizable(journal, :notes), :id => "journal-#{journal.id}-notes", :class => "wiki")
+ content_tag('div', textilizable(journal, :notes),
+ id: "journal-#{journal.id}-notes", class: "wiki journal-note", data: { quote_reply_target: 'content' })
end
def render_private_notes_indicator(journal)
diff --git a/app/helpers/messages_helper.rb b/app/helpers/messages_helper.rb
index fd9ba3bcb..92f788d0c 100644
--- a/app/helpers/messages_helper.rb
+++ b/app/helpers/messages_helper.rb
@@ -19,4 +19,5 @@
module MessagesHelper
include Redmine::QuoteReply::Helper
+ include ReactionsHelper
end
diff --git a/app/helpers/news_helper.rb b/app/helpers/news_helper.rb
index a5c50fdfd..cd7b6734a 100644
--- a/app/helpers/news_helper.rb
+++ b/app/helpers/news_helper.rb
@@ -18,4 +18,5 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module NewsHelper
+ include ReactionsHelper
end
diff --git a/app/helpers/principal_memberships_helper.rb b/app/helpers/principal_memberships_helper.rb
index d9caf4f50..e69324247 100644
--- a/app/helpers/principal_memberships_helper.rb
+++ b/app/helpers/principal_memberships_helper.rb
@@ -38,27 +38,27 @@ module PrincipalMembershipsHelper
end
end
- def new_principal_membership_path(principal, *args)
+ def new_principal_membership_path(principal, *)
if principal.is_a?(Group)
- new_group_membership_path(principal, *args)
+ new_group_membership_path(principal, *)
else
- new_user_membership_path(principal, *args)
+ new_user_membership_path(principal, *)
end
end
- def edit_principal_membership_path(principal, *args)
+ def edit_principal_membership_path(principal, *)
if principal.is_a?(Group)
- edit_group_membership_path(principal, *args)
+ edit_group_membership_path(principal, *)
else
- edit_user_membership_path(principal, *args)
+ edit_user_membership_path(principal, *)
end
end
- def principal_membership_path(principal, membership, *args)
+ def principal_membership_path(principal, membership, *)
if principal.is_a?(Group)
- group_membership_path(principal, membership, *args)
+ group_membership_path(principal, membership, *)
else
- user_membership_path(principal, membership, *args)
+ user_membership_path(principal, membership, *)
end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 01a5452f7..bae1c4e3a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -80,8 +80,8 @@ module ProjectsHelper
classes += %w(icon icon-bookmarked-project) if bookmarked_project_ids.include?(project.id)
s = link_to_project(project, {}, :class => classes.uniq.join(' '))
- s << sprite_icon('user', l(:label_my_projects), icon_only: true) if User.current.member_of?(project)
- s << sprite_icon('bookmarked', l(:label_my_bookmarks), icon_only: true) if bookmarked_project_ids.include?(project.id)
+ s << tag.span(sprite_icon('user', l(:label_my_projects), icon_only: true), class: 'icon-only icon-user my-project') if User.current.member_of?(project)
+ s << tag.span(sprite_icon('bookmarked', l(:label_my_bookmarks), icon_only: true), class: 'icon-only icon-bookmarked-project') if bookmarked_project_ids.include?(project.id)
if project.description.present?
s << content_tag('div', textilizable(project.short_description, :project => project), :class => 'wiki description')
end
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index ca7168f27..3aef7083a 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -169,7 +169,7 @@ module QueriesHelper
group_name = format_object(group)
end
group_name ||= ""
- group_count = result_count_by_group ? result_count_by_group[group] : nil
+ group_count = result_count_by_group&.[](group)
group_totals = totals_by_group.map {|column, t| total_tag(column, t[group] || 0)}.join(" ").html_safe
end
end
diff --git a/app/helpers/reactions_helper.rb b/app/helpers/reactions_helper.rb
new file mode 100644
index 000000000..e02e1c9f9
--- /dev/null
+++ b/app/helpers/reactions_helper.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+module ReactionsHelper
+ # Maximum number of users to display in the reaction button tooltip
+ DISPLAY_REACTION_USERS_LIMIT = 10
+
+ def reaction_button(object)
+ return unless Redmine::Reaction.visible?(object, User.current)
+
+ detail = object.reaction_detail
+
+ user_reaction = detail.user_reaction
+ count = detail.reaction_count
+ visible_user_names = detail.visible_users.take(DISPLAY_REACTION_USERS_LIMIT).map(&:name)
+
+ tooltip = build_reaction_tooltip(visible_user_names, count)
+
+ if Redmine::Reaction.editable?(object, User.current)
+ if user_reaction.present?
+ reaction_button_reacted(object, user_reaction, count, tooltip)
+ else
+ reaction_button_not_reacted(object, count, tooltip)
+ end
+ else
+ reaction_button_readonly(object, count, tooltip)
+ end
+ end
+
+ def reaction_id_for(object)
+ dom_id(object, :reaction)
+ end
+
+ private
+
+ def reaction_button_reacted(object, reaction, count, tooltip)
+ reaction_button_wrapper object do
+ link_to(
+ sprite_icon('thumb-up-filled', count.nonzero?, style: :filled),
+ reaction_path(reaction, object_type: object.class.name, object_id: object),
+ remote: true, method: :delete,
+ class: ['icon', 'reaction-button', 'reacted'],
+ title: tooltip
+ )
+ end
+ end
+
+ def reaction_button_not_reacted(object, count, tooltip)
+ reaction_button_wrapper object do
+ link_to(
+ sprite_icon('thumb-up', count.nonzero?),
+ reactions_path(object_type: object.class.name, object_id: object),
+ remote: true, method: :post,
+ class: 'icon reaction-button',
+ title: tooltip
+ )
+ end
+ end
+
+ def reaction_button_readonly(object, count, tooltip)
+ reaction_button_wrapper object do
+ tag.span(class: 'icon reaction-button readonly', title: tooltip) do
+ sprite_icon('thumb-up', count.nonzero?)
+ end
+ end
+ end
+
+ def reaction_button_wrapper(object, &)
+ tag.span(class: 'reaction-button-wrapper', data: { 'reaction-button-id': reaction_id_for(object) }, &)
+ end
+
+ def build_reaction_tooltip(visible_user_names, count)
+ return if count.zero?
+
+ display_user_names = visible_user_names.dup
+ others = count - visible_user_names.size
+
+ if others.positive?
+ display_user_names << I18n.t(:reaction_text_x_other_users, count: others)
+ end
+
+ display_user_names.to_sentence(locale: I18n.locale)
+ end
+end
diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb
index 6390ecbdb..f8df59b00 100644
--- a/app/helpers/reports_helper.rb
+++ b/app/helpers/reports_helper.rb
@@ -34,9 +34,9 @@ module ReportsHelper
a
end
- def aggregate_link(data, criteria, *args)
+ def aggregate_link(data, criteria, *)
a = aggregate data, criteria
- a > 0 ? link_to(h(a), *args) : '-'
+ a > 0 ? link_to(h(a), *) : '-'
end
def aggregate_path(project, field, row, options={})
diff --git a/app/helpers/routes_helper.rb b/app/helpers/routes_helper.rb
index f5d6dbd38..a27ea783e 100644
--- a/app/helpers/routes_helper.rb
+++ b/app/helpers/routes_helper.rb
@@ -20,83 +20,83 @@
module RoutesHelper
# Returns the path to project issues or to the cross-project
# issue list if project is nil
- def _project_issues_path(project, *args)
+ def _project_issues_path(project, *)
if project
- project_issues_path(project, *args)
+ project_issues_path(project, *)
else
- issues_path(*args)
+ issues_path(*)
end
end
- def _project_issues_url(project, *args)
+ def _project_issues_url(project, *)
if project
- project_issues_url(project, *args)
+ project_issues_url(project, *)
else
- issues_url(*args)
+ issues_url(*)
end
end
- def _project_news_path(project, *args)
+ def _project_news_path(project, *)
if project
- project_news_index_path(project, *args)
+ project_news_index_path(project, *)
else
- news_index_path(*args)
+ news_index_path(*)
end
end
- def _new_project_issue_path(project, *args)
+ def _new_project_issue_path(project, *)
if project
- new_project_issue_path(project, *args)
+ new_project_issue_path(project, *)
else
- new_issue_path(*args)
+ new_issue_path(*)
end
end
- def _project_calendar_path(project, *args)
- project ? project_calendar_path(project, *args) : issues_calendar_path(*args)
+ def _project_calendar_path(project, *)
+ project ? project_calendar_path(project, *) : issues_calendar_path(*)
end
- def _project_gantt_path(project, *args)
- project ? project_gantt_path(project, *args) : issues_gantt_path(*args)
+ def _project_gantt_path(project, *)
+ project ? project_gantt_path(project, *) : issues_gantt_path(*)
end
- def _time_entries_path(project, issue, *args)
+ def _time_entries_path(project, issue, *)
if project
- project_time_entries_path(project, *args)
+ project_time_entries_path(project, *)
else
- time_entries_path(*args)
+ time_entries_path(*)
end
end
- def _report_time_entries_path(project, issue, *args)
+ def _report_time_entries_path(project, issue, *)
if project
- report_project_time_entries_path(project, *args)
+ report_project_time_entries_path(project, *)
else
- report_time_entries_path(*args)
+ report_time_entries_path(*)
end
end
- def _new_time_entry_path(project, issue, *args)
+ def _new_time_entry_path(project, issue, *)
if issue
- new_issue_time_entry_path(issue, *args)
+ new_issue_time_entry_path(issue, *)
elsif project
- new_project_time_entry_path(project, *args)
+ new_project_time_entry_path(project, *)
else
- new_time_entry_path(*args)
+ new_time_entry_path(*)
end
end
# Returns the path to bulk update issues or to issue path
# if only one issue is selected for bulk update
- def _bulk_update_issues_path(issue, *args)
+ def _bulk_update_issues_path(issue, *)
if issue
- issue_path(issue, *args)
+ issue_path(issue, *)
else
- bulk_update_issues_path(*args)
+ bulk_update_issues_path(*)
end
end
- def board_path(board, *args)
- project_board_path(board.project, board, *args)
+ def board_path(board, *)
+ project_board_path(board.project, board, *)
end
end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 39a836a03..c1f989805 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -244,6 +244,7 @@ module SettingsHelper
['Mystery man', 'mm'],
['Retro', 'retro'],
['Robohash', 'robohash'],
- ['Wavatars', 'wavatar']]
+ ['Wavatars', 'wavatar'],
+ ['Initials', 'initials']]
end
end
diff --git a/app/helpers/watchers_helper.rb b/app/helpers/watchers_helper.rb
index 6e6366846..bfed8adf2 100644
--- a/app/helpers/watchers_helper.rb
+++ b/app/helpers/watchers_helper.rb
@@ -48,7 +48,9 @@ module WatchersHelper
def watchers_list(object)
remove_allowed = User.current.allowed_to?(:"delete_#{object.class.name.underscore}_watchers", object.project)
content = ''.html_safe
- lis = object.watcher_users.sorted.collect do |user|
+ scope = object.watcher_users
+ scope = scope.includes(:email_address) if Setting.gravatar_enabled?
+ lis = scope.sorted.collect do |user|
s = ''.html_safe
s << avatar(user, :size => "16").to_s if user.is_a?(User)
s << link_to_principal(user, class: user.class.to_s.downcase)
diff --git a/app/javascript/application.js b/app/javascript/application.js
new file mode 100644
index 000000000..72ef077f8
--- /dev/null
+++ b/app/javascript/application.js
@@ -0,0 +1 @@
+import "controllers"
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js
new file mode 100644
index 000000000..f898b4e6b
--- /dev/null
+++ b/app/javascript/controllers/application.js
@@ -0,0 +1,8 @@
+import { Application } from '@hotwired/stimulus'
+
+const application = Application.start()
+
+application.debug = false
+window.Stimulus = application
+
+export { application }
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
new file mode 100644
index 000000000..6ffb4e9ee
--- /dev/null
+++ b/app/javascript/controllers/index.js
@@ -0,0 +1,3 @@
+import { application } from "controllers/application"
+import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
+eagerLoadControllersFrom("controllers", application)
diff --git a/app/javascript/controllers/quote_reply_controller.js b/app/javascript/controllers/quote_reply_controller.js
new file mode 100644
index 000000000..137c019ac
--- /dev/null
+++ b/app/javascript/controllers/quote_reply_controller.js
@@ -0,0 +1,224 @@
+import { Controller } from '@hotwired/stimulus'
+import TurndownService from 'turndown'
+import { post } from '@rails/request.js'
+
+class QuoteExtractor {
+ constructor(targetElement) {
+ this.targetElement = targetElement;
+ this.selection = window.getSelection();
+ }
+
+ get isSelected() {
+ return this.selection.containsNode(this.targetElement, true);
+ }
+
+ static extract(targetElement) {
+ return new QuoteExtractor(targetElement).extract();
+ }
+
+ extract() {
+ const range = this.retriveSelectedRange();
+
+ if (!range) {
+ return null;
+ }
+
+ if (!this.targetElement.contains(range.startContainer)) {
+ range.setStartBefore(this.targetElement);
+ }
+ if (!this.targetElement.contains(range.endContainer)) {
+ range.setEndAfter(this.targetElement);
+ }
+
+ return range;
+ }
+
+ retriveSelectedRange() {
+ if (!this.isSelected) {
+ return null;
+ }
+
+ // Retrive the first range that intersects with the target element.
+ // NOTE: Firefox allows to select multiple ranges in the document.
+ for (let i = 0; i < this.selection.rangeCount; i++) {
+ let range = this.selection.getRangeAt(i);
+ if (range.intersectsNode(this.targetElement)) {
+ return range;
+ }
+ }
+ return null;
+ }
+}
+
+class QuoteTextFormatter {
+ format(selectedRange) {
+ if (!selectedRange) {
+ return null;
+ }
+
+ const fragment = document.createElement('div');
+ fragment.appendChild(selectedRange.cloneContents());
+
+ // Remove all unnecessary anchor elements
+ fragment.querySelectorAll('a.wiki-anchor').forEach(e => e.remove());
+
+ const html = this.adjustLineBreaks(fragment.innerHTML);
+
+ const result = document.createElement('div');
+ result.innerHTML = html;
+
+ // Replace continuous line breaks with a single line break and remove tab characters
+ return result.textContent
+ .trim()
+ .replace(/\t/g, '')
+ .replace(/\n+/g, "\n");
+ }
+
+ adjustLineBreaks(html) {
+ return html
+ .replace(/<\/(h1|h2|h3|h4|div|p|li|tr)>/g, "\n</$1>")
+ .replace(/<br>/g, "\n")
+ }
+}
+
+class QuoteCommonMarkFormatter {
+ format(selectedRange) {
+ if (!selectedRange) {
+ return null;
+ }
+
+ const htmlFragment = this.extractHtmlFragmentFrom(selectedRange);
+ const preparedHtml = this.prepareHtml(htmlFragment);
+
+ return this.convertHtmlToCommonMark(preparedHtml);
+ }
+
+ extractHtmlFragmentFrom(range) {
+ const fragment = document.createElement('div');
+ const ancestorNodeName = range.commonAncestorContainer.nodeName;
+
+ if (ancestorNodeName == 'CODE' || ancestorNodeName == '#text') {
+ fragment.appendChild(this.wrapPreCode(range));
+ } else {
+ fragment.appendChild(range.cloneContents());
+ }
+
+ return fragment;
+ }
+
+ // When only the content within the `<code>` element is selected,
+ // the HTML within the selection range does not include the `<pre><code>` element itself.
+ // To create a complete code block, wrap the selected content with the `<pre><code>` tags.
+ //
+ // selected contentes => <pre><code class="ruby">selected contents</code></pre>
+ wrapPreCode(range) {
+ const rangeAncestor = range.commonAncestorContainer;
+
+ let codeElement = null;
+
+ if (rangeAncestor.nodeName == 'CODE') {
+ codeElement = rangeAncestor;
+ } else {
+ codeElement = rangeAncestor.parentElement.closest('code');
+ }
+
+ if (!codeElement) {
+ return range.cloneContents();
+ }
+
+ const pre = document.createElement('pre');
+ const code = codeElement.cloneNode(false);
+
+ code.appendChild(range.cloneContents());
+ pre.appendChild(code);
+
+ return pre;
+ }
+
+ convertHtmlToCommonMark(html) {
+ const turndownService = new TurndownService({
+ codeBlockStyle: 'fenced',
+ headingStyle: 'atx'
+ });
+
+ turndownService.addRule('del', {
+ filter: ['del'],
+ replacement: content => `~~${content}~~`
+ });
+
+ turndownService.addRule('checkList', {
+ filter: node => {
+ return node.type === 'checkbox' && node.parentNode.nodeName === 'LI';
+ },
+ replacement: (content, node) => {
+ return node.checked ? '[x]' : '[ ]';
+ }
+ });
+
+ // Table does not maintain its original format,
+ // and the text within the table is displayed as it is
+ //
+ // | A | B | C |
+ // |---|---|---|
+ // | 1 | 2 | 3 |
+ // =>
+ // A B C
+ // 1 2 3
+ turndownService.addRule('table', {
+ filter: ['td', 'th'],
+ replacement: (content, node) => {
+ const separator = node.parentElement.lastElementChild === node ? '' : ' ';
+ return content + separator;
+ }
+ });
+ turndownService.addRule('tableHeading', {
+ filter: ['thead', 'tbody', 'tfoot', 'tr'],
+ replacement: (content, _node) => content
+ });
+ turndownService.addRule('tableRow', {
+ filter: ['tr'],
+ replacement: (content, _node) => {
+ return content + '\n'
+ }
+ });
+
+ return turndownService.turndown(html);
+ }
+
+ prepareHtml(htmlFragment) {
+ // Remove all anchor elements.
+ // <h1>Title1<a href="#Title" class="wiki-anchor">¶</a></h1> => <h1>Title1</h1>
+ htmlFragment.querySelectorAll('a.wiki-anchor').forEach(e => e.remove());
+
+ // Convert code highlight blocks to CommonMark format code blocks.
+ // <code class="ruby" data-language="ruby"> => <code class="language-ruby" data-language="ruby">
+ htmlFragment.querySelectorAll('code[data-language]').forEach(e => {
+ e.classList.replace(e.dataset['language'], 'language-' + e.dataset['language'])
+ });
+
+ return htmlFragment.innerHTML;
+ }
+}
+
+export default class extends Controller {
+ static targets = [ 'content' ];
+
+ quote(event) {
+ const { url, textFormatting } = event.params;
+ const selectedRange = QuoteExtractor.extract(this.contentTarget);
+
+ let formatter;
+
+ if (textFormatting === 'common_mark') {
+ formatter = new QuoteCommonMarkFormatter();
+ } else {
+ formatter = new QuoteTextFormatter();
+ }
+
+ post(url, {
+ body: JSON.stringify({ quote: formatter.format(selectedRange) }),
+ contentType: 'application/json',
+ responseKind: 'script'
+ });
+ }
+}
diff --git a/app/javascript/controllers/sticky_issue_header_controller.js b/app/javascript/controllers/sticky_issue_header_controller.js
new file mode 100644
index 000000000..aebc7d2dc
--- /dev/null
+++ b/app/javascript/controllers/sticky_issue_header_controller.js
@@ -0,0 +1,22 @@
+import { Controller } from "@hotwired/stimulus";
+
+export default class extends Controller {
+ static targets = ["original", "stickyHeader"];
+
+ connect() {
+ if (!this.originalTarget || !this.stickyHeaderTarget) return;
+
+ this.observer = new IntersectionObserver(
+ ([entry]) => {
+ this.stickyHeaderTarget.classList.toggle("is-visible", !entry.isIntersecting);
+ },
+ { threshold: 0 }
+ );
+
+ this.observer.observe(this.originalTarget);
+ }
+
+ disconnect() {
+ this.observer?.disconnect();
+ }
+}
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 79eb59748..1716537af 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -19,6 +19,8 @@
class Comment < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
+
belongs_to :commented, :polymorphic => true, :counter_cache => true
belongs_to :author, :class_name => 'User'
@@ -28,6 +30,8 @@ class Comment < ApplicationRecord
safe_attributes 'comments'
+ delegate :visible?, to: :commented
+
def comments=(arg)
self.content = arg
end
@@ -36,6 +40,10 @@ class Comment < ApplicationRecord
content
end
+ def project
+ commented.respond_to?(:project) ? commented.project : nil
+ end
+
private
def send_notification
diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb
index ec8c5de8d..d14b67bdb 100644
--- a/app/models/custom_field.rb
+++ b/app/models/custom_field.rb
@@ -101,7 +101,8 @@ class CustomField < ApplicationRecord
'version_status',
'extensions_allowed',
'full_width_layout',
- 'thousands_delimiter'
+ 'thousands_delimiter',
+ 'ratio_interval'
)
def copy_from(arg, options={})
@@ -335,12 +336,12 @@ class CustomField < ApplicationRecord
args.include?(field_format)
end
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == 'url_pattern'
attr_name = "url"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
def css_classes
diff --git a/app/models/email_address.rb b/app/models/email_address.rb
index 69ae8a066..de8c86531 100644
--- a/app/models/email_address.rb
+++ b/app/models/email_address.rb
@@ -74,7 +74,7 @@ class EmailAddress < ApplicationRecord
# Returns true if domain belongs to domains list.
def self.domain_in?(domain, domains)
- domain = domain.downcase
+ domain = domain.to_s.downcase
domains = domains.to_s.split(/[\s,]+/) unless domains.is_a?(Array)
domains.reject(&:blank?).map(&:downcase).any? do |s|
s.start_with?('.') ? domain.end_with?(s) : domain == s
@@ -150,6 +150,10 @@ class EmailAddress < ApplicationRecord
def validate_email_domain
domain = address.partition('@').last
+ # Skip domain validation if the email does not contain a domain part,
+ # to avoid an incomplete error message like "domain not allowed ()"
+ return if domain.empty?
+
return if self.class.valid_domain?(domain)
if User.current.logged?
diff --git a/app/models/group.rb b/app/models/group.rb
index ea5454558..300b59b46 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -94,12 +94,12 @@ class Group < Principal
destroy_all
end
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == 'lastname'
attr_name = "name"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
def self.anonymous
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2d004a78d..576840843 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -25,6 +25,7 @@ class Issue < ApplicationRecord
before_validation :clear_disabled_fields
before_save :set_parent_id
include Redmine::NestedSet::IssueNestedSet
+ include Redmine::Reaction::Reactable
belongs_to :project
belongs_to :tracker
@@ -268,7 +269,7 @@ class Issue < ApplicationRecord
end
alias :base_reload :reload
- def reload(*args)
+ def reload(*)
@workflow_rule_by_attribute = nil
@assignable_versions = nil
@relations = nil
@@ -277,7 +278,7 @@ class Issue < ApplicationRecord
@total_estimated_hours = nil
@last_updated_by = nil
@last_notes = nil
- base_reload(*args)
+ base_reload(*)
end
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
@@ -469,7 +470,7 @@ class Issue < ApplicationRecord
end
# Overrides assign_attributes so that project and tracker get assigned first
- def assign_attributes(new_attributes, *args)
+ def assign_attributes(new_attributes, *)
return if new_attributes.nil?
attrs = new_attributes.dup
@@ -480,7 +481,7 @@ class Issue < ApplicationRecord
send :"#{attr}=", attrs.delete(attr)
end
end
- super(attrs, *args)
+ super(attrs, *)
end
def attributes=(new_attributes)
@@ -916,7 +917,8 @@ class Issue < ApplicationRecord
result = journals.
preload(:details).
preload(:user => :email_address).
- reorder(:created_on, :id).to_a
+ reorder(:created_on, :id).
+ to_a
result.each_with_index {|j, i| j.indice = i + 1}
@@ -927,6 +929,9 @@ class Issue < ApplicationRecord
end
Journal.preload_journals_details_custom_fields(result)
result.select! {|journal| journal.notes? || journal.visible_details.any?}
+
+ Journal.preload_reaction_details(result)
+
result
end
@@ -1170,7 +1175,7 @@ class Issue < ApplicationRecord
if leaf?
spent_hours
else
- self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
+ self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f
end
end
@@ -1203,11 +1208,7 @@ class Issue < ApplicationRecord
end
def last_notes
- if @last_notes
- @last_notes
- else
- journals.visible.where.not(notes: '').reorder(:id => :desc).first.try(:notes)
- end
+ @last_notes || journals.visible.where.not(notes: '').reorder(:id => :desc).first.try(:notes)
end
# Preloads relations for a collection of issues
diff --git a/app/models/issue_relation.rb b/app/models/issue_relation.rb
index e55875e4d..80af22b89 100644
--- a/app/models/issue_relation.rb
+++ b/app/models/issue_relation.rb
@@ -22,9 +22,9 @@ class IssueRelation < ApplicationRecord
class Relations < Array
include Redmine::I18n
- def initialize(issue, *args)
+ def initialize(issue, *)
@issue = issue
- super(*args)
+ super(*)
end
def to_s(*args)
diff --git a/app/models/journal.rb b/app/models/journal.rb
index 179e60c24..12f2beec8 100644
--- a/app/models/journal.rb
+++ b/app/models/journal.rb
@@ -19,6 +19,7 @@
class Journal < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
belongs_to :journalized, :polymorphic => true
# added as a quick fix to allow eager loading of the polymorphic association
@@ -157,8 +158,8 @@ class Journal < ApplicationRecord
end
end
- def visible?(*args)
- journalized.visible?(*args)
+ def visible?(*)
+ journalized.visible?(*)
end
# Returns a string of css classes
diff --git a/app/models/mail_handler.rb b/app/models/mail_handler.rb
index b6858d96a..5d246a572 100644
--- a/app/models/mail_handler.rb
+++ b/app/models/mail_handler.rb
@@ -55,8 +55,8 @@ class MailHandler < ActionMailer::Base
end
# Receives an email and rescues any exception
- def self.safe_receive(*args)
- receive(*args)
+ def self.safe_receive(*)
+ receive(*)
rescue => e
Rails.logger.error "MailHandler: an unexpected error occurred when receiving email: #{e.message}"
return false
diff --git a/app/models/member.rb b/app/models/member.rb
index b0d5c35fc..1f597c96c 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -45,9 +45,9 @@ class Member < ApplicationRecord
end)
alias :base_reload :reload
- def reload(*args)
+ def reload(*)
@managed_roles = nil
- base_reload(*args)
+ base_reload(*)
end
def role
diff --git a/app/models/message.rb b/app/models/message.rb
index c7f78d2d9..9ac88c7d1 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -19,6 +19,8 @@
class Message < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
+
belongs_to :board
belongs_to :author, :class_name => 'User'
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
diff --git a/app/models/news.rb b/app/models/news.rb
index 40cd63db9..174e4c5ac 100644
--- a/app/models/news.rb
+++ b/app/models/news.rb
@@ -19,6 +19,8 @@
class News < ApplicationRecord
include Redmine::SafeAttributes
+ include Redmine::Reaction::Reactable
+
belongs_to :project
belongs_to :author, :class_name => 'User'
has_many :comments, lambda {order("created_on")}, :as => :commented, :dependent => :delete_all
diff --git a/app/models/principal.rb b/app/models/principal.rb
index 77f599c73..0a6c32ba2 100644
--- a/app/models/principal.rb
+++ b/app/models/principal.rb
@@ -35,6 +35,8 @@ class Principal < ApplicationRecord
:foreign_key => 'user_id'
has_many :projects, :through => :memberships
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
+ # Always returns nil for groups
+ has_one :email_address, lambda {where :is_default => true}, :autosave => true, :foreign_key => 'user_id'
validate :validate_status
@@ -128,6 +130,11 @@ class Principal < ApplicationRecord
to_s
end
+ # Returns nil by default, subclasses should implement this method
+ def initials(formatter = nil)
+ nil
+ end
+
def mail=(*args)
nil
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c438be16d..b3bf88c94 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -358,12 +358,12 @@ class Project < ApplicationRecord
end
end
- def self.find_by_param(*args)
- self.find(*args)
+ def self.find_by_param(*)
+ self.find(*)
end
alias :base_reload :reload
- def reload(*args)
+ def reload(*)
@principals = nil
@users = nil
@shared_versions = nil
@@ -382,7 +382,7 @@ class Project < ApplicationRecord
@override_members = nil
@assignable_users = nil
@last_activity_date = nil
- base_reload(*args)
+ base_reload(*)
end
def to_param
diff --git a/app/models/reaction.rb b/app/models/reaction.rb
new file mode 100644
index 000000000..184ed2d6e
--- /dev/null
+++ b/app/models/reaction.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+class Reaction < ApplicationRecord
+ belongs_to :reactable, polymorphic: true
+ belongs_to :user
+
+ validates :reactable_type, inclusion: { in: Redmine::Reaction::REACTABLE_TYPES }
+
+ scope :by, ->(user) { where(user: user) }
+ scope :for_reactable, ->(reactable) { where(reactable: reactable) }
+ scope :visible, ->(user) { where(user: User.visible(user)) }
+
+ # Represents reaction details for a reactable object
+ Detail = Struct.new(
+ # Users who reacted and are visible to the target user
+ :visible_users,
+ # Reaction of the target user
+ :user_reaction
+ ) do
+ def initialize(visible_users: [], user_reaction: nil)
+ super
+ end
+
+ def reaction_count = visible_users.size
+ end
+
+ def self.build_detail_map_for(reactables, user)
+ reactions = visible(user)
+ .for_reactable(reactables)
+ .preload(:user)
+ .select(:id, :reactable_id, :user_id)
+ .order(id: :desc)
+
+ reactions.each_with_object({}) do |reaction, m|
+ m[reaction.reactable_id] ||= Detail.new
+
+ m[reaction.reactable_id].then do |detail|
+ detail.visible_users << reaction.user
+ detail.user_reaction = reaction if reaction.user == user
+ end
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a1e81baf3..f4092fc96 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -69,12 +69,12 @@ class Repository < ApplicationRecord
end
end
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == "log_encoding"
attr_name = "commit_logs_encoding"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
# Removes leading and trailing whitespace
@@ -369,8 +369,8 @@ class Repository < ApplicationRecord
subclasses.collect {|klass| [klass.scm_name, klass.name]}
end
- def self.factory(klass_name, *args)
- repository_class(klass_name).new(*args) rescue nil
+ def self.factory(klass_name, *)
+ repository_class(klass_name).new(*) rescue nil
end
def self.repository_class(class_name)
diff --git a/app/models/repository/bazaar.rb b/app/models/repository/bazaar.rb
index d9cffe810..fc42c1235 100644
--- a/app/models/repository/bazaar.rb
+++ b/app/models/repository/bazaar.rb
@@ -22,12 +22,12 @@ require 'redmine/scm/adapters/bazaar_adapter'
class Repository::Bazaar < Repository
validates_presence_of :url, :log_encoding
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == "url"
attr_name = "path_to_repository"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
def self.scm_adapter_class
diff --git a/app/models/repository/cvs.rb b/app/models/repository/cvs.rb
index a5fce91bb..d055428a5 100644
--- a/app/models/repository/cvs.rb
+++ b/app/models/repository/cvs.rb
@@ -27,14 +27,14 @@ class Repository::Cvs < Repository
'root_url',
:if => lambda {|repository, user| repository.new_record?})
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == "root_url"
attr_name = "cvsroot"
elsif attr_name == "url"
attr_name = "cvs_module"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
def self.scm_adapter_class
diff --git a/app/models/repository/filesystem.rb b/app/models/repository/filesystem.rb
index 9347de0f3..c27044a9a 100644
--- a/app/models/repository/filesystem.rb
+++ b/app/models/repository/filesystem.rb
@@ -25,12 +25,12 @@ require 'redmine/scm/adapters/filesystem_adapter'
class Repository::Filesystem < Repository
validates_presence_of :url
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == "url"
attr_name = "root_directory"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
def self.scm_adapter_class
diff --git a/app/models/repository/git.rb b/app/models/repository/git.rb
index b6b3c8336..c94acb01d 100644
--- a/app/models/repository/git.rb
+++ b/app/models/repository/git.rb
@@ -25,10 +25,10 @@ class Repository::Git < Repository
safe_attributes 'report_last_commit'
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
attr_name = 'path_to_repository' if attr_name == 'url'
- super(attr_name, *args)
+ super(attr_name, *)
end
def self.scm_adapter_class
diff --git a/app/models/repository/mercurial.rb b/app/models/repository/mercurial.rb
index 8794cde75..1d1a3c4ff 100644
--- a/app/models/repository/mercurial.rb
+++ b/app/models/repository/mercurial.rb
@@ -30,12 +30,12 @@ class Repository::Mercurial < Repository
# number of changesets to fetch at once
FETCH_AT_ONCE = 100
- def self.human_attribute_name(attribute_key_name, *args)
+ def self.human_attribute_name(attribute_key_name, *)
attr_name = attribute_key_name.to_s
if attr_name == "url"
attr_name = "path_to_repository"
end
- super(attr_name, *args)
+ super(attr_name, *)
end
def self.scm_adapter_class
diff --git a/app/models/role.rb b/app/models/role.rb
index 3ca4f92a1..870bbe945 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -198,11 +198,14 @@ class Role < ApplicationRecord
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
# * a permission Symbol (eg. :edit_project)
- def allowed_to?(action)
+ # scope can be:
+ # * an array of permissions which will be used as filter (logical AND)
+
+ def allowed_to?(action, scope=nil)
if action.is_a? Hash
- allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
+ allowed_actions(scope).include? "#{action[:controller]}/#{action[:action]}"
else
- allowed_permissions.include? action
+ allowed_permissions(scope).include? action
end
end
@@ -298,13 +301,20 @@ class Role < ApplicationRecord
private
- def allowed_permissions
- @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
+ def allowed_permissions(scope = nil)
+ scope = scope.sort if scope.present? # to maintain stable cache keys
+ @allowed_permissions ||= {}
+ @allowed_permissions[scope] ||= begin
+ unscoped = permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
+ scope.present? ? unscoped & scope : unscoped
+ end
end
- def allowed_actions
- @actions_allowed ||=
- allowed_permissions.inject([]) do |actions, permission|
+ def allowed_actions(scope = nil)
+ scope = scope.sort if scope.present? # to maintain stable cache keys
+ @actions_allowed ||= {}
+ @actions_allowed[scope] ||=
+ allowed_permissions(scope).inject([]) do |actions, permission|
actions += Redmine::AccessControl.allowed_actions(permission)
end.flatten
end
diff --git a/app/models/time_entry_query.rb b/app/models/time_entry_query.rb
index 41997180b..82b895671 100644
--- a/app/models/time_entry_query.rb
+++ b/app/models/time_entry_query.rb
@@ -164,12 +164,19 @@ class TimeEntryQuery < Query
end
def base_scope
- TimeEntry.visible.
- joins(:project, :user).
- includes(:activity).
- references(:activity).
- left_join_issue.
- where(statement)
+ scope = TimeEntry.visible
+ .joins(:project, :user)
+ .includes(:activity)
+ .references(:activity)
+ .left_join_issue
+ .where(statement)
+
+ if Redmine::Database.mysql? && ActiveRecord::Base.connection.supports_optimizer_hints?
+ # Provides MySQL with a hint to use a better join order and avoid slow response times
+ scope.optimizer_hints('JOIN_ORDER(time_entries, projects, users)')
+ else
+ scope
+ end
end
def results_scope(options={})
diff --git a/app/models/user.rb b/app/models/user.rb
index 4ce63f809..496084ceb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,46 +28,55 @@ class User < Principal
USER_FORMATS = {
:firstname_lastname => {
:string => '#{firstname} #{lastname}',
+ :initials => '#{firstname.to_s.first}#{lastname.to_s.first}',
:order => %w(firstname lastname id),
:setting_order => 1
},
:firstname_lastinitial => {
:string => '#{firstname} #{lastname.to_s.chars.first}.',
+ :initials => '#{firstname.to_s.first}#{lastname.to_s.first}',
:order => %w(firstname lastname id),
:setting_order => 2
},
:firstinitial_lastname => {
:string => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\')} #{lastname}',
+ :initials => '#{firstname.to_s.gsub(/(([[:alpha:]])[[:alpha:]]*\.?)/, \'\2.\').first}#{lastname.to_s.first}',
:order => %w(firstname lastname id),
:setting_order => 2
},
:firstname => {
:string => '#{firstname}',
+ :initials => '#{firstname.to_s.first(2)}',
:order => %w(firstname id),
:setting_order => 3
},
:lastname_firstname => {
:string => '#{lastname} #{firstname}',
+ :initials => '#{lastname.to_s.first}#{firstname.to_s.first}',
:order => %w(lastname firstname id),
:setting_order => 4
},
:lastnamefirstname => {
:string => '#{lastname}#{firstname}',
+ :initials => '#{lastname.to_s.first}#{firstname.to_s.first}',
:order => %w(lastname firstname id),
:setting_order => 5
},
:lastname_comma_firstname => {
:string => '#{lastname}, #{firstname}',
+ :initials => '#{lastname.to_s.first}#{firstname.to_s.first}',
:order => %w(lastname firstname id),
:setting_order => 6
},
:lastname => {
:string => '#{lastname}',
+ :initials => '#{lastname.to_s.first(2)}',
:order => %w(lastname id),
:setting_order => 7
},
:username => {
:string => '#{login}',
+ :initials => '#{login.to_s.first(2)}',
:order => %w(login id),
:setting_order => 8
},
@@ -89,10 +98,10 @@ class User < Principal
:after_remove => Proc.new {|user, group| group.user_removed(user)}
has_many :changesets, :dependent => :nullify
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
- has_one :atom_token, lambda {where "action='feeds'"}, :class_name => 'Token'
- has_one :api_token, lambda {where "action='api'"}, :class_name => 'Token'
- has_one :email_address, lambda {where :is_default => true}, :autosave => true
+ has_one :atom_token, lambda {where "#{table.name}.action='feeds'"}, :class_name => 'Token'
+ has_one :api_token, lambda {where "#{table.name}.action='api'"}, :class_name => 'Token'
has_many :email_addresses, :dependent => :delete_all
+ has_many :reactions, dependent: :delete_all
belongs_to :auth_source
scope :logged, lambda {where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}")}
@@ -103,6 +112,7 @@ class User < Principal
attr_accessor :password, :password_confirmation, :generate_password
attr_accessor :last_before_login_on
attr_accessor :remote_ip
+ attr_writer :oauth_scope
LOGIN_LENGTH_LIMIT = 60
MAIL_LENGTH_LIMIT = 254
@@ -170,7 +180,7 @@ class User < Principal
end
alias :base_reload :reload
- def reload(*args)
+ def reload(*)
@name = nil
@roles = nil
@projects_by_role = nil
@@ -181,7 +191,7 @@ class User < Principal
@builtin_role = nil
@visible_project_ids = nil
@managed_roles = nil
- base_reload(*args)
+ base_reload(*)
end
def mail
@@ -275,6 +285,14 @@ class User < Principal
end
end
+ # Return user's initials based on name format
+ def initials(formatter = nil)
+ f = self.class.name_formatter(formatter)
+ format = f[:initials] || USER_FORMATS[:firstname_lastname][:initials]
+ initials = eval('"' + format + '"')
+ initials.upcase
+ end
+
def registered?
self.status == STATUS_REGISTERED
end
@@ -643,7 +661,7 @@ class User < Principal
def projects_by_role
return @projects_by_role if @projects_by_role
- result = Hash.new([])
+ result = Hash.new {|_h, _k| []}
project_ids_by_role.each do |role, ids|
result[role] = Project.where(:id => ids).to_a
end
@@ -676,7 +694,7 @@ class User < Principal
hash[role_id] << project_id
end
- result = Hash.new([])
+ result = Hash.new {|_h, _k| []}
if hash.present?
roles = Role.where(:id => hash.keys).to_a
hash.each do |role_id, proj_ids|
@@ -715,6 +733,20 @@ class User < Principal
end
end
+ def admin?
+ if authorized_by_oauth?
+ # when signed in via oauth, the user only acts as admin when the admin scope is set
+ super and @oauth_scope.include?(:admin)
+ else
+ super
+ end
+ end
+
+ # true if the user has signed in via oauth
+ def authorized_by_oauth?
+ !@oauth_scope.nil?
+ end
+
# Return true if the user is allowed to do the specified action on a specific context
# Action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
@@ -735,7 +767,7 @@ class User < Principal
roles.any? do |role|
(context.is_public? || role.member?) &&
- role.allowed_to?(action) &&
+ role.allowed_to?(action, @oauth_scope) &&
(block ? yield(role, self) : true)
end
elsif context && context.is_a?(Array)
@@ -754,7 +786,7 @@ class User < Principal
# authorize if user has at least one role that has this permission
roles = self.roles.to_a | [builtin_role]
roles.any? do |role|
- role.allowed_to?(action) &&
+ role.allowed_to?(action, @oauth_scope) &&
(block ? yield(role, self) : true)
end
else
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 8b19d9a5a..e1842b131 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -73,7 +73,7 @@ class UserPreference < ApplicationRecord
if has_attribute? attr_name
super
else
- others ? others[attr_name] : nil
+ others&.[](attr_name)
end
end
diff --git a/app/models/version.rb b/app/models/version.rb
index 51c7c0417..3ca4f2bff 100644
--- a/app/models/version.rb
+++ b/app/models/version.rb
@@ -106,7 +106,7 @@ module FixedIssuesExtension
done = self.open(open).sum do |c|
estimated = c.total_estimated_hours.to_f
estimated = estimated_average unless estimated > 0.0
- ratio = c.closed? ? 100 : (c.done_ratio || 0)
+ ratio = open ? (c.done_ratio || 0) : 100
estimated * ratio
end
progress = done / (estimated_average * issues_count)
@@ -211,8 +211,8 @@ class Version < ApplicationRecord
end
# Version files have same visibility as project files
- def attachments_visible?(*args)
- project.present? && project.attachments_visible?(*args)
+ def attachments_visible?(*)
+ project.present? && project.attachments_visible?(*)
end
def attachments_deletable?(usr=User.current)
@@ -220,10 +220,10 @@ class Version < ApplicationRecord
end
alias :base_reload :reload
- def reload(*args)
+ def reload(*)
@default_project_version = nil
@visible_fixed_issues = nil
- base_reload(*args)
+ base_reload(*)
end
def start_date
diff --git a/app/views/activities/_activities.html.erb b/app/views/activities/_activities.html.erb
index 21ec1fb28..f2d8e22bd 100644
--- a/app/views/activities/_activities.html.erb
+++ b/app/views/activities/_activities.html.erb
@@ -4,7 +4,7 @@
<dl>
<% sort_activity_events(events_by_day[day]).each do |e, in_group| -%>
<dt class="<%= e.event_type %> icon icon-<%= e.event_type %> <%= "grouped" if in_group %> <%= User.current.logged? && e.respond_to?(:event_author) && User.current == e.event_author ? 'me' : nil %>">
- <%= activity_event_type_icon e.event_type, plugin: Redmine::Activity.plugin_name(e.activity_provider_options.keys[0]) %>
+ <%= activity_event_type_icon e.event_type, plugin: Redmine::Activity.plugin_name(e.class) %>
<%= avatar(e.event_author) if e.respond_to?(:event_author) %>
<span class="time"><%= format_time(e.event_datetime, false) %></span>
<%= content_tag('span', e.project, :class => 'project') if @project.nil? || @project != e.project %>
diff --git a/app/views/attachments/_form.html.erb b/app/views/attachments/_form.html.erb
index 7bda59c95..e5b10fb55 100644
--- a/app/views/attachments/_form.html.erb
+++ b/app/views/attachments/_form.html.erb
@@ -7,17 +7,22 @@
<% css_class = (defined?(filedrop) && filedrop == false ? '' : (attachment_format_custom_field ? 'custom-field-filedrop' : 'filedrop')) %>
<span class="attachments_form">
+ <span class="attachments_icons hidden">
+ <%= sprite_icon('del', icon_only: true, css_class: 'svg-del') %>
+ <%= sprite_icon('attachment', icon_only: true, size: 16, css_class: 'svg-attachment') %>
+ </span>
<span class="attachments_fields">
<% if saved_attachments.present? %>
<% saved_attachments.each_with_index do |attachment, i| %>
<span id="attachments_p<%= i %>">
+ <%= sprite_icon('attachment', icon_only: true, size: 16, css_class: 'svg-attachment') %>
<%= text_field_tag("#{attachment_param}[p#{i}][filename]", attachment.filename, :class => 'filename') %>
<% if attachment.container_id.present? %>
- <%= link_to l(:label_delete), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %>
+ <%= link_to sprite_icon('del', l(:button_delete), icon_only: true), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %>
<%= hidden_field_tag "#{attachment_param}[p#{i}][id]", attachment.id %>
<% else %>
<%= text_field_tag("#{attachment_param}[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') if description %>
- <%= link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'icon-only icon-del remove-upload') %>
+ <%= link_to(sprite_icon('del', l(:button_delete), icon_only: true), attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'icon-only icon-del remove-upload') %>
<%= hidden_field_tag "#{attachment_param}[p#{i}][token]", attachment.token %>
<% end %>
</span>
diff --git a/app/views/attachments/other.html.erb b/app/views/attachments/other.html.erb
index f0f732f6f..613c470df 100644
--- a/app/views/attachments/other.html.erb
+++ b/app/views/attachments/other.html.erb
@@ -14,6 +14,7 @@
:download_link => link_to_attachment(
@attachment,
:text => l(:label_no_preview_download),
+ :icon => 'download',
:download => true,
:class => 'icon icon-download'
)
diff --git a/app/views/calendars/show.html.erb b/app/views/calendars/show.html.erb
index c1d412a0b..d5cb6a6a1 100644
--- a/app/views/calendars/show.html.erb
+++ b/app/views/calendars/show.html.erb
@@ -10,7 +10,7 @@
<div id="query_form_content">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>">
- <%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right") %>
+ <%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right", rtl: !@query.new_record?) %>
<%= l(:label_filter_plural) %>
</legend>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
diff --git a/app/views/context_menus/issues.html.erb b/app/views/context_menus/issues.html.erb
index 3406e78d6..ee7eaa18c 100644
--- a/app/views/context_menus/issues.html.erb
+++ b/app/views/context_menus/issues.html.erb
@@ -12,7 +12,7 @@
<% if @allowed_statuses.present? %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_status) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% @allowed_statuses.each do |s| -%>
<li>
@@ -34,7 +34,7 @@
<% if @trackers.present? %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_tracker) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% @trackers.each do |t| -%>
<li><%= context_menu_link t.name, _bulk_update_issues_path(@issue, :ids => @issue_ids, :issue => {'tracker_id' => t}, :back_url => @back), :method => :patch,
@@ -47,7 +47,7 @@
<% if @safe_attributes.include?('priority_id') && @priorities.present? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_priority) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% @priorities.each do |p| -%>
<li><%= context_menu_link p.name, _bulk_update_issues_path(@issue, :ids => @issue_ids, :issue => {'priority_id' => p}, :back_url => @back), :method => :patch,
@@ -60,7 +60,7 @@
<% if @safe_attributes.include?('fixed_version_id') && @versions.present? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% @versions.sort.each do |v| -%>
<li><%= context_menu_link format_version_name(v), _bulk_update_issues_path(@issue, :ids => @issue_ids, :issue => {'fixed_version_id' => v}, :back_url => @back), :method => :patch,
@@ -75,7 +75,7 @@
<% if @safe_attributes.include?('assigned_to_id') && @assignables.present? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% if @assignables.include?(User.current) %>
<li><%= context_menu_link "<< #{l(:label_me)} >>", _bulk_update_issues_path(@issue, :ids => @issue_ids, :issue => {'assigned_to_id' => User.current}, :back_url => @back), :method => :patch,
@@ -94,7 +94,7 @@
<% if @safe_attributes.include?('category_id') && @project && @project.issue_categories.any? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_category) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% @project.issue_categories.each do |u| -%>
<li><%= context_menu_link u.name, _bulk_update_issues_path(@issue, :ids => @issue_ids, :issue => {'category_id' => u}, :back_url => @back), :method => :patch,
@@ -109,7 +109,7 @@
<% if @safe_attributes.include?('done_ratio') && Issue.use_field_for_done_ratio? %>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% (0..10).map{|x|x*10}.each do |p| -%>
<li><%= context_menu_link "#{p}%", _bulk_update_issues_path(@issue, :ids => @issue_ids, :issue => {'done_ratio' => p}, :back_url => @back), :method => :patch,
@@ -122,7 +122,7 @@
<% @options_by_custom_field.each do |field, options| %>
<li class="folder <%= field.css_classes %>">
<a href="#" class="submenu"><%= field.name %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% options.each do |text, value| %>
<li><%= bulk_update_custom_field_context_menu_link(field, text, value || text) %></li>
@@ -137,7 +137,7 @@
<% if @can[:add_watchers] %>
<li class="folder">
<a href="#" class="submenu"><%= l(:label_issue_watchers) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<li><%= context_menu_link sprite_icon('add', l(:button_add)),
new_watchers_path(:object_type => 'issue', :object_id => @issue_ids),
diff --git a/app/views/context_menus/time_entries.html.erb b/app/views/context_menus/time_entries.html.erb
index 0073b87cf..d43021f59 100644
--- a/app/views/context_menus/time_entries.html.erb
+++ b/app/views/context_menus/time_entries.html.erb
@@ -12,7 +12,7 @@
<% if @activities.present? -%>
<li class="folder">
<a href="#" class="submenu"><%= l(:field_activity) %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% @activities.each do |u| -%>
<li><%= context_menu_link u.name, {:controller => 'timelog', :action => 'bulk_update', :ids => @time_entries.collect(&:id), :time_entry => {'activity_id' => u}, :back_url => @back}, :method => :post,
@@ -25,7 +25,7 @@
<% @options_by_custom_field.each do |field, options| %>
<li class="folder <%= field.css_classes %>">
<a href="#" class="submenu"><%= field.name %></a>
- <span class="icon-only"><%= sprite_icon('angle-right') %></span>
+ <span class="icon-only"><%= sprite_icon('angle-right', rtl: true) %></span>
<ul>
<% options.each do |text, value| %>
<li><%= bulk_update_time_entry_custom_field_context_menu_link(field, text, value || text) %></li>
diff --git a/app/views/custom_fields/formats/_progressbar.html.erb b/app/views/custom_fields/formats/_progressbar.html.erb
new file mode 100644
index 000000000..ceae14ac8
--- /dev/null
+++ b/app/views/custom_fields/formats/_progressbar.html.erb
@@ -0,0 +1,6 @@
+<p>
+ <%= f.select :ratio_interval,
+ [5, 10].collect {|i| ["#{i} %", i]},
+ selected: f.object.new_record? ? Redmine::FieldFormat::ProgressbarFormat.default_ratio_interval : f.object.ratio_interval,
+ required: true %>
+</p>
diff --git a/app/views/custom_fields/index.api.rsb b/app/views/custom_fields/index.api.rsb
index 9f46f89f2..d4b19d62b 100644
--- a/app/views/custom_fields/index.api.rsb
+++ b/app/views/custom_fields/index.api.rsb
@@ -15,6 +15,7 @@ api.array :custom_fields do
api.multiple field.multiple?
api.default_value field.default_value
api.visible field.visible?
+ api.editable field.editable?
values = field.possible_values_options
if values.present?
diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb
new file mode 100644
index 000000000..e4f778f63
--- /dev/null
+++ b/app/views/doorkeeper/applications/_form.html.erb
@@ -0,0 +1,39 @@
+<%= error_messages_for 'application' %>
+<div class="box tabular">
+ <p><%= f.text_field :name, :required => true %></p>
+
+ <p>
+ <%= f.text_area :redirect_uri, :required => true, :size => 60, :label => :'activerecord.attributes.doorkeeper/application.redirect_uri' %>
+ <em class="info">
+ <%= t('doorkeeper.applications.help.redirect_uri') %>
+ </em>
+ </p>
+</div>
+
+<h3><%= l(:'activerecord.attributes.doorkeeper/application.scopes') %></h3>
+<p><em class="info"><%= l :text_oauth_info_scopes %></em></p>
+<div class="box tabular" id="scopes">
+<fieldset><legend><%= l :label_oauth_admin_access %></legend>
+ <label class="floating" style="width: auto;">
+ <%= check_box_tag 'doorkeeper_application[scopes][]', 'admin', @application.scopes.include?('admin'),
+ :id => "doorkeeper_application_scopes_admin"
+ %>
+ <%= l :text_oauth_admin_permission %>
+ </label>
+</fieldset>
+<% perms_by_module = Redmine::AccessControl.permissions.group_by {|p| p.project_module.to_s} %>
+<% perms_by_module.keys.sort.each do |mod| %>
+ <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
+ <% perms_by_module[mod].each do |permission| %>
+ <label class="floating">
+ <%= check_box_tag 'doorkeeper_application[scopes][]', permission.name.to_s, (permission.public? || @application.scopes.include?( permission.name.to_s)),
+ :id => "doorkeeper_application_scopes_#{permission.name}",
+ :disabled => permission.public? %>
+ <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
+ </label>
+ <% end %>
+ </fieldset>
+<% end %>
+<br /><%= check_all_links 'scopes' %>
+<%= hidden_field_tag 'doorkeeper_application[scopes][]', '' %>
+</div>
diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb
new file mode 100644
index 000000000..aebc1a841
--- /dev/null
+++ b/app/views/doorkeeper/applications/edit.html.erb
@@ -0,0 +1,6 @@
+<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
+
+<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
+ <%= render :partial => 'form', :locals => {:f => f} %>
+ <%= submit_tag l(:button_save) %>
+<% end %>
diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb
new file mode 100644
index 000000000..0ba31c0e8
--- /dev/null
+++ b/app/views/doorkeeper/applications/index.html.erb
@@ -0,0 +1,33 @@
+<div class="contextual">
+<%= link_to sprite_icon('add', t('.new')), new_oauth_application_path, :class => 'icon icon-add' %>
+</div>
+
+<%= title l 'label_oauth_application_plural' %>
+
+<% if @applications.any? %>
+<div class="autoscroll">
+<table class="list">
+ <thead><tr>
+ <th><%= t('.name') %></th>
+ <th><%= t('.callback_url') %></th>
+ <th><%= t('.scopes') %></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+ <% @applications.each do |application| %>
+ <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
+ <td class="name"><span><%= link_to application.name, oauth_application_path(application) %></span></td>
+ <td class="description"><%= truncate application.redirect_uri.split.join(', '), length: 50 %></td>
+ <td class="description"><%= safe_join application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></td>
+ <td class="buttons">
+ <%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(application), class: 'icon icon-edit' %>
+ <%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+</div>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb
new file mode 100644
index 000000000..e2a39ac93
--- /dev/null
+++ b/app/views/doorkeeper/applications/new.html.erb
@@ -0,0 +1,6 @@
+<%= title [l('label_oauth_application_plural'), oauth_applications_path], t('.title') %>
+
+<%= labelled_form_for @application, url: doorkeeper_submit_path(@application) do |f| %>
+<%= render :partial => 'form', :locals => { :f => f } %>
+<%= submit_tag l(:button_create) %>
+<% end %>
diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb
new file mode 100644
index 000000000..c98e7d29c
--- /dev/null
+++ b/app/views/doorkeeper/applications/show.html.erb
@@ -0,0 +1,54 @@
+<div class="contextual">
+<%= link_to sprite_icon('edit', t('doorkeeper.applications.buttons.edit')), edit_oauth_application_path(@application), :accesskey => accesskey(:edit), class: 'icon icon-edit' %>
+<%= link_to sprite_icon('del', t('doorkeeper.applications.buttons.destroy')), oauth_application_path(@application), :data => {:confirm => t('doorkeeper.applications.confirmations.destroy')}, :method => :delete, :class => 'icon icon-del' %>
+</div>
+
+<%= title [l('label_oauth_application_plural'), oauth_applications_path], @application.name %>
+
+<div class="box">
+ <h3 class="icon icon-passwd"><%= sprite_icon('key', l(:label_information_plural)) %></h3>
+ <p>
+ <span class="label"><%= t('.application_id') %>:</span>
+ <code><%= h @application.uid %></code>
+ </p>
+ <p>
+ <span class="label"><%= t('.secret') %>:</span>
+ <code>
+ <% secret = flash[:application_secret].presence || @application.plaintext_secret %>
+ <% flash.delete :application_secret %>
+ <% if secret.blank? && Doorkeeper.config.application_secret_hashed? %>
+ <%= t('.secret_hashed') %>
+ <% else %>
+ <%= secret %>
+ <% end %>
+ </code>
+ <% if secret.present? && Doorkeeper.config.application_secret_hashed? %>
+ <strong><%= t "text_oauth_copy_secret_now" %></strong>
+ <% end %>
+ </p>
+ <p>
+ <span class="label"><%= t('.scopes') %>:</span>
+ <code><%= safe_join @application.scopes.map{|scope| h l_or_humanize(scope, prefix: 'permission_')}, ", " %></code>
+ </p>
+</div>
+
+<h3><%= t('.callback_urls') %></h3>
+
+<div class="autoscroll">
+<table class="list">
+ <thead><tr>
+ <th><%= t('.callback_url') %></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+ <% @application.redirect_uri.split.each do |uri| %>
+ <tr class="<%= cycle("odd", "even") %>">
+ <td class="name"><span><%= uri %></span></td>
+ <td class="buttons">
+ <%= link_to sprite_icon('shield-check', t('doorkeeper.applications.buttons.authorize')), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code', scope: @application.scopes), class: 'icon icon-authorize', target: '_blank' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+</div>
diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb
new file mode 100644
index 000000000..59cedf8f3
--- /dev/null
+++ b/app/views/doorkeeper/authorizations/error.html.erb
@@ -0,0 +1,6 @@
+<h2><%= t('doorkeeper.authorizations.error.title') %></h2>
+
+<p id="errorExplanation"><%= @pre_auth.error_response.body[:error_description] %></p>
+<p><a href="javascript:history.back()"><%= l(:button_back) %></a></p>
+
+<% html_title t('doorkeeper.authorizations.error.title') %>
diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb
new file mode 100644
index 000000000..898f2e645
--- /dev/null
+++ b/app/views/doorkeeper/authorizations/new.html.erb
@@ -0,0 +1,48 @@
+<%= title t('.title') %>
+
+<div class="warning">
+<p><strong><%=h @pre_auth.client.name %></strong></p>
+
+<p><%= raw t('.prompt', client_name: content_tag(:strong, class: "text-info") { @pre_auth.client.name }) %></p>
+
+<div class="oauth-permissions">
+ <p><%= t('.able_to') %>:</p>
+ <ul>
+ <li><%= l :text_oauth_implicit_permissions %></li>
+ <% @pre_auth.scopes.each do |scope| %>
+ <% if scope == 'admin' %>
+ <li><%= l :label_oauth_permission_admin %></li>
+ <% else %>
+ <li><%= l_or_humanize(scope, prefix: 'permission_') %></li>
+ <% end %>
+ <% end %>
+ </ul>
+</div>
+
+<% if @pre_auth.scopes.include?('admin') %>
+ <p><%= l :text_oauth_admin_permission_info %></p>
+<% end %>
+</div>
+
+<p>
+ <%= form_tag oauth_authorization_path, method: :post do %>
+ <%= hidden_field_tag :client_id, @pre_auth.client.uid %>
+ <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
+ <%= hidden_field_tag :state, @pre_auth.state %>
+ <%= hidden_field_tag :response_type, @pre_auth.response_type %>
+ <%= hidden_field_tag :scope, @pre_auth.scope %>
+ <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
+ <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
+ <%= submit_tag t('doorkeeper.authorizations.buttons.authorize') %>
+ <% end %>
+ <%= form_tag oauth_authorization_path, method: :delete do %>
+ <%= hidden_field_tag :client_id, @pre_auth.client.uid %>
+ <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %>
+ <%= hidden_field_tag :state, @pre_auth.state %>
+ <%= hidden_field_tag :response_type, @pre_auth.response_type %>
+ <%= hidden_field_tag :scope, @pre_auth.scope %>
+ <%= hidden_field_tag :code_challenge, @pre_auth.code_challenge %>
+ <%= hidden_field_tag :code_challenge_method, @pre_auth.code_challenge_method %>
+ <%= submit_tag t('doorkeeper.authorizations.buttons.deny') %>
+ <% end %>
+</p>
diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb
new file mode 100644
index 000000000..25ee88a87
--- /dev/null
+++ b/app/views/doorkeeper/authorizations/show.html.erb
@@ -0,0 +1,8 @@
+<%= title [l('label_oauth_authorized_application_plural'), oauth_authorized_applications_path] %>
+
+<fieldset class="tabular"><legend><%= l(:label_information_plural) %></legend>
+ <p>
+ <label><%= t('.title') %>:</label>
+ <code><%= params[:code] %></code>
+ </p>
+</fieldset>
diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb
new file mode 100644
index 000000000..0a1fc8a00
--- /dev/null
+++ b/app/views/doorkeeper/authorized_applications/index.html.erb
@@ -0,0 +1,31 @@
+<%= title [t(:label_my_account), my_account_path], l('label_oauth_authorized_application_plural') %>
+
+<% if @applications.any? %>
+<div class="autoscroll">
+<table class="list">
+ <thead><tr>
+ <th><%= t('doorkeeper.authorized_applications.index.application') %></th>
+ <th><%= t('doorkeeper.authorized_applications.index.created_at') %></th>
+ <th></th>
+ </tr></thead>
+ <tbody>
+ <% @applications.each do |application| %>
+ <tr id="application_<%= application.id %>" class="<%= cycle("odd", "even") %>">
+ <td class="name"><span><%= application.name %></span></td>
+ <td ><%= format_date application.created_at %></td>
+ <td class="buttons">
+ <%= link_to sprite_icon('del', t('doorkeeper.authorized_applications.buttons.revoke')), oauth_authorized_application_path(application), :data => {:confirm => t('doorkeeper.authorized_applications.confirmations.revoke')}, :method => :delete, :class => 'icon icon-del' %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+</div>
+<% else %>
+ <p class="nodata"><%= l(:label_no_data) %></p>
+<% end %>
+
+<% content_for :sidebar do %>
+<% @user = User.current %>
+<%= render :partial => 'my/sidebar' %>
+<% end %>
diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb
index c43f10fdd..45428b03d 100644
--- a/app/views/gantts/show.html.erb
+++ b/app/views/gantts/show.html.erb
@@ -16,7 +16,7 @@
<div id="query_form_content">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>">
- <%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right") %>
+ <%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right", rtl: !@query.new_record?) %>
<%= l(:label_filter_plural) %>
</legend>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
@@ -26,7 +26,7 @@
<fieldset id="options" class="collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
- <%= sprite_icon("angle-right") %>
+ <%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_options) %>
</legend>
<div style="display: none;">
@@ -308,7 +308,7 @@
style += "width:#{width}px;"
style += "height:#{height}px;"
style += "font-size:0.7em;"
- clss = "gantt_hdr"
+ clss = +"gantt_hdr"
clss << " nwday" if @gantt.non_working_week_days.include?(wday)
%>
<%= content_tag(:div, :style => style, :class => clss) do %>
@@ -339,7 +339,7 @@
style += "width: #{width}px;"
style += "height: #{height}px;"
style += "font-size:0.7em;"
- clss = "gantt_hdr"
+ clss = +"gantt_hdr"
clss << " nwday" if @gantt.non_working_week_days.include?(g_date.cwday)
%>
<%= content_tag(:div, :style => style, :class => clss) do %>
diff --git a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb
index 486b96424..a650b2751 100644
--- a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb
+++ b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_common_mark.html.erb
@@ -81,6 +81,14 @@
<th></th><td>HTML is &lt;del&gt;not&lt;/del&gt; &lt;u&gt;allowed&lt;/u&gt;.</td><td>HTML is <del>not</del> <u>allowed</u>.</td>
</tr>
+<tr><th colspan="3">Alerts <span class="more_info">(<a href="<%= help_wiki_syntax_path(:detailed, anchor: "16") %>" target="_blank">more</a>)</span></th></tr>
+<tr><th></th><td>> [!NOTE]<br>> You can use alerts like [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION].</td><td>
+<div class="markdown-alert markdown-alert-note">
+<p class="markdown-alert-title">Note</p>
+<p>You can use alerts like [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION].</p>
+</div>
+</td></tr>
+
</table>
<p><a href="<%= help_wiki_syntax_path(:detailed) %>" onclick="window.open('<%= help_wiki_syntax_path(:detailed) %>', '', ''); return false;">More Information</a></p>
diff --git a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb
index a47a570f1..a74094460 100644
--- a/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb
+++ b/app/views/help/wiki_syntax/common_mark/en/wiki_syntax_detailed_common_mark.html.erb
@@ -27,6 +27,7 @@
<li><a href='#12'>Macros</a></li>
<li><a href='#13'>Code highlighting</a></li>
<li><a href='#15'>Raw HTML</a></li>
+ <li><a href='#16'>Alerts</a></li>
</ul>
<h2><a name="2" class="wiki-page"></a>Links</h2>
@@ -359,8 +360,8 @@ It can be expanded by clicking a link.
<p>The <strong>style</strong> attribute can be used in raw HTML to apply custom formatting. The following CSS properties are allowed:</p>
<pre><code>
color background-color
- width
- height
+ width min-width max-width
+ height min-height max-height
padding padding-left padding-right padding-top padding-bottom
margin margin-left margin-right margin-top margin-bottom
border border-left border-right border-top border-bottom border-radius border-style border-collapse border-spacing
@@ -369,5 +370,52 @@ It can be expanded by clicking a link.
float
</code></pre>
+ <h2><a name="16" class="wiki-page"></a>Alerts</h2>
+
+ <p>
+ <dl>
+ <dt><code>NOTE</code></dt>
+ <dd>
+ <pre><code>> [!NOTE]<br>> Wiki page edits are preserved as history, allowing you to restore previous versions if needed.</code></pre>
+ <div class="markdown-alert markdown-alert-note">
+ <p class="markdown-alert-title">Note</p>
+ <p>Wiki page edits are preserved as history, allowing you to restore previous versions if needed.</p>
+ </div>
+ </dd>
+ <dt><code>TIP</code></dt>
+ <dd>
+ <pre><code>> [!TIP]<br>> To quickly review the update history of an issue, use the "History" tab for convenient access.</code></pre>
+ <div class="markdown-alert markdown-alert-tip">
+ <p class="markdown-alert-title">Tip</p>
+ <p>To quickly review the update history of an issue, use the "History" tab for convenient access.</p>
+ </div>
+ </dd>
+ <dt><code>WARNING</code></dt>
+ <dd>
+ <pre><code>> [!WARNING]<br>> Deleting an issue is a permanent action. Be certain it is truly necessary before proceeding.</code></pre>
+ <div class="markdown-alert markdown-alert-warning">
+ <p class="markdown-alert-title">Warning</p>
+ <p>Deleting an issue is a permanent action. Be certain it is truly necessary before proceeding.</p>
+ </div>
+ </dd>
+ <dt><code>IMPORTANT</code></dt>
+ <dd>
+ <pre><code>> [!IMPORTANT]<br>> Changing role permissions can affect user access across all projects, so be mindful of potential impacts.</code></pre>
+ <div class="markdown-alert markdown-alert-important">
+ <p class="markdown-alert-title">Important</p>
+ <p>Changing role permissions can affect user access across all projects, so be mindful of potential impacts.</p>
+ </div>
+ </dd>
+ <dt><code>CAUTION</code></dt>
+ <dd>
+ <pre><code>> [!CAUTION]<br>> When installing plugins, make sure to verify compatibility. Version differences can cause unexpected behavior.</code></pre>
+ <div class="markdown-alert markdown-alert-caution">
+ <p class="markdown-alert-title">Caution</p>
+ <p>When installing plugins, make sure to verify compatibility. Version differences can cause unexpected behavior.</p>
+ </div>
+ </dd>
+ </dl>
+ </p>
+
</body>
</html>
diff --git a/app/views/imports/_issues_mapping.html.erb b/app/views/imports/_issues_mapping.html.erb
index 86e2dd89a..539bad9fe 100644
--- a/app/views/imports/_issues_mapping.html.erb
+++ b/app/views/imports/_issues_mapping.html.erb
@@ -7,7 +7,7 @@
<fieldset class="box tabular collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
- <%= sprite_icon("angle-right") %>
+ <%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_relations_mapping) %>
</legend>
<div id="relations-mapping" style="display: none;">
diff --git a/app/views/imports/_issues_relations_mapping.html.erb b/app/views/imports/_issues_relations_mapping.html.erb
index fa0b0950f..9a88c6ebc 100644
--- a/app/views/imports/_issues_relations_mapping.html.erb
+++ b/app/views/imports/_issues_relations_mapping.html.erb
@@ -5,27 +5,27 @@
<%= mapping_select_tag @import, 'unique_id' %>
</p>
<p>
- <label for="import_settings_mapping_parent_issue_id"><%= l(:field_parent_issue) %></label>
+ <label for="import_mapping_parent_issue_id"><%= l(:field_parent_issue) %></label>
<%= mapping_select_tag @import, 'parent_issue_id' %>
</p>
<p>
- <label for="import_settings_mapping_relation_duplicates"><%= l(:label_duplicates) %></label>
+ <label for="import_mapping_relation_duplicates"><%= l(:label_duplicates) %></label>
<%= mapping_select_tag @import, 'relation_duplicates' %>
</p>
<p>
- <label for="import_settings_mapping_relation_duplicated"><%= l(:label_duplicated_by) %></label>
+ <label for="import_mapping_relation_duplicated"><%= l(:label_duplicated_by) %></label>
<%= mapping_select_tag @import, 'relation_duplicated' %>
</p>
<p>
- <label for="import_settings_mapping_relation_blocks"><%= l(:label_blocks) %></label>
+ <label for="import_mapping_relation_blocks"><%= l(:label_blocks) %></label>
<%= mapping_select_tag @import, 'relation_blocks' %>
</p>
<p>
- <label for="import_settings_mapping_relation_blocked"><%= l(:label_blocked_by) %></label>
+ <label for="import_mapping_relation_blocked"><%= l(:label_blocked_by) %></label>
<%= mapping_select_tag @import, 'relation_blocked' %>
</p>
</div>
@@ -33,27 +33,27 @@
<div class="splitcontentright">
<p></p>
<p>
- <label for="import_settings_mapping_relation_relates"><%= l(:label_relates_to) %></label>
+ <label for="import_mapping_relation_relates"><%= l(:label_relates_to) %></label>
<%= mapping_select_tag @import, 'relation_relates' %>
</p>
<p>
- <label for="import_settings_mapping_relation_precedes"><%= l(:label_precedes) %></label>
+ <label for="import_mapping_relation_precedes"><%= l(:label_precedes) %></label>
<%= mapping_select_tag @import, 'relation_precedes' %>
</p>
<p>
- <label for="import_settings_mapping_relation_follows"><%= l(:label_follows) %></label>
+ <label for="import_mapping_relation_follows"><%= l(:label_follows) %></label>
<%= mapping_select_tag @import, 'relation_follows' %>
</p>
<p>
- <label for="import_settings_mapping_relation_copied_to"><%= l(:label_copied_to) %></label>
+ <label for="import_mapping_relation_copied_to"><%= l(:label_copied_to) %></label>
<%= mapping_select_tag @import, 'relation_copied_to' %>
</p>
<p>
- <label for="import_settings_mapping_relation_copied_from"><%= l(:label_copied_from) %></label>
+ <label for="import_mapping_relation_copied_from"><%= l(:label_copied_from) %></label>
<%= mapping_select_tag @import, 'relation_copied_from' %>
</p>
</div>
diff --git a/app/views/imports/show.html.erb b/app/views/imports/show.html.erb
index cb698f018..b505d9912 100644
--- a/app/views/imports/show.html.erb
+++ b/app/views/imports/show.html.erb
@@ -12,8 +12,8 @@
<table id="unsaved-items" class="list">
<thead>
<tr>
- <th>Position</th>
- <th>Message</th>
+ <th><%= l(:label_position) %></th>
+ <th><%= l(:label_message) %></th>
</tr>
</thead>
<tbody>
diff --git a/app/views/issues/_list.html.erb b/app/views/issues/_list.html.erb
index df562c6f2..e8b151ef6 100644
--- a/app/views/issues/_list.html.erb
+++ b/app/views/issues/_list.html.erb
@@ -15,7 +15,7 @@
<% query.inline_columns.each do |column| %>
<%= column_header(query, column, query_options) %>
<% end %>
- <th class="buttons"></th>
+ <th class="buttons hide-when-print"></th>
</tr>
</thead>
<tbody>
@@ -36,7 +36,7 @@
<% query.inline_columns.each do |column| %>
<%= content_tag('td', column_content(column, issue), :class => column.css_classes) %>
<% end %>
- <td class="buttons"><%= link_to_context_menu %></td>
+ <td class="buttons hide-when-print"><%= link_to_context_menu %></td>
</tr>
<% query.block_columns.each do |column|
if (text = column_content(column, issue)) && text.present? -%>
diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb
index af2510827..70f0b740a 100644
--- a/app/views/issues/index.html.erb
+++ b/app/views/issues/index.html.erb
@@ -35,9 +35,9 @@
<% end %>
<% other_formats_links do |f| %>
- <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
<%= f.link_to_with_query_parameters 'PDF' %>
+ <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<% end %>
<div id="csv-export-options" style="display:none;">
diff --git a/app/views/issues/show.html.erb b/app/views/issues/show.html.erb
index 8f732032a..6892269e4 100644
--- a/app/views/issues/show.html.erb
+++ b/app/views/issues/show.html.erb
@@ -1,7 +1,3 @@
-<% content_for :header_tags do %>
- <%= javascripts_for_quote_reply_include_tag %>
-<% end %>
-
<%= render :partial => 'action_menu' %>
<h2 class="inline-block"><%= issue_heading(@issue) %></h2><%= issue_status_type_badge(@issue.status) %>
@@ -32,13 +28,23 @@
</div>
<% end %>
- <div class="gravatar-with-child">
+ <div class="avatar-with-child">
<%= author_avatar(@issue.author, :size => "50") %>
- <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "gravatar-child") if @issue.assigned_to %>
+ <%= assignee_avatar(@issue.assigned_to, :size => "22", :class => "avatar-child") if @issue.assigned_to %>
</div>
-<div class="subject">
-<%= render_issue_subject_with_tree(@issue) %>
+<div data-controller="sticky-issue-header">
+ <div class="subject" data-sticky-issue-header-target="original">
+ <%= render_issue_subject_with_tree(@issue) %>
+ </div>
+ <div id="sticky-issue-header" data-sticky-issue-header-target="stickyHeader" class="issue">
+ <span class="issue-heading"><%= issue_heading(@issue) %>:</span>
+ <span class="subject"><%= @issue.subject %></span>
+ </div>
+</div>
+
+<div class="reaction">
+ <%= reaction_button @issue %>
</div>
<p class="author">
<%= authoring @issue.created_on, @issue.author %>.
@@ -86,13 +92,13 @@ end %>
<% if @issue.description? %>
<hr />
-<div class="description">
+<div class="description" data-controller="quote-reply">
<div class="contextual">
- <%= quote_reply(quoted_issue_path(@issue), '#issue_description_wiki') if @issue.notes_addable? %>
+ <%= quote_reply_button(url: quoted_issue_path(@issue)) if @issue.notes_addable? %>
</div>
<p><strong><%=l(:field_description)%></strong></p>
- <div id="issue_description_wiki" class="wiki">
+ <div id="issue_description_wiki" class="wiki" data-quote-reply-target="content">
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
</div>
</div>
@@ -125,15 +131,15 @@ end %>
<%= render partial: 'action_menu_edit' if User.current.wants_comments_in_reverse_order? %>
-<div id="history">
+<div id="history" class="journals">
<%= render_tabs issue_history_tabs, issue_history_default_tab %>
</div>
<%= render partial: 'action_menu_edit' unless User.current.wants_comments_in_reverse_order? %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<%= f.link_to 'PDF' %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<% end %>
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
diff --git a/app/views/issues/tabs/_changesets.html.erb b/app/views/issues/tabs/_changesets.html.erb
index bf00dcb7b..2df4d40c2 100644
--- a/app/views/issues/tabs/_changesets.html.erb
+++ b/app/views/issues/tabs/_changesets.html.erb
@@ -1,27 +1,29 @@
<% @changesets.each do |changeset| %>
<div id="changeset-<%= changeset.id %>" class="changeset journal">
- <div class="note">
- <h4 class='note-header'>
- <%= avatar(changeset.user, :size => "24") %>
- <%= authoring changeset.committed_on, changeset.author, :label => :label_added_time_by %>
+ <h4 class="journal-header">
+ <span class="journal-info">
+ <%= avatar(changeset.user, :size => "24") %>
+ <%= authoring changeset.committed_on, changeset.author, :label => :label_added_time_by %>
+ </span>
</h4>
- <p>
- <%= "#{changeset.project.name} - " unless changeset.project == project %>
- <%= link_to_revision(changeset, changeset.repository,
- :text => "#{l(:label_revision)} #{changeset.format_identifier}") %>
- <% if changeset.filechanges.any? && User.current.allowed_to?(:browse_repository, changeset.project) %>
- (<%= link_to(l(:label_diff),
- :controller => 'repositories',
- :action => 'diff',
- :id => changeset.project,
- :repository_id => changeset.repository.identifier_param,
- :path => "",
- :rev => changeset.identifier) %>)
- <% end %></p>
-
- <div class="wiki changeset-comments">
- <%= format_changeset_comments changeset %>
- </div>
+ <div class="journal-content">
+ <p>
+ <%= "#{changeset.project.name} - " unless changeset.project == project %>
+ <%= link_to_revision(changeset, changeset.repository,
+ :text => "#{l(:label_revision)} #{changeset.format_identifier}") %>
+ <% if changeset.filechanges.any? && User.current.allowed_to?(:browse_repository, changeset.project) %>
+ (<%= link_to(l(:label_diff),
+ :controller => 'repositories',
+ :action => 'diff',
+ :id => changeset.project,
+ :repository_id => changeset.repository.identifier_param,
+ :path => "",
+ :rev => changeset.identifier) %>)
+ <% end %>
+ </p>
+ <div class="wiki changeset-comments">
+ <%= format_changeset_comments changeset %>
+ </div>
</div>
</div>
<%= call_hook(:view_issues_history_changeset_bottom, { :changeset => changeset }) %>
diff --git a/app/views/issues/tabs/_history.html.erb b/app/views/issues/tabs/_history.html.erb
index aa5795400..b416a9d37 100644
--- a/app/views/issues/tabs/_history.html.erb
+++ b/app/views/issues/tabs/_history.html.erb
@@ -5,34 +5,39 @@
<% reply_links = issue.notes_addable? -%>
<% for journal in journals %>
- <div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
+ <div id="change-<%= journal.id %>" class="<%= journal.css_classes %>" data-controller="quote-reply">
<div id="note-<%= journal.indice %>" class="note">
- <div class="contextual">
- <span class="journal-actions"><%= render_journal_actions(issue, journal, :reply_links => reply_links) %></span>
- <a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
- </div>
- <h4 class='note-header'>
- <%= avatar(journal.user) %>
- <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
- <%= render_private_notes_indicator(journal) %>
- <%= render_journal_update_info(journal) %>
- </h4>
-
- <% if journal.details.any? %>
- <ul class="details">
- <% details_to_strings(journal.visible_details).each do |string| %>
- <li><%= string %></li>
- <% end %>
- </ul>
- <% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %>
- <div class="thumbnails">
- <% thumbnail_attachments.each do |attachment| %>
- <%= thumbnail_tag(attachment) %>
+ <h4 class="journal-header">
+ <span class="journal-info">
+ <%= avatar(journal.user) %>
+ <%= authoring journal.created_on, journal.user, :label => :label_updated_time_by %>
+ <%= render_private_notes_indicator(journal) %>
+ <%= render_journal_update_info(journal) %>
+ </span>
+ <span class="journal-meta">
+ <span class="journal-actions">
+ <%= render_journal_actions(issue, journal, :reply_links => reply_links) %>
+ </span>
+ <a href="#note-<%= journal.indice %>" class="journal-link">#<%= journal.indice %></a>
+ </span>
+ </h4>
+ <div class="journal-content">
+ <% if journal.details.any? %>
+ <ul class="journal-details">
+ <% details_to_strings(journal.visible_details).each do |string| %>
+ <li><%= string %></li>
+ <% end %>
+ </ul>
+ <% if Setting.thumbnails_enabled? && (thumbnail_attachments = journal_thumbnail_attachments(journal)).any? %>
+ <div class="thumbnails">
+ <% thumbnail_attachments.each do |attachment| %>
+ <%= thumbnail_tag(attachment) %>
+ <% end %>
+ </div>
+ <% end %>
<% end %>
+ <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
- <% end %>
- <% end %>
- <%= render_notes(issue, journal, :reply_links => reply_links) unless journal.notes.blank? %>
</div>
</div>
<%= call_hook(:view_issues_history_journal_bottom, { :journal => journal }) %>
diff --git a/app/views/issues/tabs/_time_entries.html.erb b/app/views/issues/tabs/_time_entries.html.erb
index 4cbf5d01e..86a23d0c5 100644
--- a/app/views/issues/tabs/_time_entries.html.erb
+++ b/app/views/issues/tabs/_time_entries.html.erb
@@ -1,31 +1,33 @@
-<% for time_entry in time_entries%>
+<% for time_entry in time_entries %>
<div id="time-entry-<%= time_entry.id %>" class="time_entry journal">
- <div class="note">
- <% if time_entry.editable_by?(User.current) -%>
- <div class="contextual">
- <span class="journal-actions">
- <%= link_to sprite_icon('edit', l(:button_edit)), edit_time_entry_path(time_entry),
- :title => l(:button_edit),
- :class => 'icon-only icon-edit ' %>
- <%= link_to sprite_icon('del', l(:button_delete)), time_entry_path(time_entry),
- :data => {:confirm => l(:text_are_you_sure)},
- :method => :delete,
- :title => l(:button_delete),
- :class => 'icon-only icon-del ' %>
+ <h4 class="journal-header">
+ <span class="journal-info">
+ <%= avatar(time_entry.user, :size => "24") %>
+ <%= authoring time_entry.created_on, time_entry.user, :label => :label_added_time_by %>
</span>
- </div>
- <% end -%>
- <h4 class='note-header'>
- <%= avatar(time_entry.user, :size => "24") %>
- <%= authoring time_entry.created_on, time_entry.user, :label => :label_added_time_by %>
+ <% if time_entry.editable_by?(User.current) -%>
+ <span class="journal-meta">
+ <%= link_to sprite_icon('edit', l(:button_edit)), edit_time_entry_path(time_entry),
+ :title => l(:button_edit),
+ :class => 'icon-only icon-edit' %>
+ <%= link_to sprite_icon('del', l(:button_delete)), time_entry_path(time_entry),
+ :data => { :confirm => l(:text_are_you_sure) },
+ :method => :delete,
+ :title => l(:button_delete),
+ :class => 'icon-only icon-del' %>
+ </span>
+ <% end -%>
</h4>
- <ul class="details">
- <li>
- <strong><%= l(:label_time_entry_plural) %></strong>:
- <%= l_hours_short time_entry.hours %>
- </li>
- </ul>
- <p><%= time_entry.comments %></p>
+ <div class="journal-content">
+ <ul class="journal-details">
+ <li>
+ <strong><%= l(:label_time_entry_plural) %></strong>:
+ <%= l_hours_short time_entry.hours %>
+ </li>
+ </ul>
+ <div class="journal-note">
+ <%= time_entry.comments %>
+ </div>
</div>
</div>
<%= call_hook(:view_issues_history_time_entry_bottom, { :time_entry => time_entry }) %>
diff --git a/app/views/journals/update.js.erb b/app/views/journals/update.js.erb
index 227d169fc..cf6bcd28f 100644
--- a/app/views/journals/update.js.erb
+++ b/app/views/journals/update.js.erb
@@ -7,7 +7,7 @@
$("#journal-<%= @journal.id %>-notes").replaceWith('<%= escape_javascript(render_notes(@journal.issue, @journal, :reply_links => authorize_for('issues', 'edit'))) %>');
$("#journal-<%= @journal.id %>-notes").show();
$("#journal-<%= @journal.id %>-form").remove();
- var journal_header = $("#change-<%= @journal.id %>>div.note>h4.note-header");
+ var journal_header = $("#change-<%= @journal.id %>>div.note>h4.journal-header>.journal-info");
var journal_updated_info = journal_header.find("span.update-info");
if (journal_updated_info.length > 0) {
journal_updated_info.replaceWith('<%= escape_javascript(render_journal_update_info(@journal)) %>');
@@ -15,6 +15,8 @@
journal_header.append('<%= escape_javascript(render_journal_update_info(@journal)) %>');
}
setupWikiTableSortableHeader();
+ setupCopyButtonsToPreElements();
+ setupHoverTooltips();
<% end %>
<%= call_hook(:view_journals_update_js_bottom, { :journal => @journal }) %>
diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb
index 3432cb655..9e2ef51b5 100644
--- a/app/views/layouts/base.html.erb
+++ b/app/views/layouts/base.html.erb
@@ -10,8 +10,10 @@
<%= favicon %>
<%= stylesheet_link_tag 'jquery/jquery-ui-1.13.2', 'tribute-5.1.3', 'application', 'responsive', :media => 'all' %>
<%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
+<%= javascript_importmap_tags %>
<%= javascript_heads %>
<%= heads_for_theme %>
+<%= heads_for_i18n %>
<%= heads_for_auto_complete(@project) %>
<%= call_hook :view_layouts_base_html_head %>
<!-- page specific tags -->
@@ -34,11 +36,9 @@
<% end %>
<% if User.current.logged? %>
- <div class="flyout-menu__avatar <% if !Setting.gravatar_enabled? %>flyout-menu__avatar--no-avatar<% end %>">
- <% if Setting.gravatar_enabled? %>
- <%= link_to(avatar(User.current, :size => "80"), user_path(User.current)) %>
- <% end %>
- <%= link_to_user(User.current, :format => :username) %>
+ <div class="flyout-menu__avatar">
+ <%= link_to(avatar(User.current, :size => "40"), user_path(User.current)) %>
+ <%= link_to_user(User.current, :format => :username) %>
</div>
<% end %>
@@ -106,7 +106,7 @@
<% if sidebar_content? %>
<div id="sidebar-switch-panel" style="visibility: hidden;">
<a id="sidebar-switch-button" class="" href="#">
- <%= sprite_icon("chevrons-right", size: 20) %></a>
+ <%= sprite_icon("chevrons-right", size: 20, rtl: true) %></a>
</div>
<%= javascript_tag "$('#sidebar-switch-panel').css('visibility', 'visible');" %>
<% end %>
@@ -129,6 +129,7 @@
<div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
<div id="ajax-modal" style="display:none;"></div>
+<div id="icon-copy-source" style="display: none;"><%= sprite_icon('') %></div>
</div>
<%= call_hook :view_layouts_base_body_bottom %>
diff --git a/app/views/messages/show.html.erb b/app/views/messages/show.html.erb
index b265cc962..e60c803b7 100644
--- a/app/views/messages/show.html.erb
+++ b/app/views/messages/show.html.erb
@@ -1,99 +1,104 @@
-<% content_for :header_tags do %>
- <%= javascripts_for_quote_reply_include_tag %>
-<% end %>
-
<%= board_breadcrumb(@message) %>
-<div class="contextual">
+<div data-controller="quote-reply">
+ <div class="contextual">
<%= watcher_link(@topic, User.current) %>
- <%= quote_reply(
- url_for(:action => 'quote', :id => @topic, :format => 'js'),
- '#message_topic_wiki'
+ <%= quote_reply_button(
+ url: url_for(action: 'quote', id: @topic, format: 'js')
) if !@topic.locked? && authorize_for('messages', 'reply') %>
<%= link_to(
sprite_icon('edit', l(:button_edit)),
- {:action => 'edit', :id => @topic},
+ { :action => 'edit', :id => @topic },
:class => 'icon icon-edit'
) if @message.editable_by?(User.current) %>
<%= link_to(
sprite_icon('del', l(:button_delete)),
- {:action => 'destroy', :id => @topic},
+ { :action => 'destroy', :id => @topic },
:method => :post,
- :data => {:confirm => l(:text_are_you_sure)},
+ :data => { :confirm => l(:text_are_you_sure) },
:class => 'icon icon-del'
- ) if @message.destroyable_by?(User.current) %>
-</div>
+ ) if @message.destroyable_by?(User.current) %>
+ </div>
-<h2><%= avatar(@topic.author) %><%= @topic.subject %></h2>
+ <h2><%= avatar(@topic.author) %><%= @topic.subject %></h2>
-<div class="message">
-<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
-<div id="message_topic_wiki" class="wiki">
-<%= textilizable(@topic, :content) %>
-</div>
-<%= link_to_attachments @topic, :author => false, :thumbnails => true %>
+ <div class="message">
+ <div class="reaction">
+ <%= reaction_button @topic %>
+ </div>
+ <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
+ <div id="message_topic_wiki" class="wiki" data-quote-reply-target="content">
+ <%= textilizable(@topic, :content) %>
+ </div>
+ <%= link_to_attachments @topic, :author => false, :thumbnails => true %>
+ </div>
</div>
-<br />
+<br/>
<% unless @replies.empty? %>
-<div id="replies">
-<h3 class="comments icon icon-comments"><%= sprite_icon('comments', l(:label_reply_plural)) %> (<%= @reply_count %>)</h3>
-<% if !@topic.locked? && authorize_for('messages', 'reply') && @replies.size >= 3 %>
- <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content', :scroll => "message_content" %></p>
-<% end %>
-<% @replies.each do |message| %>
- <div class="message reply" id="<%= "message-#{message.id}" %>">
- <div class="contextual">
- <%= quote_reply(
- url_for(:action => 'quote', :id => message, :format => 'js'),
- "#message-#{message.id} .wiki",
- icon_only: true
- ) if !@topic.locked? && authorize_for('messages', 'reply') %>
- <%= link_to(
- sprite_icon('edit', l(:button_edit), icon_only: true),
- {:action => 'edit', :id => message},
- :title => l(:button_edit),
- :class => 'icon icon-edit'
- ) if message.editable_by?(User.current) %>
- <%= link_to(
- sprite_icon('del', l(:button_delete), icon_only: true),
- {:action => 'destroy', :id => message},
- :method => :post,
- :data => {:confirm => l(:text_are_you_sure)},
- :title => l(:button_delete),
- :class => 'icon icon-del'
- ) if message.destroyable_by?(User.current) %>
- </div>
- <h4 class='reply-header'>
- <%= avatar(message.author) %>
- <%= link_to message.subject, { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
- -
- <%= authoring message.created_on, message.author %>
- </h4>
- <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
- <%= link_to_attachments message, :author => false, :thumbnails => true %>
+ <div id="replies" class="journals">
+ <h3 class="comments icon icon-comments"><%= sprite_icon('comments', l(:label_reply_plural)) %>
+ (<%= @reply_count %>)</h3>
+ <% if !@topic.locked? && authorize_for('messages', 'reply') && @replies.size >= 3 %>
+ <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content', :scroll => "message_content" %></p>
+ <% end %>
+ <% @replies.each do |message| %>
+ <div class="message reply journal" id="<%= "message-#{message.id}" %>" data-controller="quote-reply">
+ <h4 class='reply-header journal-header'>
+ <span class="journal-info">
+ <%= avatar(message.author) %>
+ <%= link_to message.subject, { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
+ -
+ <%= authoring message.created_on, message.author %>
+ </span>
+ <span class="journal-meta">
+ <%= reaction_button message %>
+ <%= quote_reply_button(
+ url: url_for(action: 'quote', id: message, format: 'js'),
+ icon_only: true
+ ) if !@topic.locked? && authorize_for('messages', 'reply') %>
+ <%= link_to(
+ sprite_icon('edit', l(:button_edit), icon_only: true),
+ { :action => 'edit', :id => message },
+ :title => l(:button_edit),
+ :class => 'icon icon-edit'
+ ) if message.editable_by?(User.current) %>
+ <%= link_to(
+ sprite_icon('del', l(:button_delete), icon_only: true),
+ { :action => 'destroy', :id => message },
+ :method => :post,
+ :data => { :confirm => l(:text_are_you_sure) },
+ :title => l(:button_delete),
+ :class => 'icon icon-del'
+ ) if message.destroyable_by?(User.current) %>
+ </span>
+ </h4>
+ <div class="wiki journal-content" data-quote-reply-target="content">
+ <%= textilizable message, :content, :attachments => message.attachments %>
+ </div>
+ <%= link_to_attachments message, :author => false, :thumbnails => true %>
+ </div>
+ <% end %>
</div>
-<% end %>
-</div>
-<span class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></span>
+ <span class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></span>
<% end %>
<% if !@topic.locked? && authorize_for('messages', 'reply') %>
-<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
-<div id="reply" style="display:none;">
-<%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
- <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
- <%= submit_tag l(:button_submit) %>
-<% end %>
-</div>
+ <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
+ <div id="reply" style="display:none;">
+ <%= form_for @reply, :as => :reply, :url => { :action => 'reply', :id => @topic }, :html => { :multipart => true, :id => 'message-form' } do |f| %>
+ <%= render :partial => 'form', :locals => { :f => f, :replying => true } %>
+ <%= submit_tag l(:button_submit) %>
+ <% end %>
+ </div>
<% end %>
<% html_title @topic.subject %>
<% content_for :sidebar do %>
<% if User.current.allowed_to?(:add_message_watchers, @project) ||
- (@topic.watchers.present? && User.current.allowed_to?(:view_message_watchers, @project)) %>
+ (@topic.watchers.present? && User.current.allowed_to?(:view_message_watchers, @project)) %>
<div id="watchers">
- <%= render :partial => 'watchers/watchers', :locals => {:watched => @topic} %>
+ <%= render :partial => 'watchers/watchers', :locals => { :watched => @topic } %>
</div>
<% end %>
<% end %>
diff --git a/app/views/my/account.html.erb b/app/views/my/account.html.erb
index c8706a5f5..95afbabac 100644
--- a/app/views/my/account.html.erb
+++ b/app/views/my/account.html.erb
@@ -1,6 +1,7 @@
<div class="contextual">
<%= additional_emails_link(@user) %>
<%= link_to(sprite_icon('key', l(:button_change_password)), { :action => 'password'}, :class => 'icon icon-passwd') if @user.change_password_allowed? %>
+<%= link_to(sprite_icon('apps', l('label_oauth_authorized_application_plural')), oauth_authorized_applications_path, :class => 'icon icon-applications') if Setting.rest_api_enabled? %>
<%= call_hook(:view_my_account_contextual, :user => @user)%>
</div>
diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb
index d07a09eb7..704d3d04e 100644
--- a/app/views/news/show.html.erb
+++ b/app/views/news/show.html.erb
@@ -22,30 +22,43 @@
</div>
<% end %>
-<p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %>
-<span class="author"><%= authoring @news.created_on, @news.author %></span></p>
-<div class="wiki">
-<%= textilizable(@news, :description) %>
+<div class="news">
+ <div class="reaction">
+ <%= reaction_button @news %>
+ </div>
+ <p><% unless @news.summary.blank? %><em><%= @news.summary %></em><br /><% end %>
+ <span class="author"><%= authoring @news.created_on, @news.author %></span></p>
+ <div class="wiki">
+ <%= textilizable(@news, :description) %>
+ </div>
+ <%= link_to_attachments @news %>
</div>
-<%= link_to_attachments @news %>
<br />
-<div id="comments" style="margin-bottom:16px;">
+<div id="comments" class="journals">
<h3 class="comments"><%= l(:label_comment_plural) %></h3>
<% if @news.commentable? && @comments.size >= 3 %>
<p><%= toggle_link l(:label_comment_add), "add_comment_form", :focus => "comment_comments", :scroll => "comment_comments" %></p>
<% end %>
<% @comments.each do |comment| %>
- <% next if comment.new_record? %>
- <div class="contextual">
- <%= link_to_if_authorized sprite_icon('del', l(:button_delete)), { :controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
- :data => {:confirm => l(:text_are_you_sure)}, :method => :delete,
- :title => l(:button_delete),
- :class => 'icon-only icon-del' %>
- </div>
- <h4><%= avatar(comment.author) %><%= authoring comment.created_on, comment.author %></h4>
- <div class="wiki">
- <%= textilizable(comment.comments) %>
+ <div class="message reply journal" id="<%= "message-#{comment.id}" %>">
+ <% next if comment.new_record? %>
+ <h4 class="reply-header journal-header">
+ <span class="journal-info">
+ <%= avatar(comment.author) %>
+ <%= authoring comment.created_on, comment.author %>
+ </span>
+ <span class="journal-meta">
+ <%= reaction_button comment %>
+ <%= link_to_if_authorized sprite_icon('del', l(:button_delete)), { :controller => 'comments', :action => 'destroy', :id => @news, :comment_id => comment},
+ :data => {:confirm => l(:text_are_you_sure)}, :method => :delete,
+ :title => l(:button_delete),
+ :class => 'icon-only icon-del' %>
+ </span>
+ </h4>
+ <div class="wiki journal-content">
+ <%= textilizable(comment.comments) %>
+ </div>
</div>
<% end if @comments.any? %>
</div>
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index 3a1c047c9..875e736b1 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -31,10 +31,10 @@
<% end %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<% if @query.display_type == 'list' %>
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
<% end %>
+ <%= f.link_to 'Atom', :url => {:key => User.current.atom_key} %>
<% end %>
<% html_title(l(:label_project_plural)) -%>
diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb
index a1118f6ab..42756775a 100644
--- a/app/views/queries/_filters.html.erb
+++ b/app/views/queries/_filters.html.erb
@@ -22,6 +22,5 @@ $(document).ready(function(){
<%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
</div>
-<div id="icon-copy-source" style="display: none;"><%= sprite_icon('') %></div>
<%= hidden_field_tag 'f[]', '' %>
<% include_calendar_headers_tags %>
diff --git a/app/views/queries/_query_form.html.erb b/app/views/queries/_query_form.html.erb
index d04cd290e..77094e16e 100644
--- a/app/views/queries/_query_form.html.erb
+++ b/app/views/queries/_query_form.html.erb
@@ -6,7 +6,7 @@
<div id="query_form_content">
<fieldset id="filters" class="collapsible <%= @query.new_record? ? "" : "collapsed" %>">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= @query.new_record? ? "expanded" : "collapsed" %>">
- <%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right") %>
+ <%= sprite_icon(@query.new_record? ? "angle-down" : "angle-right", rtl: !@query.new_record?) %>
<%= l(:label_filter_plural) %>
</legend>
<div style="<%= @query.new_record? ? "" : "display: none;" %>">
@@ -17,7 +17,7 @@
<% if @query.available_columns.any? %>
<fieldset id="options" class="collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
- <%= sprite_icon("angle-right") %>
+ <%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_options) %>
</legend>
<div class="hidden">
diff --git a/app/views/reactions/_replace_button.js.erb b/app/views/reactions/_replace_button.js.erb
new file mode 100644
index 000000000..a5c923ea4
--- /dev/null
+++ b/app/views/reactions/_replace_button.js.erb
@@ -0,0 +1,7 @@
+(() => {
+ const button = $('[data-reaction-button-id=<%= reaction_id_for @object %>]');
+
+ removeHoverTooltips(button);
+ button.html($('<%=j reaction_button @object %>').children());
+ setupHoverTooltips(button);
+})();
diff --git a/app/views/reactions/create.js.erb b/app/views/reactions/create.js.erb
new file mode 100644
index 000000000..20f3cc7ed
--- /dev/null
+++ b/app/views/reactions/create.js.erb
@@ -0,0 +1 @@
+<%= render 'replace_button' %>
diff --git a/app/views/reactions/destroy.js.erb b/app/views/reactions/destroy.js.erb
new file mode 100644
index 000000000..20f3cc7ed
--- /dev/null
+++ b/app/views/reactions/destroy.js.erb
@@ -0,0 +1 @@
+<%= render 'replace_button' %>
diff --git a/app/views/repositories/_breadcrumbs.html.erb b/app/views/repositories/_breadcrumbs.html.erb
index 4a5903e14..15b7b2c5f 100644
--- a/app/views/repositories/_breadcrumbs.html.erb
+++ b/app/views/repositories/_breadcrumbs.html.erb
@@ -9,7 +9,7 @@ breadcrumbs << link_to(
@repository.identifier.presence || 'root', :action => 'show',
:id => @project, :repository_id => @repository.identifier_param,
:path => nil, :rev => @rev)
-link_path = ''
+link_path = +''
dirs.each do |dir|
next if dir.blank?
diff --git a/app/views/repositories/_dir_list_content.html.erb b/app/views/repositories/_dir_list_content.html.erb
index 991400d7a..aed3dcc0c 100644
--- a/app/views/repositories/_dir_list_content.html.erb
+++ b/app/views/repositories/_dir_list_content.html.erb
@@ -14,7 +14,7 @@
:path => to_path_param(ent_path),
:rev => @rev,
:depth => (depth + 1),
- :parent_id => tr_id)) %>');"><%= sprite_icon('angle-right') %></span>
+ :parent_id => tr_id)) %>');"><%= sprite_icon('angle-right', rtl: true) %></span>
<% end %>
<%= link_to file_icon(entry, ent_name),
{:action => (entry.is_dir? ? 'show' : 'entry'), :id => @project, :repository_id => @repository.identifier_param, :path => to_path_param(ent_path), :rev => @rev},
diff --git a/app/views/roles/permissions.html.erb b/app/views/roles/permissions.html.erb
index 63a1267fc..573fbc9fa 100644
--- a/app/views/roles/permissions.html.erb
+++ b/app/views/roles/permissions.html.erb
@@ -3,7 +3,7 @@
<div class="hide-when-print">
<fieldset id="filters" class="collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
- <%= sprite_icon("angle-right") %>
+ <%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_filter_plural) %>
</legend>
<div style="display: none;">
diff --git a/app/views/search/index.html.erb b/app/views/search/index.html.erb
index 7b5fc4f36..c17bbd8ea 100644
--- a/app/views/search/index.html.erb
+++ b/app/views/search/index.html.erb
@@ -25,7 +25,7 @@
<fieldset class="collapsible collapsed">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
- <%= sprite_icon("angle-right") %>
+ <%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_options) %>
</legend>
<div id="options-content" style="display:none;">
diff --git a/app/views/settings/_display.html.erb b/app/views/settings/_display.html.erb
index 62c53dfbb..3b2f95798 100644
--- a/app/views/settings/_display.html.erb
+++ b/app/views/settings/_display.html.erb
@@ -22,7 +22,12 @@
<p><%= setting_check_box :gravatar_enabled, :data => {:enables => '#settings_gravatar_default'} %>
<em class="info"><%= t(:text_avatar_server_config_html, :url => Redmine::Configuration['avatar_server_url']) %></em></p>
-<p><%= setting_select :gravatar_default, gravatar_default_setting_options, :blank => :label_none %></p>
+<p>
+ <%= setting_select :gravatar_default, gravatar_default_setting_options, :blank => :label_none %>
+ <em class="<%= Setting.gravatar_default == "initials" ? "info" : "hidden" %>">
+ <%= t(:text_setting_gravatar_default_initials_html) %>
+ </em>
+</p>
<p><%= setting_check_box :thumbnails_enabled, :data => {:enables => '#settings_thumbnails_size'} %></p>
@@ -35,3 +40,18 @@
<%= submit_tag l(:button_save) %>
<% end %>
+
+<%= javascript_tag do %>
+ $('#settings_gravatar_default').on('change', function(e){
+ const gravatar_default = e.target.value;
+ const em = e.target.parentElement.getElementsByTagName('em')[0];
+
+ if (gravatar_default === 'initials') {
+ em.classList.remove('hidden');
+ em.classList.add('info');
+ } else {
+ em.classList.add('hidden');
+ em.classList.remove('info');
+ }
+ });
+<% end %> \ No newline at end of file
diff --git a/app/views/settings/_general.html.erb b/app/views/settings/_general.html.erb
index 043067f18..44206b6c2 100644
--- a/app/views/settings/_general.html.erb
+++ b/app/views/settings/_general.html.erb
@@ -37,6 +37,8 @@
<p><%= setting_text_field :feeds_limit, :size => 6 %></p>
+<p><%= setting_check_box :reactions_enabled %></p>
+
<%= call_hook(:view_settings_general_form) %>
</div>
diff --git a/app/views/timelog/_list.html.erb b/app/views/timelog/_list.html.erb
index 1a82b5f51..aa1c1c293 100644
--- a/app/views/timelog/_list.html.erb
+++ b/app/views/timelog/_list.html.erb
@@ -11,7 +11,7 @@
<% @query.inline_columns.each do |column| %>
<%= column_header(@query, column) %>
<% end %>
- <th></th>
+ <th class="buttons hide-when-print"></th>
</tr>
</thead>
<tbody>
@@ -36,7 +36,7 @@
<% @query.inline_columns.each do |column| %>
<%= content_tag('td', column_content(column, entry), :class => column.css_classes) %>
<% end %>
- <td class="buttons">
+ <td class="buttons hide-when-print">
<% if entry.editable_by?(User.current) -%>
<%= link_to sprite_icon('edit', l(:button_edit)), edit_time_entry_path(entry),
:title => l(:button_edit),
diff --git a/app/views/timelog/index.html.erb b/app/views/timelog/index.html.erb
index 55e2312b3..d9985e922 100644
--- a/app/views/timelog/index.html.erb
+++ b/app/views/timelog/index.html.erb
@@ -29,8 +29,8 @@
<span class="pagination"><%= pagination_links_full @entry_pages, @entry_count %></span>
<% other_formats_links do |f| %>
- <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
+ <%= f.link_to_with_query_parameters 'Atom', :key => User.current.atom_key %>
<% end %>
<div id="csv-export-options" style="display:none;">
diff --git a/app/views/users/show.api.rsb b/app/views/users/show.api.rsb
index bf415795d..0681903b8 100644
--- a/app/views/users/show.api.rsb
+++ b/app/views/users/show.api.rsb
@@ -11,7 +11,7 @@ api.user do
api.passwd_changed_on @user.passwd_changed_on
api.avatar_url gravatar_url(@user.mail, {rating: nil, size: nil, default: Setting.gravatar_default}) if @user.mail && Setting.gravatar_enabled?
api.twofa_scheme @user.twofa_scheme if User.current.admin? || (User.current == @user)
- api.api_key @user.api_key if User.current.admin? || (User.current == @user)
+ api.api_key @user.api_key if (User.current.admin? || (User.current == @user && !User.current.authorized_by_oauth?))
api.status @user.status if User.current.admin?
render_api_custom_values @user.visible_custom_field_values, api
diff --git a/app/views/versions/_sidebar.html.erb b/app/views/versions/_sidebar.html.erb
index 3b5269ed8..a1b9452d8 100644
--- a/app/views/versions/_sidebar.html.erb
+++ b/app/views/versions/_sidebar.html.erb
@@ -42,7 +42,7 @@
</ul>
<% if @completed_versions.present? %>
<p>
- <%= link_to_function sprite_icon('angle-right', l(:label_completed_versions)),
+ <%= link_to_function sprite_icon('angle-right', l(:label_completed_versions), rtl: true),
'$("#toggle-completed-versions").toggleClass("icon-collapsed icon-expanded"); $("#completed-versions").toggle(); toggleExpendCollapseIcon(this);',
:id => 'toggle-completed-versions', :class => 'icon icon-collapsed collapsible' %>
<ul id = "completed-versions" style = "display:none;">
diff --git a/app/views/versions/index.html.erb b/app/views/versions/index.html.erb
index 6c3d518bc..45b254a7d 100644
--- a/app/views/versions/index.html.erb
+++ b/app/views/versions/index.html.erb
@@ -40,7 +40,7 @@
<td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
<td class="assigned_to"><%= assignee_avatar(issue.assigned_to, :size => 16) %></td>
<td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
- <td class="buttons"><%= link_to_context_menu %></td>
+ <td class="buttons hide-when-print"><%= link_to_context_menu %></td>
</tr>
<% end -%>
</table>
diff --git a/app/views/versions/show.html.erb b/app/views/versions/show.html.erb
index f83aff80d..cdd2b3029 100644
--- a/app/views/versions/show.html.erb
+++ b/app/views/versions/show.html.erb
@@ -54,7 +54,7 @@
<td class="checkbox"><%= check_box_tag 'ids[]', issue.id, false, :id => nil %></td>
<td class="assigned_to"><%= assignee_avatar(issue.assigned_to, :size => 16) %></td>
<td class="subject"><%= link_to_issue(issue, :project => (@project != issue.project)) %></td>
- <td class="buttons"><%= link_to_context_menu %></td>
+ <td class="buttons hide-when-print"><%= link_to_context_menu %></td>
</tr>
<% end %>
</table>
diff --git a/app/views/wiki/date_index.html.erb b/app/views/wiki/date_index.html.erb
index 7ee5b467d..c8acf933c 100644
--- a/app/views/wiki/date_index.html.erb
+++ b/app/views/wiki/date_index.html.erb
@@ -29,11 +29,11 @@
<% unless @pages.empty? %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %>
<% if User.current.allowed_to?(:export_wiki_pages, @project) %>
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
<% end %>
+ <%= f.link_to 'Atom', :url => {:controller => 'activities', :action => 'index', :id => @project, :show_wiki_edits => 1, :key => User.current.atom_key} %>
<% end %>
<% end %>
diff --git a/app/views/wiki/index.html.erb b/app/views/wiki/index.html.erb
index 3f6d05fc2..a4afcb28e 100644
--- a/app/views/wiki/index.html.erb
+++ b/app/views/wiki/index.html.erb
@@ -22,14 +22,14 @@
<% unless @pages.empty? %>
<% other_formats_links do |f| %>
- <%= f.link_to 'Atom',
- :url => {:controller => 'activities', :action => 'index',
- :id => @project, :show_wiki_edits => 1,
- :key => User.current.atom_key} %>
<% if User.current.allowed_to?(:export_wiki_pages, @project) %>
<%= f.link_to('PDF', :url => {:action => 'export', :format => 'pdf'}) %>
<%= f.link_to('HTML', :url => {:action => 'export'}) %>
<% end %>
+ <%= f.link_to 'Atom',
+ :url => {:controller => 'activities', :action => 'index',
+ :id => @project, :show_wiki_edits => 1,
+ :key => User.current.atom_key} %>
<% end %>
<% end %>
diff --git a/app/views/wiki/show.html.erb b/app/views/wiki/show.html.erb
index 7c35463a0..4b222ef4b 100644
--- a/app/views/wiki/show.html.erb
+++ b/app/views/wiki/show.html.erb
@@ -63,7 +63,7 @@
<fieldset class="collapsible collapsed hide-when-print">
<legend onclick="toggleFieldset(this);" class="icon icon-collapsed">
- <%= sprite_icon("angle-right") %>
+ <%= sprite_icon("angle-right", rtl: true) %>
<%= l(:label_attachment_plural) %> (<%= @page.attachments.length %>)
</legend>
<div style="display: none;">
diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb
index 2262fc1d1..3265f68de 100644
--- a/app/views/workflows/edit.html.erb
+++ b/app/views/workflows/edit.html.erb
@@ -41,7 +41,7 @@
<fieldset class="collapsible" style="padding: 0; margin-top: 0.5em;">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= @workflows['author'].present? ? "expanded" : "collapsed" %>">
- <%= sprite_icon(@workflows['author'].present? ? "angle-down" : "angle-right") %>
+ <%= sprite_icon(@workflows['author'].present? ? "angle-down" : "angle-right", rtl: !@workflows['author'].present?) %>
<%= l(:label_additional_workflow_transitions_for_author) %>
</legend>
<div id="author_workflows" style="margin: 0.5em 0 0.5em 0;">
@@ -52,7 +52,7 @@
<fieldset class="collapsible" style="padding: 0;">
<legend onclick="toggleFieldset(this);" class="icon icon-<%= @workflows['assignee'].present? ? "expanded" : "collapsed" %>">
- <%= sprite_icon(@workflows['assignee'].present? ? "angle-down" : "angle-right") %>
+ <%= sprite_icon(@workflows['assignee'].present? ? "angle-down" : "angle-right", rtl: !@workflows['assignee'].present?) %>
<%= l(:label_additional_workflow_transitions_for_assignee) %>
</legend>
<div id="assignee_workflows" style="margin: 0.5em 0 0.5em 0;">
diff --git a/bin/importmap b/bin/importmap
new file mode 100644
index 000000000..36502ab16
--- /dev/null
+++ b/bin/importmap
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+
+require_relative "../config/application"
+require "importmap/commands"
diff --git a/config/application.rb b/config/application.rb
index 1beeb2db2..96c6f9fb4 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -59,6 +59,7 @@ module RedmineApp
config.i18n.enforce_available_locales = true
config.i18n.fallbacks = true
config.i18n.default_locale = 'en'
+ config.i18n.available_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym }
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
diff --git a/config/boot.rb b/config/boot.rb
index 7479b5aff..e7d68c4ad 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,5 +1,21 @@
# frozen_string_literal: true
+# Rack 3.1.14 or later sets default limits of 4MB for query string bytesize
+# and 4096 for the number of query parameters. These limits are too low
+# for Redmine and can cause the following issues:
+#
+# - The low bytesize limit prevents the mail handler from processing incoming
+# emails larger than 4MB (https://www.redmine.org/issues/42962)
+# - The low parameter limit prevents saving workflows with many statuses
+# (https://www.redmine.org/issues/42875)
+#
+# See also:
+# - https://github.com/rack/rack/blob/v3.1.16/README.md#configuration
+# - https://github.com/rack/rack/blob/v3.1.16/lib/rack/query_parser.rb#L54
+# - https://github.com/rack/rack/blob/v3.1.16/lib/rack/query_parser.rb#L57
+ENV['RACK_QUERY_PARSER_BYTESIZE_LIMIT'] ||= '33554432'
+ENV['RACK_QUERY_PARSER_PARAMS_LIMIT'] ||= '65536'
+
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
diff --git a/config/database.yml.example b/config/database.yml.example
index 6b0849602..9fdccac06 100644
--- a/config/database.yml.example
+++ b/config/database.yml.example
@@ -4,6 +4,8 @@
production:
adapter: mysql2
+ # You can also use "trilogy", an adapter for MySQL-compatible servers.
+ # adapter: trilogy
database: redmine
host: localhost
username: root
@@ -19,6 +21,7 @@ production:
development:
adapter: mysql2
+ # adapter: trilogy
database: redmine_development
host: localhost
username: root
@@ -33,6 +36,7 @@ development:
# Do not set this db to the same as development or production.
test:
adapter: mysql2
+ # adapter: trilogy
database: redmine_test
host: localhost
username: root
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 5b3ff43dc..d41e817f3 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -59,4 +59,10 @@ Rails.application.configure do
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
+
+ config.after_initialize do
+ Bullet.enable = true
+ Bullet.rails_logger = true
+ Bullet.skip_user_in_notification = true
+ end
end
diff --git a/config/icon_source.yml b/config/icon_source.yml
index d48944c91..64f0a1d8c 100644
--- a/config/icon_source.yml
+++ b/config/icon_source.yml
@@ -220,3 +220,19 @@
svg: eye
- name: unwatch
svg: eye-off
+- name: copy-pre-content
+ svg: clipboard
+- name: thumb-up
+ svg: thumb-up
+- name: thumb-up-filled
+ svg: thumb-up
+ style: filled
+- name: alert-circle
+ svg: alert-circle
+- name: bulb
+ svg: bulb
+- name: message-report
+ svg: message-report
+- name: quote-filled
+ svg: quote
+ style: filled \ No newline at end of file
diff --git a/config/importmap.rb b/config/importmap.rb
new file mode 100644
index 000000000..3560330c1
--- /dev/null
+++ b/config/importmap.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+# Pin npm packages by running ./bin/importmap
+
+pin "application"
+pin "@hotwired/stimulus", to: "stimulus.min.js"
+pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
+pin "turndown" # @7.2.0
+pin_all_from "app/javascript/controllers", under: "controllers"
diff --git a/config/initializers/30-redmine.rb b/config/initializers/30-redmine.rb
index cf13cab20..16bcebec4 100644
--- a/config/initializers/30-redmine.rb
+++ b/config/initializers/30-redmine.rb
@@ -4,9 +4,7 @@ require 'redmine/configuration'
require 'redmine/plugin_loader'
Rails.application.config.to_prepare do
- I18n.backend = Redmine::I18n::Backend.new
- # Forces I18n to load available locales from the backend
- I18n.config.available_locales = nil
+ I18n::Backend::Simple.include(I18n::Backend::Pluralization)
# Use Nokogiri as XML backend instead of Rexml
ActiveSupport::XmlMini.backend = 'Nokogiri'
@@ -23,6 +21,72 @@ end
Redmine::PluginLoader.load
Rails.application.config.to_prepare do
+ Doorkeeper.configure do
+ orm :active_record
+
+ # Issue access tokens with refresh token
+ use_refresh_token
+
+ # Authorization Code expiration time (default: 10 minutes).
+ #
+ # authorization_code_expires_in 10.minutes
+
+ # Access token expiration time (default: 2 hours).
+ # If you want to disable expiration, set this to `nil`.
+ #
+ # access_token_expires_in 2.hours
+
+ # Hash access and refresh tokens before persisting them.
+ # https://doorkeeper.gitbook.io/guides/security/token-and-application-secrets
+ hash_token_secrets
+
+ # Hash application secrets before persisting them.
+ hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt'
+
+ # limit supported flows to Auth code
+ grant_flows ['authorization_code']
+
+ realm Redmine::Info.app_name
+ base_controller 'ApplicationController'
+ default_scopes(*Redmine::AccessControl.public_permissions.map(&:name))
+ optional_scopes(*(Redmine::AccessControl.permissions.map(&:name) << :admin))
+
+ # Forbids creating/updating applications with arbitrary scopes that are
+ # not in configuration, i.e. +default_scopes+ or +optional_scopes+.
+ enforce_configured_scopes
+
+ allow_token_introspection false
+
+ # allow http loopback redirect URIs but require https for all others
+ force_ssl_in_redirect_uri { |uri| !%w[localhost 127.0.0.1 web localohst:8080].include?(uri.host) }
+
+ # Specify what redirect URI's you want to block during Application creation.
+ forbid_redirect_uri { |uri| %w[data vbscript javascript].include?(uri.scheme.to_s.downcase) }
+
+ resource_owner_authenticator do
+ if require_login
+ if Setting.rest_api_enabled?
+ User.current
+ else
+ deny_access
+ end
+ end
+ end
+
+ admin_authenticator do |_routes|
+ if !Setting.rest_api_enabled? || !User.current.admin?
+ deny_access
+ end
+ end
+ end
+
+ # Use Redmine standard layouts and helpers for Doorkeeper OAuth2 screens
+ Doorkeeper::ApplicationsController.layout "admin"
+ Doorkeeper::ApplicationsController.main_menu = false
+ Doorkeeper::AuthorizationsController.layout "base"
+ Doorkeeper::AuthorizedApplicationsController.layout "base"
+ Doorkeeper::AuthorizedApplicationsController.main_menu = false
+
default_paths = []
default_paths << Rails.root.join("app/assets/javascripts")
default_paths << Rails.root.join("app/assets/images")
@@ -42,6 +106,14 @@ Rails.application.config.to_prepare do
paths = theme.asset_paths
Rails.application.config.assets.redmine_extension_paths << paths if paths.present?
end
+
+ Doorkeeper::ApplicationsController.class_eval do
+ require_sudo_mode :create, :show, :update, :destroy
+ end
+
+ Doorkeeper::AuthorizationsController.class_eval do
+ require_sudo_mode :create, :destroy
+ end
end
Rails.application.deprecators[:redmine] = ActiveSupport::Deprecation.new('7.0', 'Redmine')
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
new file mode 100644
index 000000000..40888ad8b
--- /dev/null
+++ b/config/initializers/doorkeeper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+# rubocop:disable Lint/EmptyBlock
+Doorkeeper.configure do
+end
+
+Rails.application.config.to_prepare do
+end
+# rubocop:enable Lint/EmptyBlock
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index f31207988..ab68e841e 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -94,8 +94,9 @@ ar:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "و"
- skip_last_comma: false
+ last_word_connector: " و "
+ two_words_connector: " و "
+ words_connector: " ، "
activerecord:
errors:
@@ -1502,3 +1503,34 @@ ar:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/az.yml b/config/locales/az.yml
index 5cee01eac..54b623974 100644
--- a/config/locales/az.yml
+++ b/config/locales/az.yml
@@ -208,9 +208,9 @@ az:
support:
array:
- # Rails 2.2
- sentence_connector: "və"
- skip_last_comma: true
+ last_word_connector: " və "
+ two_words_connector: " və "
+ words_connector: ", "
# Rails 2.3
words_connector: ", "
@@ -1594,3 +1594,34 @@ az:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index a176cc620..e4eb0d224 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -96,8 +96,9 @@ bg:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "и"
- skip_last_comma: false
+ last_word_connector: " и "
+ two_words_connector: " и "
+ words_connector: ", "
activerecord:
errors:
@@ -210,6 +211,7 @@ bg:
error_can_not_delete_custom_field: Невъзможност за изтриване на потребителско поле
error_can_not_delete_tracker_html: Този тракер съдържа задачи и не може да бъде изтрит.<p>The following projects have issues with this tracker:<br>%{projects}</p>
error_can_not_remove_role: Тази роля се използва и не може да бъде изтрита.
+ error_can_not_remove_role_reason_members_html: "<p>Следващите проекти имат членове с тази роля:<br>%{projects}</p>"
error_can_not_reopen_issue_on_closed_version: Задача, асоциирана със затворена версия не може да бъде отворена отново
error_can_not_archive_project: Този проект не може да бъде архивиран
error_issue_done_ratios_not_updated: Процентът на завършените задачи не е обновен.
@@ -238,6 +240,7 @@ bg:
error_exceeds_maximum_hours_per_day: Не можете да запишете повече от %{max_hours} часа на един ден (%{logged_hours} часове вече са записани)
error_can_not_delete_auth_source: Този режим за идентификация се използва и не може да бъде премахнат.
error_spent_on_future_date: Не е възможно да се отчете изразходвано време на дата в бъдещето
+ error_spent_on_closed_issue: Не е възможно да се отчете изразходвано време за затворена задача
error_not_allowed_to_log_time_for_other_users: Вие нямате разрешение да записвате изразходвано време за други потребители
error_can_not_execute_macro_html: Грешка при изпълнение на <strong>%{name}</strong> макрос
(%{error})
@@ -522,9 +525,13 @@ bg:
setting_timelog_accept_0_hours: Приемане на записи с 0 часа
setting_timelog_max_hours_per_day: Максимум часове, които могат да бъдат записани за ден и за потребител
setting_timelog_accept_future_dates: Разрешено отчитане на изразходвано време на дата в бъдещето
+ setting_timelog_accept_closed_issues: Разрешено отчитане на изразходвано време за затворени задачи
setting_show_status_changes_in_mail_subject: Показване на промените на състоянието на задачите в поле Относно на имейлите
setting_project_list_defaults: Проектен списък
setting_twofa: Двуфакторна автентикация
+ setting_related_issues_default_columns: Колони по подразбиране за свързани и подзадачи
+ setting_display_related_issues_table_headers: Показване на заглавия на таблиците
+ setting_reactions_enabled: Разрешаване на реакциите
permission_add_project: Създаване на проект
permission_add_subprojects: Създаване на подпроекти
@@ -1338,6 +1345,7 @@ bg:
Бие можете да го конфигурирате в config/configuration.yml.
text_allowed_queries_to_select: Само публичните (за всички потребители) заявки са достъпни за избор
text_setting_config_change: Вие можете да конфигурирате поведението в config/configuration.yml. Моля, рестартирайте Redmine след редактиране на файла.
+ text_setting_gravatar_default_initials_html: Инициалите на потребителите са изпратени на <a href="https://www.gravatar.com">https://www.gravatar.com</a> за генериране на техните аватари.
default_role_manager: Мениджър
default_role_developer: Разработчик
@@ -1389,6 +1397,7 @@ bg:
label_import_time_entries: Импортиране на записи за използвано време
label_import_users: Импортиране на потребители
sudo_mode_new_info_html: "<strong>Какво се случва?</strong> Трябва да потвърдите вашата парола преди да предприемете административни действия. Това осигурява вашият акаунт."
+ label_progressbar: Прогрес бар
twofa__totp__name: Authenticator app
twofa__totp__text_pairing_info_html: Сканирайте този QR код или въведете текстовия ключ
@@ -1443,7 +1452,28 @@ bg:
За да потвърдите, въведете името (%{login}) по-долу.
text_project_destroy_enter_identifier: За да потвърдите действието, въведете идентификатора на проекта (%{identifier}) по-долу.
field_name_or_email_or_login: Име, e-mail или login име
- setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
- label_progressbar: Progress bar
- error_spent_on_closed_issue: Cannot log time on a closed issue
- setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_wiki_tablesort_enabled: Сортиране на таблици чрез Javascript във wiki страници
+ reaction_text_x_other_users:
+ one: 1 друг
+ other: "%{count} други"
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/bs.yml b/config/locales/bs.yml
index d8799f421..be1d37733 100644
--- a/config/locales/bs.yml
+++ b/config/locales/bs.yml
@@ -113,8 +113,9 @@ bs:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "i"
- skip_last_comma: false
+ last_word_connector: " i "
+ two_words_connector: " i "
+ words_connector: ", "
activerecord:
errors:
@@ -1488,3 +1489,34 @@ bs:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 5904d26d4..f047922c6 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -99,8 +99,9 @@ ca:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "i"
- skip_last_comma: false
+ last_word_connector: ", i "
+ two_words_connector: " i "
+ words_connector: ", "
activerecord:
errors:
@@ -1489,3 +1490,34 @@ ca:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 070ffacd7..c2438194d 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -99,8 +99,9 @@ cs:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "a"
- skip_last_comma: false
+ last_word_connector: " a "
+ two_words_connector: " a "
+ words_connector: ", "
activerecord:
errors:
@@ -379,7 +380,7 @@ cs:
permission_manage_project_activities: Spravovat aktivity projektu
permission_manage_versions: Spravování verzí
permission_manage_categories: Spravování kategorií úkolů
- permission_view_issues: Zobrazit úkoly
+ permission_view_issues: Zobrazení úkolů
permission_add_issues: Přidávání úkolů
permission_edit_issues: Upravování úkolů
permission_manage_issue_relations: Spravování vazeb mezi úkoly
@@ -390,7 +391,7 @@ cs:
permission_manage_public_queries: Správa veřejných dotazů
permission_save_queries: Ukládání dotazů
permission_view_gantt: Zobrazení ganttova diagramu
- permission_view_calendar: Prohlížení kalendáře
+ permission_view_calendar: Zobrazení kalendáře
permission_view_issue_watchers: Zobrazení seznamu sledujících uživatelů
permission_add_issue_watchers: Přidání sledujících uživatelů
permission_delete_issue_watchers: Smazat sledující uživatele
@@ -400,23 +401,23 @@ cs:
permission_edit_own_time_entries: Upravování vlastních zázamů o stráveném čase
permission_manage_news: Spravování novinek
permission_comment_news: Komentování novinek
- permission_view_documents: Prohlížení dokumentů
+ permission_view_documents: Zobrazení dokumentů
permission_manage_files: Spravování souborů
- permission_view_files: Prohlížení souborů
+ permission_view_files: Zobrazení souborů
permission_manage_wiki: Spravování Wiki
permission_rename_wiki_pages: Přejmenovávání Wiki stránek
permission_delete_wiki_pages: Mazání stránek na Wiki
- permission_view_wiki_pages: Prohlížení Wiki
- permission_view_wiki_edits: Prohlížení historie Wiki
+ permission_view_wiki_pages: Zobrazení Wiki
+ permission_view_wiki_edits: Zobrazení historie Wiki
permission_edit_wiki_pages: Upravování stránek Wiki
permission_delete_wiki_pages_attachments: Mazání příloh
permission_protect_wiki_pages: Zabezpečení Wiki stránek
permission_manage_repository: Spravování repozitáře
permission_browse_repository: Procházení repozitáře
- permission_view_changesets: Zobrazování revizí
+ permission_view_changesets: Zobrazení revizí
permission_commit_access: Commit přístup
permission_manage_boards: Správa diskusních fór
- permission_view_messages: Prohlížení příspěvků
+ permission_view_messages: Zobrazení příspěvků
permission_add_messages: Posílání příspěvků
permission_edit_messages: Upravování příspěvků
permission_edit_own_messages: Upravit vlastní příspěvky
@@ -1033,7 +1034,7 @@ cs:
label_any_issues_in_project: jakékoli úkoly v projektu
label_any_issues_not_in_project: jakékoli úkoly mimo projekt
field_private_notes: Soukromé poznámky
- permission_view_private_notes: Zobrazit soukromé poznámky
+ permission_view_private_notes: Zobrazení soukromých poznámek
permission_set_notes_private: Nastavit poznámky jako soukromé
label_no_issues_in_project: žádné úkoly v projektu
label_any: vše
@@ -1205,7 +1206,7 @@ cs:
field_digest: Kontrolní součet
field_default_assigned_to: Výchozí přiřazený uživatel
setting_show_custom_fields_on_registration: Zobraz uživatelská pole při registraci
- permission_view_news: Zobraz novinky
+ permission_view_news: Zobrazení novinek
label_no_preview_alternative_html: "Náhled není k dispozici. Soubor: %{link}."
label_no_preview_download: Stažení
setting_close_duplicate_issues: Automaticky uzavři duplicitní úkoly
@@ -1256,7 +1257,7 @@ cs:
permission_edit_own_issues: Upravování vlastních úkolů
text_select_apply_tracker: Použít frontu
label_updated_issues: Aktualizované úkoly
- text_avatar_server_config_html: Aktuální avatar server je <a href="%{url}">%{url}</a>.
+ text_avatar_server_config_html: Aktuální Gravatar server je <a href="%{url}">%{url}</a>.
Nastavení lze změnit v config/configuration.yml.
setting_gantt_months_limit: Maximální počet měsíců zobrazených v ganttově diagramu
permission_import_time_entries: Import stráveného času
@@ -1480,7 +1481,37 @@ cs:
zero: "%{filename}"
one: "%{filename} a 1 soubor"
other: "%{filename} a %{count} soubory(ů)"
- setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
- label_progressbar: Progress bar
- error_spent_on_closed_issue: Cannot log time on a closed issue
- setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_wiki_tablesort_enabled: Třídení tabulek na Wiki založené na JavaScriptu
+ label_progressbar: Indikátor průběhu
+ error_spent_on_closed_issue: Nelze zaznamenat čas na uzavřený úkol
+ setting_timelog_accept_closed_issues: Povolit zaznamenání časů na uzavřených úkolech
+ setting_related_issues_default_columns: Výchozí sloupce zobrazené v seznamu souvisejících úkolů
+ setting_display_related_issues_table_headers: Zobrazit záhlaví tabulky
+ error_can_not_remove_role_reason_members_html: "<p>Následující projekty mají členy
+ s touto rolí:<br>%{projects}</p>"
+ setting_reactions_enabled: Povolit reakce
+ reaction_text_x_other_users:
+ one: 1 jiný
+ other: "%{count} jiných"
+ text_setting_gravatar_default_initials_html: Uživatelovy iniciály jsou poslány do <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ k vygenerování avataru.
+ permission_view_project: Zobrazení projektů
+ permission_search_project: Vyhledání projektů
+ permission_view_members: Zobrazení členů projektu
+ label_oauth_permission_admin: Spravovat Redmine
+ label_oauth_admin_access: Administrace
+ label_oauth_application_plural: Aplikace
+ label_oauth_authorized_application_plural: Autorizované aplikace
+ text_oauth_admin_permission: Plný administrátorský přítup. Po potvrzení administrátorem
+ bude aplikace oprávněná číst a zapisovat všechna data a vydávat se za jiné uživatele.
+ text_oauth_admin_permission_info: Tato aplikace vyžaduje plný administrátorský přístup.
+ Jestliže jste administrátorem, nebo se jím stanete, bude moct číst
+ a zapisovat všechna data a vydávat se za jiné uživatele vaším jménem. Jestliže tomu chcete zabránit,
+ autorizujte ji jako uživatel bez práv administrátora.
+ text_oauth_copy_secret_now: Zkopírujte si kód na bezpečné místo, příště už nebude zobrazen.
+ text_oauth_implicit_permissions: Zobrazení Vašeho jména, přihlašovacího jména a hlavního e-mailu
+ text_oauth_info_scopes: Vyberte rozsah, který může aplikace požadovat. Aplikace
+ nebude oprávněna dělat více než je zde vybráno. Vždy to bude také
+ omezeno rolí a členstvím v projektu uživatele, který to povolil.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/da.yml b/config/locales/da.yml
index cd04ad7ab..1226d4a3c 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -30,8 +30,9 @@ da:
support:
array:
- sentence_connector: "og"
- skip_last_comma: true
+ last_word_connector: " og "
+ two_words_connector: " og "
+ words_connector: ", "
datetime:
distance_in_words:
@@ -1519,3 +1520,34 @@ da:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/de.yml b/config/locales/de.yml
index c14c36e5d..9c8da2187 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -109,8 +109,9 @@ de:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "und"
- skip_last_comma: true
+ last_word_connector: " und "
+ two_words_connector: " und "
+ words_connector: ", "
activerecord:
errors:
@@ -970,6 +971,9 @@ de:
permission_view_time_entries: Gebuchte Aufwände ansehen
permission_view_wiki_edits: Wiki-Versionsgeschichte ansehen
permission_view_wiki_pages: Wiki ansehen
+ permission_view_project: Projekte ansehen
+ permission_search_project: Projekte suchen
+ permission_view_members: Projektmitglieder anzeigen
project_module_boards: Foren
project_module_calendar: Kalender
@@ -1468,3 +1472,22 @@ de:
setting_wiki_tablesort_enabled: Javascript-basierte Tabellensortierung in Wiki-Inhalten
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ label_oauth_permission_admin: Administrator-Zugriff
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applikationen
+ label_oauth_authorized_application_plural: Autorisierte Applikationen
+ text_oauth_admin_permission: Voller Admin-Zugriff. Wenn diese Applikation durch einen Administrator autorisiert wird, kann sie alle Daten lesen und schreiben, auch im Namen anderer Benutzer.
+ text_oauth_admin_permission_info: Diese Applikation verlangt vollen Administrator-Zugriff. Wenn Sie ein Administrator sind (oder in Zukunft Administrator werden), wird sie in der Lage sein, alle Daten zu lesen und zu schreiben, auch im Namen anderer Benutzer. Dies kann vermieden werden, indem die Applikation mit einem anderen Benutzerkonto ohne Administrator-Privileg autorisiert wird.
+ text_oauth_copy_secret_now: Das Geheimnis bitte jetzt an einen sicheren Ort kopieren, es kann nicht erneut angezeigt werden.
+ text_oauth_implicit_permissions: Zugriff auf Benutzername, Login sowie auf die primäre Email-Adresse
+ text_oauth_info_scopes: Scopes für die Applikation auswählen. Die Applikation wird niemals mehr Rechte haben als hier ausgewählt. Sie wird außerdem auf die Rollen und Projektmitgliedschaften des Benutzers, der sie autorisiert hat, beschränkt sein.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/el.yml b/config/locales/el.yml
index ced271d8e..642d77281 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -96,8 +96,9 @@ el:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "and"
- skip_last_comma: false
+ last_word_connector: " και "
+ two_words_connector: " και "
+ words_connector: ", "
activerecord:
errors:
@@ -1502,3 +1503,34 @@ el:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml
index 44446612e..1fc956a0c 100644
--- a/config/locales/en-GB.yml
+++ b/config/locales/en-GB.yml
@@ -99,8 +99,9 @@ en-GB:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "and"
- skip_last_comma: false
+ last_word_connector: ", and "
+ two_words_connector: " and "
+ words_connector: ", "
activerecord:
errors:
@@ -1503,3 +1504,34 @@ en-GB:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f6e1d2d96..ef7f17e2f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -95,8 +95,9 @@ en:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "and"
- skip_last_comma: false
+ last_word_connector: ", and "
+ two_words_connector: " and "
+ words_connector: ", "
activerecord:
errors:
@@ -138,6 +139,9 @@ en:
must_contain_special_chars: "must contain special characters (!, $, %, ...)"
domain_not_allowed: "contains a domain not allowed (%{domain})"
too_simple: "is too simple"
+ attributes:
+ doorkeeper/application:
+ scopes: Scopes
actionview_instancetag_blank_option: Please select
@@ -207,6 +211,7 @@ en:
error_can_not_delete_custom_field: Unable to delete custom field
error_can_not_delete_tracker_html: "This tracker contains issues and cannot be deleted.<p>The following projects have issues with this tracker:<br>%{projects}</p>"
error_can_not_remove_role: "This role is in use and cannot be deleted."
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members with this role:<br>%{projects}</p>"
error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
error_can_not_archive_project: This project cannot be archived
error_issue_done_ratios_not_updated: "Issue done ratios not updated."
@@ -526,6 +531,7 @@ en:
setting_twofa: Two-factor authentication
setting_related_issues_default_columns: Related and sub issues list defaults
setting_display_related_issues_table_headers: Show table headers
+ setting_reactions_enabled: Enable reactions
permission_add_project: Create project
permission_add_subprojects: Create subprojects
@@ -602,6 +608,10 @@ en:
permission_manage_related_issues: Manage related issues
permission_import_issues: Import issues
permission_log_time_for_other_users: Log spent time for other users
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+
project_module_issue_tracking: Issue tracking
project_module_time_tracking: Time tracking
@@ -1111,6 +1121,8 @@ en:
label_relations_mapping: Relations mapping
label_file_content_preview: File content preview
label_create_missing_values: Create missing values
+ label_position: Position
+ label_message: Message
label_api: API
label_field_format_enumeration: Key/value list
label_default_values_for_new_users: Default values for new users
@@ -1155,6 +1167,10 @@ en:
label_time_by_author: "%{time} by %{author}"
label_involved_principals: Author / Previous assignee
label_progressbar: Progress bar
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
button_login: Login
button_submit: Submit
@@ -1339,6 +1355,12 @@ en:
text_no_subject: no subject
text_allowed_queries_to_select: Public (to any users) queries only selectable
text_setting_config_change: You can configure the behaviour in config/configuration.yml. Please restart the application after editing it.
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a> to generate their avatars.
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator, this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access. If you are an Administrator (or become one in the future), it will be able to read and write all data and impersonate other users on your behalf. If you want to avoid this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application will not be allowed to do more than what is selected here. It will also always be limited by the roles and project memberships of the user who authorized it.
default_role_manager: Manager
default_role_developer: Developer
@@ -1430,3 +1452,6 @@ en:
text_project_destroy_enter_identifier: "To confirm, please enter the project's identifier (%{identifier}) below."
field_name_or_email_or_login: Name, email or login
setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
+ reaction_text_x_other_users:
+ one: "1 other"
+ other: "%{count} others"
diff --git a/config/locales/es-PA.yml b/config/locales/es-PA.yml
index 0ee321400..e13baf1f2 100644
--- a/config/locales/es-PA.yml
+++ b/config/locales/es-PA.yml
@@ -191,7 +191,9 @@ es-PA:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "y"
+ last_word_connector: " y "
+ two_words_connector: " y "
+ words_connector: ", "
actionview_instancetag_blank_option: Por favor seleccione
@@ -1532,3 +1534,34 @@ es-PA:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/es.yml b/config/locales/es.yml
index db96b053e..f4abb327d 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -189,7 +189,9 @@ es:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "y"
+ last_word_connector: " y "
+ two_words_connector: " y "
+ words_connector: ", "
actionview_instancetag_blank_option: Por favor seleccione
@@ -1568,3 +1570,34 @@ es:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/et.yml b/config/locales/et.yml
index b7a06cfa8..8f3dc3d88 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -112,8 +112,9 @@ et:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "ja"
- skip_last_comma: false
+ last_word_connector: " ja "
+ two_words_connector: " ja "
+ words_connector: ", "
activerecord:
errors:
@@ -1507,3 +1508,34 @@ et:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/eu.yml b/config/locales/eu.yml
index 303dd5ce9..cfdba7ad0 100644
--- a/config/locales/eu.yml
+++ b/config/locales/eu.yml
@@ -97,8 +97,9 @@ eu:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "eta"
- skip_last_comma: false
+ last_word_connector: " eta "
+ two_words_connector: " eta "
+ words_connector: ", "
activerecord:
errors:
@@ -1503,3 +1504,34 @@ eu:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index fa8fce508..9e4e58d1e 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -98,8 +98,9 @@ fa:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "و"
- skip_last_comma: false
+ last_word_connector: " و "
+ two_words_connector: " و "
+ words_connector: "، "
activerecord:
errors:
@@ -140,7 +141,7 @@ fa:
must_contain_digits: "باید شامل عدد باشد (0-9)"
must_contain_special_chars: "باید شامل نویسه‌های خاص باشد (!, $, %, ...)"
domain_not_allowed: "شامل دامنه‌ی غیرمجازست (%{domain})"
- too_simple: "is too simple"
+ too_simple: "خیلی ساده است"
actionview_instancetag_blank_option: انتخاب کنید
@@ -274,10 +275,9 @@ fa:
mail_body_security_notification_notify_disabled: "نشانی رایانامه %{value} دیگر آگاه‌سازی دریافت نخواهد کرد."
mail_body_settings_updated: "تنظیمات زیر تغییر کردند:"
mail_body_password_updated: "گذرواژه شما تغییر کرد."
- mail_destroy_project_failed: Project %{value} could not be deleted.
- mail_destroy_project_successful: Project %{value} was deleted successfully.
- mail_destroy_project_with_subprojects_successful: Project %{value} and its subprojects
- were deleted successfully.
+ mail_destroy_project_failed: پروژه %{value} نمی‌تواند حذف شود.
+ mail_destroy_project_successful: پروژه %{value} با موفقیت حذف شد.
+ mail_destroy_project_with_subprojects_successful: پروژه %{value} و زیرپروژه‌های آن با موفقیت حذف شدند.
field_name: نام
field_description: توضیح
@@ -421,6 +421,10 @@ fa:
field_default_issue_query: جستار پیش‌فرض مسئله‌ها
field_default_project_query: جستار پیش‌فرض پروژه
field_default_time_entry_activity: دسته‌بندی پیش‌فرض زمان صرف شده
+ field_any_searchable: هر متن قابل جستجویی
+ field_estimated_remaining_hours: باقی‌مانده زمان برآورد شده
+ field_last_activity_date: آخرین فعالیت
+ field_thousands_delimiter: جداکننده هزارگان
setting_app_title: عنوان برنامه
setting_welcome_text: متن خوش‌آمد گویی
@@ -478,6 +482,7 @@ fa:
setting_default_projects_modules: ابزارهای پیش‌فرض فعال برای پروژه‌های جدید
setting_issue_done_ratio: برآورد اندازه انجام شده مسئله با
setting_issue_done_ratio_issue_field: محاسبه بر اساس درصد پیش‌رفت مسئله
+ setting_issue_done_ratio_interval: فاصله گزینه‌های درصد انجام
setting_issue_done_ratio_issue_status: محاسبه بر اساس وضعیت مسئله
setting_start_of_week: آغاز تقویم از
setting_rest_api_enabled: فعال‌سازی وب سرویس‌های REST
@@ -502,6 +507,7 @@ fa:
setting_force_default_language_for_anonymous: الزام زبان پیش فرض برای کاربران ناشناس
setting_force_default_language_for_loggedin: الزام زبان پیش‌فرض برای کاربران واردشده
setting_link_copied_issue: ارتباط مسائل در هنگام رونوشت
+ setting_copy_attachments_on_issue_copy: رونوشت پیوست‌ها در زمان رونوشت
setting_max_additional_emails: بیشینه تعداد نشانی‌های رایانامه
setting_email_domains_allowed: دامنه‌های مجاز برای نشانی رایانامه
setting_email_domains_denied: دامنه‌های غیرمجاز برای نشانی رایانامه
@@ -525,6 +531,7 @@ fa:
permission_edit_project: ویرایش پروژه
permission_close_project: بستن / بازگشایی پروژه
permission_delete_project: حذف پروژه
+ permission_select_project_publicity: تنظیم پروژه به عمومی یا محرمانه
permission_select_project_modules: انتخاب ابزارهای پروژه
permission_manage_members: مدیریت اعضا
permission_manage_project_activities: مدیریت دسته‌بندی زمان‌های پروژه
@@ -631,6 +638,7 @@ fa:
label_issue_assigned_to_updated: مسئول به‌روز شد
label_issue_priority_updated: اولویت به‌روز شد
label_issue_fixed_version_updated: نسخه هدف به‌روز شد
+ label_issue_attachment_added: پیوست اضافه شد
label_document: سند
label_document_new: سند جدید
label_document_plural: اسناد
@@ -712,6 +720,10 @@ fa:
label_attachment_plural: پیوست‌ها
label_file_added: پرونده افزوده شد
label_attachment_description: توضیحات پیوست
+ label_attachment_summary:
+ zero: "%{filename}"
+ one: "%{filename} و 1 پرونده"
+ other: "%{filename} و %{count} پرونده"
label_report: گزارش
label_report_plural: گزارش‌ها
label_news: مطلب
@@ -811,6 +823,7 @@ fa:
label_more_than_ago: بعد از چند روز پیش
label_ago: روز قبل
label_contains: شامل
+ label_contains_any_of: شامل یکی از
label_not_contains: فاقد
label_starts_with: شروع با
label_ends_with: پایان با
@@ -819,6 +832,9 @@ fa:
label_no_issues_in_project: مسئله‌ای در پروژه وجود ندارد
label_any_open_issues: هر مسئله‌ی باز
label_no_open_issues: بدون هیچ مسئله باز
+ label_has_been: بوده است
+ label_has_never_been: هرگز نبوده است
+ label_changed_from: تغییر کرده از
label_day_plural: روز
label_repository: مخزن
label_repository_new: مخزن جدید
@@ -838,6 +854,7 @@ fa:
label_latest_revision_plural: آخرین بازبینی‌ها
label_view_revisions: مشاهده بازبینی‌ها
label_view_all_revisions: مشاهده همه بازبینی‌ها
+ label_view_previous_annotation: مشاهده حاشیه‌نویسی قبل از این تغییر
label_x_revisions: "%{count} بازبینی"
label_max_size: بیشترین اندازه
label_roadmap: نقشه راه
@@ -963,6 +980,7 @@ fa:
label_optional_description: توضیح اختیاری
label_add_another_file: افزودن پرونده دیگر
label_auto_watch_on: ناظر خودکار
+ label_auto_watch_on_issue_created: مسئله‌هایی که من ساخته‌ام
label_auto_watch_on_issue_contributed_to: مسئله‌هایی که در آن‌ها مشارکت داشته‌ام
label_preferences: ترجیح‌ها
label_chronological_order: به ترتیب تاریخ
@@ -1115,6 +1133,8 @@ fa:
label_inherited_from_group: "ارث‌بری از گروه %{name}"
label_trackers_description: توضیحات انواع مسئله
label_open_trackers_description: نمایش توضیحات تمامی انواع مسئله
+ label_issue_statuses_description: توضیحات وضعیت‌های مسئله
+ label_open_issue_statuses_description: دیدن توضیحات همه وضعیت‌های مسئله‌ها
label_preferred_body_part_text: متن
label_preferred_body_part_html: HTML
label_issue_history_properties: تغییرات بخش‌ها
@@ -1132,6 +1152,7 @@ fa:
label_default_query: جستار پیش‌فرض
label_edited: ویرایش شده
label_time_by_author: "%{time} توسط %{author}"
+ label_involved_principals: نویسنده / مسئول قبلی
button_login: ورود
button_submit: ثبت
@@ -1197,6 +1218,7 @@ fa:
button_edit_object: ویرایش %{object_name}
button_delete_object: حذف %{object_name}
button_create_and_follow: ساخت و برگشت
+ button_apply_issues_filter: اعمال غربال مسئله‌ها
status_active: فعال
status_registered: ثبت‌نام‌شده
@@ -1273,6 +1295,7 @@ fa:
text_minimagick_available: MiniMagick در دست‌رس است (اختیاری)
text_convert_available: تبدیل ImageMagick در دست‌رس است (اختیاری)
text_gs_available: پشتیبانی ImageMagick PDF موجود است (اختیاری)
+ text_default_active_job_queue_changed: آداپتور پیش‌فرض صف که به شکل مناسبی کار می‌کند تنها برای توسعه/آزمون تغییر کرد
text_destroy_time_entries_question: "روی مسئله‌هایی که می‌خواهید حذف کنید، %{hours} ساعت زمان ثبت شده است. می‌خواهید چه‌کار کنید؟"
text_destroy_time_entries: ساعت‌های ثبت شده حذف شوند
text_assign_time_entries_to_project: ساعت‌های ثبت شده به پروژه واگذار شوند
@@ -1309,11 +1332,11 @@ fa:
text_project_closed: این پروژه بسته و فقط‌خواندنی است.
text_turning_multiple_off: "اگر این گزینه را غیرفعال کنید، در بخش‌های سفارشیِ ذخیره‌شده، فقط یک مقدار باقی خواهد ماند و بقیه موارد حذف خواهند شد."
text_select_apply_tracker: "انتخاب نوع مسئله"
+ text_select_apply_issue_status: "وضعیت مسئله را انتخا کنید"
text_avatar_server_config_html: خادم فعلی آواتار <a href="%{url}">%{url}</a> می‌باشد. شما می‌توانید آن را در config/configuration.yml تغییر دهید.
text_no_subject: بدون موضوع
text_allowed_queries_to_select: تنها جستارهای عمومی (برای همه کاربران) قابل انتخاب است
- text_setting_config_change: می‌توانید رفتار را در config/configuration.yml ویرایش کنید.
- لطفا پس از تغییر، برنامه را دوباره اجرا کنید.
+ text_setting_config_change: می‌توانید رفتار را در config/configuration.yml ویرایش کنید. لطفا پس از تغییر، برنامه را دوباره اجرا کنید.
default_role_manager: مدیر
default_role_developer: برنامه‌نویس
@@ -1364,22 +1387,18 @@ fa:
label_import_time_entries: ورود زمان‌ها
label_import_users: وارد کردن فهرست کاربران
sudo_mode_new_info_html: "<strong>چه اتفاقی می‌افتد؟</strong> قبل از انجام هرگونه اقداماتِ راه‌بری باید گذرواژه خود را دوباره وارد کنید، این کار تضمین می‌کند که حساب شما محافظت شود."
+
twofa__totp__name: برنامک احراز هویت
- twofa__totp__text_pairing_info_html: لطفا کد QR را را در یک برنامه TOTP اسکن کنید یا کلید متنی را در آن وارد کنید (به عنوان مثال <a href="https://support.google.com/accounts/answer/1066447"> Google
- Authenticator </a>، <a href="https://authy.com/download/"> Authy </a>، <a href = "https://guide.duo.com/third-party-accounts" > Duo
- Mobile </a>) سپس کدی که برنامه در اختیار شما می‌گذارد را در قسمت زیر وارد نمایید تا احراز هویت دو مرحله ای فعال شود.
+ twofa__totp__text_pairing_info_html: لطفا کد QR را را در یک برنامه TOTP اسکن کنید یا کلید متنی را در آن وارد کنید (به عنوان مثال <a href="https://support.google.com/accounts/answer/1066447"> Google Authenticator </a>، <a href="https://authy.com/download/"> Authy </a>، <a href = "https://guide.duo.com/third-party-accounts" > Duo Mobile </a>) سپس کدی که برنامه در اختیار شما می‌گذارد را در قسمت زیر وارد نمایید تا احراز هویت دو مرحله ای فعال شود.
twofa__totp__label_plain_text_key: کلید متنی
twofa__totp__label_activate: فعال‌سازی برنامک احراز هویت
twofa_currently_active: 'فعال: %{twofa_scheme_name}'
twofa_not_active: فعال نشده است
twofa_label_code: کد
twofa_hint_disabled_html: تنظیم <strong>%{label}</strong> احراز هویت دو عاملی را غیرفعال کرده و اتصال همه دستگاه‌های تمامی کاربران را قطع می‌کند.
- twofa_hint_optional_html: تنظیم <strong>%{label}</strong> به کاربران اجازه می‌دهد
- احراز هویت دوعاملی را در صورت تمایل فعال کنند، مگر این‌که توسط یکی از گروه‌های کاربری الزامی شده باشد.
+ twofa_hint_optional_html: تنظیم <strong>%{label}</strong> به کاربران اجازه می‌دهد احراز هویت دوعاملی را در صورت تمایل فعال کنند، مگر این‌که توسط یکی از گروه‌های کاربری الزامی شده باشد.
twofa_hint_required_html: تنظیم <strong>%{label}</strong> تمامی کاربران را ملزم می‌کند تا احراز هویت دو عاملی را در اولین ورود بعدی فعال کنند.
- twofa_hint_required_administrators_html: تنظیم <strong>%{label}</strong> شبیه
- احراز هویت دوعاملی اختیاری عمل می‌کند, اما برای راه‌برها احراز هویت دوعاملی را
- در اولین ورود بعدی الزامی می‌کند.
+ twofa_hint_required_administrators_html: تنظیم <strong>%{label}</strong> شبیه احراز هویت دوعاملی اختیاری عمل می‌کند, اما برای راه‌برها احراز هویت دوعاملی را در اولین ورود بعدی الزامی می‌کند.
twofa_label_setup: فعال‌سازی احراز هویت دو عاملی
twofa_label_deactivation_confirmation: غیرفعال‌سازی احراز هویت دو عاملی
twofa_notice_select: 'لطفاً طرح دو عاملی را که می خواهید استفاده کنید انتخاب کنید:'
@@ -1407,32 +1426,39 @@ fa:
twofa_text_group_disabled: "این تنظیم تنها زمانی مؤثر است که احراز هویت دوعاملی روی «اختیاری» تنظیم شده باشد. در حال حاضر، احراز هویت دوعاملی غیرفعال است."
text_user_destroy_confirmation: مطمئنید که می خواهید این کاربر و همه ارجاع‌های مربوط به او را حذف کنید؟ این کار قابل بازگشت نیست. غالبا، قفل کردن کاربر به جای حذف، راه حل به‌تری است. برای تأیید، لطفاً شناسه ورود کاربر (%{login}) را در زیر وارد کنید.
text_project_destroy_enter_identifier: برای تایید، لطفا شناسه پروژه (%{identifier}) را وارد کنید.
- permission_select_project_publicity: تنظیم عمومی یا خصوصی بودن پروژه
- label_auto_watch_on_issue_created: مسئله‌هایی که من ساخته‌ام
- field_any_searchable: هر متن قابل جستجویی
- label_contains_any_of: شامل یکی از
- button_apply_issues_filter: اعمال غربال مسئله‌ها
- label_view_previous_annotation: مشاهده حاشیه‌نویسی قبل از این تغییر
- label_has_been: بوده است
- label_has_never_been: نبوده است
- label_changed_from: تغییر یافته از
- label_issue_statuses_description: توضیحات وضعیت‌های مسئله
- label_open_issue_statuses_description: مشاهده توضیحات همه وضعیت‌های مسئله‌ها
- text_select_apply_issue_status: وضعیت مسئله را انتخاب کنید
field_name_or_email_or_login: نام، رایانامه یا شناسه کاربری
- text_default_active_job_queue_changed: آداپتور پیش‌فرض صف که به شکل مناسبی کار می‌کند تنها برای توسعه/آزمون تغییر کرد
- label_issue_attachment_added: Attachment added
- field_estimated_remaining_hours: Estimated remaining time
- field_last_activity_date: Last activity
- setting_issue_done_ratio_interval: Done ratio options interval
- setting_copy_attachments_on_issue_copy: Copy attachments on copy
- field_thousands_delimiter: Thousands delimiter
- label_involved_principals: Author / Previous assignee
- label_attachment_summary:
- zero: "%{filename}"
- one: "%{filename} and 1 file"
- other: "%{filename} and %{count} files"
setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index 93bd58182..2105ff7ee 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -33,8 +33,6 @@ fi:
two_words_connector: " ja "
last_word_connector: " ja "
-
-
number:
format:
separator: ","
@@ -1523,3 +1521,34 @@ fi:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index b640ecf73..bbbffe200 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -111,8 +111,6 @@ fr:
support:
array:
- sentence_connector: 'et'
- skip_last_comma: true
word_connector: ", "
two_words_connector: " et "
last_word_connector: " et "
@@ -1482,3 +1480,34 @@ fr:
setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/gl.yml b/config/locales/gl.yml
index aa8ac7481..7df031878 100644
--- a/config/locales/gl.yml
+++ b/config/locales/gl.yml
@@ -123,7 +123,9 @@ gl:
support:
array:
- sentence_connector: e
+ last_word_connector: " e "
+ two_words_connector: " e "
+ words_connector: ", "
activerecord:
models:
@@ -1496,14 +1498,45 @@ gl:
field_estimated_remaining_hours: Tempo restante estimado
field_last_activity_date: Última actividade
setting_issue_done_ratio_interval: Intervalo de proporción de completado
- setting_copy_attachments_on_issue_copy: Copy attachments on copy
- field_thousands_delimiter: Thousands delimiter
- label_involved_principals: Author / Previous assignee
+ setting_copy_attachments_on_issue_copy: Copiar os anexos ao copiar
+ field_thousands_delimiter: Separador de miles
+ label_involved_principals: Autor / Asignado previo
label_attachment_summary:
zero: "%{filename}"
- one: "%{filename} and 1 file"
- other: "%{filename} and %{count} files"
- setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
- label_progressbar: Progress bar
- error_spent_on_closed_issue: Cannot log time on a closed issue
- setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ one: "%{filename} e 1 ficheiro"
+ other: "%{filename} e %{count} ficheiros"
+ setting_wiki_tablesort_enabled: Ordenación de táboas empregando Javascript no contido de wiki
+ label_progressbar: Barra de progreso
+ error_spent_on_closed_issue: Non se pode imputar tempo nunha tarefa pechada
+ setting_timelog_accept_closed_issues: Permitir imputar tempo en tarefas pechadas
+ setting_related_issues_default_columns: Configuración por defecto para listaxes de tarefas relacionadas e fillas
+ setting_display_related_issues_table_headers: Amosar as cabeceiras da táboa
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/he.yml b/config/locales/he.yml
index e50e46274..d6b575bad 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -101,8 +101,9 @@ he:
support:
array:
- sentence_connector: "וגם"
- skip_last_comma: true
+ last_word_connector: " ו"
+ two_words_connector: " ו"
+ words_connector: ", "
activerecord:
errors:
@@ -1507,3 +1508,34 @@ he:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index c5cb0e1c1..23bf9602d 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -90,8 +90,9 @@ hr:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "i"
- skip_last_comma: false
+ last_word_connector: " i "
+ two_words_connector: " i "
+ words_connector: ", "
activerecord:
errors:
@@ -1499,3 +1500,34 @@ hr:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 6d74a883b..79fbe4578 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -114,8 +114,6 @@
support:
array:
-# sentence_connector: "és"
-# skip_last_comma: true
words_connector: ", "
two_words_connector: " és "
last_word_connector: " és "
@@ -1494,3 +1492,34 @@
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/id.yml b/config/locales/id.yml
index f7c5e9089..8ecb084b2 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -95,8 +95,9 @@ id:
support:
array:
- sentence_connector: "dan"
- skip_last_comma: false
+ last_word_connector: ", dan "
+ two_words_connector: " dan "
+ words_connector: ", "
activerecord:
errors:
@@ -1504,3 +1505,34 @@ id:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 8795d1449..b6f34ecbc 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -107,8 +107,9 @@ it:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "e"
- skip_last_comma: false
+ last_word_connector: " e "
+ two_words_connector: " e "
+ words_connector: ", "
activerecord:
errors:
@@ -218,7 +219,7 @@ it:
error_no_tracker_in_project: 'Nessun tracker è associato a questo progetto. Per favore verifica le impostazioni del Progetto.'
error_no_default_issue_status: 'Nessuno stato predefinito delle segnalazioni è configurato. Per favore verifica le impostazioni (Vai in "Amministrazione -> Stati segnalazioni").'
error_can_not_delete_custom_field: Impossibile eliminare il campo personalizzato
- error_can_not_delete_tracker_html: "Questo tracker contiene segnalazioni e non può essere eliminato.<p>The following projects have issues with this tracker:<br>%{projects}</p>"
+ error_can_not_delete_tracker_html: "Questo tracker contiene segnalazioni e non può essere eliminato.<p>I seguenti progetti presentano problemi con questo tracker:<br>%{projects}</p>"
error_can_not_remove_role: "Questo ruolo è in uso e non può essere eliminato."
error_can_not_reopen_issue_on_closed_version: Una segnalazione assegnata ad una versione chiusa non può essere riaperta
error_can_not_archive_project: Questo progetto non può essere archiviato
@@ -730,6 +731,10 @@ it:
label_attachment_plural: File
label_file_added: File aggiunti
label_attachment_description: Descrizione del file
+ label_attachment_summary:
+ zero: "%{filename}"
+ one: "%{filename} e 1 file"
+ other: "%{filename} e %{count} file"
label_report: Report
label_report_plural: Report
label_news: News
@@ -1433,11 +1438,38 @@ it:
text_user_destroy_confirmation: "Vuoi davvero eliminare questo utente e rimuovere tutti i riferimenti ad esso associati? Questa operazione non può essere annullata. Spesso, bloccare un utente anziché eliminarlo è la soluzione migliore. Per confermare, inserisci il suo login (%{login}) qui sotto."
text_project_destroy_enter_identifier: "Per confermare, inserisci l'identificativo del progetto (%{identifier}) qui sotto."
field_name_or_email_or_login: Nome, email o login
- label_attachment_summary:
- zero: "%{filename}"
- one: "%{filename} and 1 file"
- other: "%{filename} and %{count} files"
setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index de23dd6a3..9d51f82a9 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -114,8 +114,9 @@ ja:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "及び"
- skip_last_comma: true
+ last_word_connector: "、"
+ two_words_connector: "、"
+ words_connector: "、"
activerecord:
errors:
@@ -1172,6 +1173,8 @@ ja:
label_fields_mapping: フィールドの対応関係
label_file_content_preview: ファイル内容のプレビュー
label_create_missing_values: 存在しない値は新規作成
+ label_position: 位置
+ label_message: メッセージ
button_import: インポート
field_total_estimated_hours: 合計予定工数
label_api: API
@@ -1449,7 +1452,34 @@ ja:
label_attachment_summary:
zero: "%{filename}"
other: "%{filename} ほか%{count}件"
- setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
- label_progressbar: Progress bar
- error_spent_on_closed_issue: Cannot log time on a closed issue
- setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_wiki_tablesort_enabled: コンテンツ内テーブルの Tablesort (JavaScript) による並べ替え
+ label_progressbar: 進捗バー
+ error_spent_on_closed_issue: 完了したチケットに作業時間を記録することはできません
+ setting_timelog_accept_closed_issues: 完了したチケットへの作業時間の記録を許可
+ setting_related_issues_default_columns: 関連するチケットと子チケットの一覧で表示する項目
+ setting_display_related_issues_table_headers: テーブルヘッダを表示
+ error_can_not_remove_role_reason_members_html: "<p>以下のプロジェクトにこのロールのメンバーがいます:<br>%{projects}</p>"
+ setting_reactions_enabled: リアクション機能を有効にする
+ reaction_text_x_other_users:
+ one: 他1人
+ other: "他%{count}人"
+ text_setting_gravatar_default_initials_html: ユーザーの姓と名それぞれの先頭文字がアイコン生成のために <a href="https://www.gravatar.com">https://www.gravatar.com</a> に送信されます。
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 4a900ef5d..d811f93e8 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -140,8 +140,6 @@ ko:
words_connector: ", "
two_words_connector: "과 "
last_word_connector: ", "
- sentence_connector: "그리고"
- skip_last_comma: false
activerecord:
errors:
@@ -487,7 +485,7 @@ ko:
label_logout: 로그아웃
label_help: 도움말
label_reported_issues: 보고한 일감
- label_assigned_to_me_issues: 내가 맡은 일감
+ label_assigned_to_me_issues: 내가 맡은 일감
label_registered_on: 등록시각
label_activity: 작업내역
label_user_activity: "%{value}의 작업내역"
@@ -648,7 +646,7 @@ ko:
label_options: 옵션
label_copy_workflow_from: 업무흐름 복사하기
label_permissions_report: 권한 보고서
- label_watched_issues: 지켜보고 있는 일감
+ label_watched_issues: 지켜보고 있는 일감
label_related_issues: 연결된 일감
label_applied_status: 적용된 상태
label_loading: 읽는 중...
@@ -847,7 +845,7 @@ ko:
enumeration_doc_categories: 문서 범주
enumeration_activities: 작업분류(시간추적)
- field_issue_to: 관련 일감
+ field_issue_to: 관련 일감
label_view_all_revisions: 모든 개정판 표시
label_tag: 태그(Tag)
label_branch: 브랜치(Branch)
@@ -1021,7 +1019,7 @@ ko:
notice_failed_to_save_time_entries: "%{total} 개의 시간입력중 다음 %{count} 개의 저장에 실패했습니다:: %{ids}."
label_x_issues:
zero: 0 일감
- one: 1 일감
+ one: 1 일감
other: "%{count} 일감"
label_repository_new: 저장소 추가
field_repository_is_default: 주 저장소
@@ -1522,3 +1520,34 @@ ko:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 982f7c111..d768b59f0 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -103,8 +103,9 @@ lt:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "ir"
- skip_last_comma: false
+ last_word_connector: " ir "
+ two_words_connector: " ir "
+ words_connector: ", "
activerecord:
errors:
@@ -1463,3 +1464,34 @@ lt:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index c5990eb19..f88bd078a 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -89,8 +89,9 @@ lv:
support:
array:
- sentence_connector: "un"
- skip_last_comma: false
+ last_word_connector: " un "
+ two_words_connector: " un "
+ words_connector: ", "
activerecord:
errors:
@@ -1496,3 +1497,34 @@ lv:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/mk.yml b/config/locales/mk.yml
index c04e8f2fe..6f69193bf 100644
--- a/config/locales/mk.yml
+++ b/config/locales/mk.yml
@@ -96,8 +96,9 @@ mk:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "и"
- skip_last_comma: false
+ last_word_connector: ", и "
+ two_words_connector: " и "
+ words_connector: ", "
activerecord:
errors:
@@ -1502,3 +1503,34 @@ mk:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/mn.yml b/config/locales/mn.yml
index a9577a211..6f6ef7fc2 100644
--- a/config/locales/mn.yml
+++ b/config/locales/mn.yml
@@ -95,8 +95,9 @@ mn:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "бас"
- skip_last_comma: false
+ last_word_connector: " болон "
+ two_words_connector: " болон "
+ words_connector: ", "
activerecord:
errors:
@@ -1502,3 +1503,34 @@ mn:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 949fd9a26..d937e44d9 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -93,8 +93,9 @@ nl:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "en"
- skip_last_comma: false
+ last_word_connector: " en "
+ two_words_connector: " en "
+ words_connector: ", "
activerecord:
errors:
@@ -1477,3 +1478,34 @@ nl:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/no.yml b/config/locales/no.yml
index 3fb86b91e..26c96e893 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -2,7 +2,9 @@
"no":
support:
array:
- sentence_connector: "og"
+ last_word_connector: " og "
+ two_words_connector: " og "
+ words_connector: ", "
direction: ltr
date:
formats:
@@ -1492,3 +1494,34 @@
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 135f016ae..c5e286563 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -107,8 +107,9 @@ pl:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "i"
- skip_last_comma: true
+ last_word_connector: " oraz "
+ two_words_connector: " i "
+ words_connector: ", "
activerecord:
errors:
@@ -1446,3 +1447,34 @@ pl:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 503bfe3ea..c087d46a2 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -114,8 +114,9 @@ pt-BR:
tb: "TB"
support:
array:
- sentence_connector: "e"
- skip_last_comma: true
+ last_word_connector: " e "
+ two_words_connector: " e "
+ words_connector: ", "
# Active Record
activerecord:
@@ -1507,3 +1508,34 @@ pt-BR:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index bcf4df0cb..34825aea7 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -6,8 +6,9 @@
pt:
support:
array:
- sentence_connector: "e"
- skip_last_comma: true
+ last_word_connector: " e "
+ two_words_connector: " e "
+ words_connector: ", "
direction: ltr
date:
@@ -1495,3 +1496,34 @@ pt:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index fc6b489f7..441b672a5 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -90,8 +90,9 @@ ro:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "și"
- skip_last_comma: true
+ last_word_connector: " și "
+ two_words_connector: " și "
+ words_connector: ", "
activerecord:
errors:
@@ -1497,3 +1498,34 @@ ro:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 1605e2011..80e442b02 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -218,9 +218,9 @@ ru:
support:
array:
- # Rails 2.2
- sentence_connector: "и"
- skip_last_comma: true
+ last_word_connector: " и "
+ two_words_connector: " и "
+ words_connector: ", "
# Rails 2.3
words_connector: ", "
@@ -1572,3 +1572,34 @@ ru:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 4cf59ed96..8852ae029 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -94,8 +94,9 @@ sk:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "a"
- skip_last_comma: false
+ last_word_connector: " a "
+ two_words_connector: " a "
+ words_connector: ", "
activerecord:
errors:
@@ -1491,3 +1492,34 @@ sk:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index a055c1ca5..e87a57170 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -94,8 +94,9 @@ sl:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "in"
- skip_last_comma: false
+ last_word_connector: " in "
+ two_words_connector: " in "
+ words_connector: ", "
activerecord:
errors:
@@ -1502,3 +1503,34 @@ sl:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index cb41cdcec..14c958e65 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -95,8 +95,9 @@ sq:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "dhe"
- skip_last_comma: false
+ last_word_connector: ", dhe "
+ two_words_connector: " dhe "
+ words_connector: ", "
activerecord:
errors:
@@ -1464,3 +1465,34 @@ sq:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/sr-YU.yml b/config/locales/sr-YU.yml
index 550505dd2..8e31e93db 100644
--- a/config/locales/sr-YU.yml
+++ b/config/locales/sr-YU.yml
@@ -98,8 +98,9 @@ sr-YU:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "i"
- skip_last_comma: false
+ last_word_connector: ", i "
+ two_words_connector: " i "
+ words_connector: ", "
activerecord:
errors:
@@ -296,7 +297,7 @@ sr-YU:
field_delay: Kašnjenje
field_assignable: Problem može biti dodeljen ovoj ulozi
field_redirect_existing_links: Preusmeri postojeće veze
- field_estimated_hours: Procenjeno vreme
+ field_estimated_hours: Procenjeno vreme
field_column_names: Kolone
field_time_zone: Vremenska zona
field_searchable: Može da se pretražuje
@@ -1504,3 +1505,34 @@ sr-YU:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index 42864c917..0c04d39bb 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -96,8 +96,9 @@ sr:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "и"
- skip_last_comma: false
+ last_word_connector: ", и "
+ two_words_connector: " и "
+ words_connector: ", "
activerecord:
errors:
@@ -1503,3 +1504,34 @@ sr:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index b41de9ada..ab63dd62b 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -2,6 +2,7 @@
# by Johan Lundström (johanlunds@gmail.com),
# with parts taken from http://github.com/daniel/swe_rails
# Update based on Redmine 2.6.0 by Khedron Wilk (khedron.wilk@gmail.com) 6th Dec 2014
+# Update based on Redmine 6.0.1 by Jimmy 'Grovkillen' Westberg (jimmy@grovkillen.com) 5th Dec 2024
sv:
number:
# Used in number_with_delimiter()
@@ -135,14 +136,14 @@ sv:
circular_dependency: "Denna relation skulle skapa ett cirkulärt beroende"
cant_link_an_issue_with_a_descendant: "Ett ärende kan inte länkas till ett av dess underärenden"
earlier_than_minimum_start_date: "kan inte vara tidigare än %{date} på grund av föregående ärenden"
- not_a_regexp: "is not a valid regular expression"
- open_issue_with_closed_parent: "An open issue cannot be attached to a closed parent task"
- must_contain_uppercase: "must contain uppercase letters (A-Z)"
- must_contain_lowercase: "must contain lowercase letters (a-z)"
- must_contain_digits: "must contain digits (0-9)"
- must_contain_special_chars: "must contain special characters (!, $, %, ...)"
- domain_not_allowed: "contains a domain not allowed (%{domain})"
- too_simple: "is too simple"
+ not_a_regexp: "är inte korrekt formaterad RegEx"
+ open_issue_with_closed_parent: "Ett öppet ärende kan inte kopplas till ett stängt föräldraärende"
+ must_contain_uppercase: "måste innehålla versaler (A-Z)"
+ must_contain_lowercase: "måste innehålla gemener (a-z)"
+ must_contain_digits: "måste innehålla siffror (0-9)"
+ must_contain_special_chars: "måste innehålla specialtecken (!, $, %, ...)"
+ domain_not_allowed: "består av en ej godkänd domän (%{domain})"
+ too_simple: "är för simpel"
direction: ltr
date:
@@ -216,16 +217,16 @@ sv:
notice_feeds_access_key_reseted: Din Atom-nyckel återställdes.
notice_api_access_key_reseted: Din API-nyckel återställdes.
notice_failed_to_save_issues: "Misslyckades med att spara %{count} ärende(n) på %{total} valda: %{ids}."
- notice_failed_to_save_time_entries: "Misslyckades med att spara %{count} tidloggning(ar) på %{total} valda: %{ids}."
+ notice_failed_to_save_time_entries: "Misslyckades med att spara %{count} tidsstämpling(ar) på %{total} valda: %{ids}."
notice_failed_to_save_members: "Misslyckades med att spara medlem(mar): %{errors}."
notice_account_pending: "Ditt konto skapades och avvaktar nu administratörens godkännande."
notice_default_data_loaded: Standardkonfiguration inläst.
notice_unable_delete_version: Denna version var inte möjlig att ta bort.
- notice_unable_delete_time_entry: Tidloggning kunde inte tas bort.
+ notice_unable_delete_time_entry: Tidsstämplingen kunde inte tas bort.
notice_issue_done_ratios_updated: "% klart uppdaterade."
notice_gantt_chart_truncated: "Schemat förminskades eftersom det överskrider det maximala antalet aktiviteter som kan visas (%{max})"
notice_issue_successful_create: "Ärende %{id} skapades."
- notice_issue_update_conflict: "Detta ärende har uppdaterats av en annan användare samtidigt som du redigerade det."
+ notice_issue_update_conflict: "Detta ärende har uppdaterats av en annan användare under tiden du redigerade det."
notice_account_deleted: "Ditt konto har avslutats permanent."
notice_user_successful_create: "Användare %{id} skapad."
@@ -236,9 +237,9 @@ sv:
error_scm_annotate_big_text_file: Inlägget kan inte annoteras eftersom det överskrider maximal storlek för textfiler.
error_issue_not_found_in_project: 'Ärendet hittades inte eller så tillhör det inte detta projekt'
error_no_tracker_in_project: 'Ingen ärendetyp är associerad med projektet. Vänligen kontrollera projektinställningarna.'
- error_no_default_issue_status: 'Ingen status är definierad som standard för nya ärenden. Vänligen kontrollera din konfiguration (Gå till "Administration -> Ärendestatus").'
- error_can_not_delete_custom_field: Kan inte ta bort användardefinerat fält
- error_can_not_delete_tracker_html: "Det finns ärenden av denna typ och den är därför inte möjlig att ta bort.<p>The following projects have issues with this tracker:<br>%{projects}</p>"
+ error_no_default_issue_status: 'Ingen status är definierad som standard för nya ärenden. Vänligen kontrollera din konfiguration (gå till "Administration -> Ärendestatus").'
+ error_can_not_delete_custom_field: Kan inte ta bort anpassat fält
+ error_can_not_delete_tracker_html: "Det finns ärenden av denna typ och den är därför inte möjlig att ta bort.<p>Följande projekt har ärenden kopplad till denna ärendetyp:<br>%{projects}</p>"
error_can_not_remove_role: "Denna roll används och den är därför inte möjlig att ta bort."
error_can_not_reopen_issue_on_closed_version: 'Ett ärende tilldelat en stängd version kan inte öppnas på nytt'
error_can_not_archive_project: Detta projekt kan inte arkiveras
@@ -266,7 +267,6 @@ sv:
mail_subject_wiki_content_updated: "'%{id}' wikisida har uppdaterats"
mail_body_wiki_content_updated: "The '%{id}' wikisida har uppdaterats av %{author}."
-
field_name: Namn
field_description: Beskrivning
field_summary: Sammanfattning
@@ -277,14 +277,14 @@ sv:
field_filename: Fil
field_filesize: Storlek
field_downloads: Nerladdningar
- field_author: Författare
+ field_author: Skapare
field_created_on: Skapad
field_updated_on: Uppdaterad
field_closed_on: Stängd
field_field_format: Format
field_is_for_all: För alla projekt
field_possible_values: Möjliga värden
- field_regexp: Reguljärt uttryck
+ field_regexp: Reguljärt uttryck (RegEx)
field_min_length: Minimilängd
field_max_length: Maxlängd
field_value: Värde
@@ -299,18 +299,18 @@ sv:
field_tracker: Ärendetyp
field_subject: Ämne
field_due_date: Deadline
- field_assigned_to: Tilldelad till
+ field_assigned_to: Tilldelad
field_priority: Prioritet
field_fixed_version: Versionsmål
field_user: Användare
- field_principal: User or Group
+ field_principal: Användare eller grupp
field_role: Roll
field_homepage: Hemsida
field_is_public: Publik
field_parent: Underprojekt till
field_is_in_roadmap: Visa ärenden i roadmap
field_login: Användarnamn
- field_mail_notification: Mailnotifieringar
+ field_mail_notification: E-postaviseringar
field_admin: Administratör
field_last_login_on: Senaste inloggning
field_language: Språk
@@ -333,7 +333,7 @@ sv:
field_done_ratio: "% Klart"
field_auth_source: Autentiseringsläge
field_hide_mail: Dölj min mailadress
- field_comments: Kommentar
+ field_comments: Kommentarer
field_url: URL
field_start_page: Startsida
field_subproject: Underprojekt
@@ -346,7 +346,7 @@ sv:
field_delay: Fördröjning
field_assignable: Ärenden kan tilldelas denna roll
field_redirect_existing_links: Omdirigera existerande länkar
- field_estimated_hours: Estimerad tid
+ field_estimated_hours: Beräknad tid
field_column_names: Kolumner
field_time_entries: Spenderad tid
field_time_zone: Tidszon
@@ -398,7 +398,7 @@ sv:
setting_feeds_limit: Innehållsgräns för Feed
setting_default_projects_public: Nya projekt är publika
setting_autofetch_changesets: Automatisk hämtning av commits
- setting_sys_api_enabled: Aktivera WS för versionsarkivhantering
+ setting_sys_api_enabled: Aktivera API för versionsarkivhantering
setting_commit_ref_keywords: Referens-nyckelord
setting_commit_fix_keywords: Fix-nyckelord
setting_autologin: Automatisk inloggning
@@ -408,7 +408,7 @@ sv:
setting_cross_project_issue_relations: Tillåt ärenderelationer mellan projekt
setting_issue_list_default_columns: Standardkolumner i ärendelistan
setting_repositories_encodings: Encoding för bilagor och versionsarkiv
- setting_emails_header: Mail-header
+ setting_emails_header: Inledning
setting_emails_footer: Signatur
setting_protocol: Protokoll
setting_per_page_options: Alternativ, objekt per sida
@@ -417,7 +417,7 @@ sv:
setting_display_subprojects_issues: Visa ärenden från underprojekt i huvudprojekt
setting_enabled_scm: Aktivera SCM
setting_mail_handler_body_delimiters: "Trunkera mail efter en av följande rader"
- setting_mail_handler_api_enabled: Aktivera WS för inkommande mail
+ setting_mail_handler_api_enabled: Aktivera API för inkommande mail
setting_mail_handler_api_key: API-nyckel
setting_sequential_project_identifiers: Generera projektidentifierare sekventiellt
setting_gravatar_enabled: Använd Gravatar-avatarer
@@ -428,21 +428,21 @@ sv:
setting_password_min_length: Minsta tillåtna lösenordslängd
setting_new_project_user_role_id: Tilldelad roll för en icke-administratör som skapar ett projekt
setting_default_projects_modules: Aktiverade moduler för nya projekt
- setting_issue_done_ratio: Beräkna % klart med
+ setting_issue_done_ratio: Beräkna "% Klart" med
setting_issue_done_ratio_issue_field: Använd ärendefältet
setting_issue_done_ratio_issue_status: Använd ärendestatus
setting_start_of_week: Första dagen i veckan
- setting_rest_api_enabled: Aktivera REST webbtjänst
- setting_cache_formatted_text: Cacha formaterad text
- setting_default_notification_option: Standard notifieringsalternativ
- setting_commit_logtime_enabled: Aktivera tidloggning
- setting_commit_logtime_activity_id: Aktivitet för loggad tid
- setting_gantt_items_limit: Maximalt antal aktiviteter som visas i gantt-schemat
- setting_issue_group_assignment: Tillåt att ärenden tilldelas till grupper
+ setting_rest_api_enabled: Aktivera REST-API
+ setting_cache_formatted_text: Förladda formaterad text
+ setting_default_notification_option: Standardaviseringsalternativ
+ setting_commit_logtime_enabled: Aktivera tidsstämpling
+ setting_commit_logtime_activity_id: Aktivitet för stämplad tid
+ setting_gantt_items_limit: Maximalt antal aktiviteter som visas i Gantt-schemat
+ setting_issue_group_assignment: Tillåt att ärenden tilldelas grupper
setting_default_issue_start_date_to_creation_date: Använd dagens datum som startdatum för nya ärenden
- setting_commit_cross_project_ref: Tillåt ärende i alla de andra projekten att bli refererade och fixade
+ setting_commit_cross_project_ref: Tillåt ärenden i alla projekt att refereras och åtgärdas
setting_unsubscribe: Tillåt användare att avsluta prenumereration
- setting_session_lifetime: Maximal sessionslivslängd
+ setting_session_lifetime: Maximal sessionslängd
setting_session_timeout: Tidsgräns för sessionsinaktivitet
setting_thumbnails_enabled: Visa miniatyrbilder av bilagor
setting_thumbnails_size: Storlek på miniatyrbilder (i pixlar)
@@ -471,17 +471,17 @@ sv:
permission_view_private_notes: Visa privata anteckningar
permission_set_notes_private: Ställa in anteckningar som privata
permission_delete_issues: Ta bort ärenden
- permission_manage_public_queries: Hantera publika frågor
- permission_save_queries: Spara frågor
+ permission_manage_public_queries: Hantera publika filtreringar
+ permission_save_queries: Spara filtreringar
permission_view_gantt: Visa Gantt-schema
permission_view_calendar: Visa kalender
permission_view_issue_watchers: Visa bevakarlista
permission_add_issue_watchers: Lägga till bevakare
permission_delete_issue_watchers: Ta bort bevakare
- permission_log_time: Logga spenderad tid
+ permission_log_time: Stämpla spenderad tid
permission_view_time_entries: Visa spenderad tid
- permission_edit_time_entries: Ändra tidloggningar
- permission_edit_own_time_entries: Ändra egna tidloggningar
+ permission_edit_time_entries: Ändra tidsstämplingar
+ permission_edit_own_time_entries: Ändra egna tidsstämplingar
permission_manage_news: Hantera nyheter
permission_comment_news: Kommentera nyheter
permission_view_documents: Visa dokument
@@ -500,7 +500,7 @@ sv:
permission_protect_wiki_pages: Skydda wikisidor
permission_manage_repository: Hantera versionsarkiv
permission_browse_repository: Bläddra i versionsarkiv
- permission_view_changesets: Visa changesets
+ permission_view_changesets: Visa kodändringar
permission_commit_access: Commit-åtkomst
permission_manage_boards: Hantera forum
permission_view_messages: Visa meddelanden
@@ -570,10 +570,10 @@ sv:
label_issue_category: Ärendekategori
label_issue_category_plural: Ärendekategorier
label_issue_category_new: Ny kategori
- label_custom_field: Användardefinerat fält
- label_custom_field_plural: Användardefinerade fält
- label_custom_field_new: Nytt användardefinerat fält
- label_enumerations: Uppräkningar
+ label_custom_field: Anpassat fält
+ label_custom_field_plural: Anpassade fält
+ label_custom_field_new: Nytt anpassat fält
+ label_enumerations: Värdelistor
label_enumeration_new: Nytt värde
label_information: Information
label_information_plural: Information
@@ -587,8 +587,8 @@ sv:
label_login: Logga in
label_logout: Logga ut
label_help: Hjälp
- label_reported_issues: Rapporterade ärenden
- label_assigned_to_me_issues: Ärenden tilldelade till mig
+ label_reported_issues: Skapade ärenden
+ label_assigned_to_me_issues: Ärenden tilldelade mig
label_registered_on: Registrerad
label_activity: Aktivitet
label_user_activity: "Aktiviteter för %{value}"
@@ -667,7 +667,7 @@ sv:
label_next: Nästa
label_previous: Föregående
label_used_by: Använd av
- label_details: Detaljer
+ label_details: Sammanfattning
label_add_note: Lägg till anteckning
label_calendar: Kalender
label_months_from: månader från
@@ -684,12 +684,12 @@ sv:
label_comment_add: Lägg till kommentar
label_comment_added: Kommentar tillagd
label_comment_delete: Ta bort kommentar
- label_query: Användardefinerad fråga
- label_query_plural: Användardefinerade frågor
- label_query_new: Ny fråga
- label_my_queries: Mina egna frågor
- label_filter_add: Lägg till filter
- label_filter_plural: Filter
+ label_query: Anpassad filtrering
+ label_query_plural: Anpassade filtreringar
+ label_query_new: Ny filtrering
+ label_my_queries: Mina egna filtreringar
+ label_filter_add: Lägg till filtreringsrad
+ label_filter_plural: Filtrering
label_equals: är
label_not_equals: är inte
label_in_less_than: om mindre än
@@ -738,7 +738,7 @@ sv:
label_view_revisions: Visa revisioner
label_view_all_revisions: Visa alla revisioner
label_max_size: Maxstorlek
- label_roadmap: Vägkarta
+ label_roadmap: Utvecklingsplan
label_roadmap_due_in: "Färdig om %{value}"
label_roadmap_overdue: "%{value} sen"
label_roadmap_no_issues: Inga ärenden för denna version
@@ -764,7 +764,7 @@ sv:
label_change_plural: Ändringar
label_statistics: Statistik
label_commits_per_month: Commits per månad
- label_commits_per_author: Commits per författare
+ label_commits_per_author: Commits per användare
label_diff: skillnader
label_view_diff: Visa skillnader
label_diff_inline: i texten
@@ -795,7 +795,7 @@ sv:
label_board_new: Nytt forum
label_board_plural: Forum
label_board_locked: Låst
- label_board_sticky: Klibbig
+ label_board_sticky: Fäst
label_topic_plural: Ämnen
label_message_plural: Meddelanden
label_message_last: Senaste meddelande
@@ -810,7 +810,7 @@ sv:
label_date_to: Till
label_language_based: Språkbaserad
label_sort_by: "Sortera på %{value}"
- label_send_test_email: Skicka testmail
+ label_send_test_email: Skicka ett testmejl
label_feeds_access_key: Atom-nyckel
label_missing_feeds_access_key: Saknar en Atom-nyckel
label_feeds_access_key_created_on: "Atom-nyckel skapad för %{value} sedan"
@@ -832,8 +832,8 @@ sv:
label_user_mail_option_selected: "För alla händelser i markerade projekt..."
label_user_mail_option_none: "Inga händelser"
label_user_mail_option_only_my_events: "Endast för saker jag bevakar eller är inblandad i"
- label_user_mail_no_self_notified: "Jag vill inte bli underrättad om ändringar som jag har gjort"
- label_registration_activation_by_email: kontoaktivering med mail
+ label_user_mail_no_self_notified: "Jag vill inte bli underrättad om ändringar som jag själv gjort"
+ label_registration_activation_by_email: kontoaktivering med e-post
label_registration_manual_activation: manuell kontoaktivering
label_registration_automatic_activation: automatisk kontoaktivering
label_display_per_page: "Per sida: %{value}"
@@ -843,13 +843,13 @@ sv:
label_scm: SCM
label_plugins: Tillägg
label_ldap_authentication: LDAP-autentisering
- label_downloads_abbr: Nerl.
+ label_downloads_abbr: Nedl.
label_optional_description: Valfri beskrivning
label_add_another_file: Lägg till ytterligare en fil
label_preferences: Användarinställningar
label_chronological_order: I kronologisk ordning
label_reverse_chronological_order: I omvänd kronologisk ordning
- label_incoming_emails: Inkommande mail
+ label_incoming_emails: Inkommande e-post
label_generate_key: Generera en nyckel
label_issue_watchers: Bevakare
label_example: Exempel
@@ -879,14 +879,14 @@ sv:
label_api_access_key_created_on: "API-nyckel skapad för %{value} sedan"
label_profile: Profil
label_subtask_plural: Underaktiviteter
- label_project_copy_notifications: Skicka mailnotifieringar när projektet kopieras
+ label_project_copy_notifications: Skicka e-postaviseringar när projektet kopieras
label_principal_search: "Sök efter användare eller grupp:"
label_user_search: "Sök efter användare:"
label_additional_workflow_transitions_for_author: Ytterligare övergångar tillåtna när användaren är den som skapat ärendet
label_additional_workflow_transitions_for_assignee: Ytterligare övergångar tillåtna när användaren är den som tilldelats ärendet
label_issues_visibility_all: Alla ärenden
label_issues_visibility_public: Alla icke-privata ärenden
- label_issues_visibility_own: Ärenden skapade av eller tilldelade till användaren
+ label_issues_visibility_own: Ärenden skapade av eller tilldelade användaren
label_git_report_last_commit: Rapportera senaste commit av filer och mappar
label_parent_revision: Förälder
label_child_revision: Barn
@@ -903,7 +903,7 @@ sv:
label_required: Nödvändig
label_attribute_of_project: Projektets %{name}
label_attribute_of_issue: Ärendets %{name}
- label_attribute_of_author: Författarens %{name}
+ label_attribute_of_author: Skaparens %{name}
label_attribute_of_assigned_to: Tilldelad användares %{name}
label_attribute_of_user: Användarens %{name}
label_attribute_of_fixed_version: Målversionens %{name}
@@ -922,7 +922,7 @@ sv:
button_expand_all: Expandera alla
button_delete: Ta bort
button_create: Skapa
- button_create_and_continue: Skapa och fortsätt
+ button_create_and_continue: Skapa och ny
button_test: Testa
button_edit: Ändra
button_edit_associated_wikipage: "Ändra associerad Wikisida: %{page_title}"
@@ -936,15 +936,15 @@ sv:
button_list: Lista
button_view: Visa
button_move: Flytta
- button_move_and_follow: Flytta och följ efter
+ button_move_and_follow: Flytta och följ
button_back: Tillbaka
button_cancel: Avbryt
button_activate: Aktivera
button_sort: Sortera
- button_log_time: Logga tid
+ button_log_time: Stämpla tid
button_rollback: Återställ till denna version
button_watch: Bevaka
- button_unwatch: Stoppa bevakning
+ button_unwatch: Ta bort bevakning
button_reply: Svara
button_archive: Arkivera
button_unarchive: Ta bort från arkiv
@@ -952,7 +952,7 @@ sv:
button_rename: Byt namn
button_change_password: Ändra lösenord
button_copy: Kopiera
- button_copy_and_follow: Kopiera och följ efter
+ button_copy_and_follow: Kopiera och följ
button_annotate: Kommentera
button_update: Uppdatera
button_configure: Konfigurera
@@ -979,12 +979,12 @@ sv:
field_active: Aktiv
- text_select_mail_notifications: Välj för vilka händelser mail ska skickas.
- text_regexp_info: eg. ^[A-Z0-9]+$
+ text_select_mail_notifications: Välj för vilka händelser e-post ska skickas.
+ text_regexp_info: t.ex. ^[A-Z0-9]+$
text_project_destroy_confirmation: Är du säker på att du vill ta bort detta projekt och all relaterad data?
text_subprojects_destroy_warning: "Alla underprojekt: %{value} kommer också tas bort."
text_workflow_edit: Välj en roll och en ärendetyp för att ändra arbetsflöde
- text_are_you_sure: Är du säker ?
+ text_are_you_sure: Är du säker?
text_journal_changed: "%{label} ändrad från %{old} till %{new}"
text_journal_changed_no_detail: "%{label} uppdaterad"
text_journal_set_to: "%{label} satt till %{value}"
@@ -1004,8 +1004,8 @@ sv:
text_issues_ref_in_commit_messages: Referera och fixa ärenden i commit-meddelanden
text_issue_added: "Ärende %{id} har rapporterats (av %{author})."
text_issue_updated: "Ärende %{id} har uppdaterats (av %{author})."
- text_wiki_destroy_confirmation: Är du säker på att du vill ta bort denna wiki och allt dess innehåll ?
- text_issue_category_destroy_question: "Några ärenden (%{count}) är tilldelade till denna kategori. Vad vill du göra ?"
+ text_wiki_destroy_confirmation: Är du säker på att du vill ta bort denna wiki och allt dess innehåll?
+ text_issue_category_destroy_question: "Några ärenden (%{count}) är tilldelade denna kategori. Vad vill du göra?"
text_issue_category_destroy_assignments: Ta bort kategoritilldelningar
text_issue_category_reassign_to: Återtilldela ärenden till denna kategori
text_user_mail_option: "För omarkerade projekt kommer du bara bli underrättad om saker du bevakar eller är inblandad i (T.ex. ärenden du skapat eller tilldelats)."
@@ -1013,25 +1013,25 @@ sv:
text_load_default_configuration: Läs in standardkonfiguration
text_status_changed_by_changeset: "Tilldelad i changeset %{value}."
text_time_logged_by_changeset: "Tilldelad i changeset %{value}."
- text_issues_destroy_confirmation: 'Är du säker på att du vill radera markerade ärende(n) ?'
+ text_issues_destroy_confirmation: 'Är du säker på att du vill radera markerade ärende(n)?'
text_issues_destroy_descendants_confirmation: Detta kommer även ta bort %{count} underaktivitet(er).
text_time_entries_destroy_confirmation: Är du säker på att du vill ta bort valda tidloggningar?
text_select_project_modules: 'Välj vilka moduler som ska vara aktiva för projektet:'
text_default_administrator_account_changed: Standardadministratörens konto ändrat
text_file_repository_writable: Arkivet för bifogade filer är skrivbart
- text_plugin_assets_writable: Arkivet för plug-ins är skrivbart
+ text_plugin_assets_writable: Arkivet för tillägg är skrivbart
text_minimagick_available: MiniMagick tillgängligt (ej obligatoriskt)
- text_destroy_time_entries_question: "%{hours} timmar har rapporterats på ärendena du är på väg att ta bort. Vad vill du göra ?"
+ text_destroy_time_entries_question: "%{hours} timmar har rapporterats på ärendena du är på väg att ta bort. Vad vill du göra?"
text_destroy_time_entries: Ta bort rapporterade timmar
text_assign_time_entries_to_project: Tilldela rapporterade timmar till projektet
text_reassign_time_entries: 'Återtilldela rapporterade timmar till detta ärende:'
text_user_wrote: "%{value} skrev:"
text_user_wrote_in: "%{value} skrev (%{link}):"
- text_enumeration_destroy_question: "%{count} objekt är tilldelade till detta värde."
+ text_enumeration_destroy_question: "%{count} objekt är tilldelade detta värde."
text_enumeration_category_reassign_to: 'Återtilldela till detta värde:'
- text_email_delivery_not_configured: "Mailfunktionen har inte konfigurerats, och notifieringar via mail kan därför inte skickas.\nKonfigurera din SMTP-server i config/configuration.yml och starta om applikationen för att aktivera dem."
- text_repository_usernames_mapping: "Välj eller uppdatera den Redmine-användare som är mappad till varje användarnamn i versionarkivloggen.\nAnvändare med samma användarnamn eller mailadress i både Redmine och versionsarkivet mappas automatiskt."
- text_diff_truncated: '... Denna diff har förminskats eftersom den överskrider den maximala storlek som kan visas.'
+ text_email_delivery_not_configured: "E-postfunktionen har inte konfigurerats, och aviseringar via e-post kan därför inte skickas.\nKonfigurera din SMTP-server i <strong>config/configuration.yml</strong> och starta om applikationen för att aktivera dem."
+ text_repository_usernames_mapping: "Välj eller uppdatera den Redmine-användare som är mappad till varje användarnamn i versionarkivloggen.\nAnvändare med samma användarnamn eller e-postadress i både Redmine och versionsarkivet mappas automatiskt."
+ text_diff_truncated: '... Denna diff har kortats ner eftersom den överskrider den maximala storlek som kan visas.'
text_custom_field_possible_values_info: 'Ett värde per rad'
text_wiki_page_destroy_question: "Denna sida har %{descendants} underliggande sidor. Vad vill du göra?"
text_wiki_page_nullify_children: "Behåll undersidor som rotsidor"
@@ -1046,13 +1046,13 @@ sv:
text_mercurial_repository_note: Lokalt versionsarkiv (t.ex. /hgrepo, c:\hgrepo)
text_scm_command: Kommando
text_scm_command_version: Version
- text_scm_config: Du kan konfigurera dina scm-kommando i config/configuration.yml. Vänligen starta om applikationen när ändringar gjorts.
+ text_scm_config: Du kan konfigurera dina scm-kommando i <strong>config/configuration.yml</strong>. Vänligen starta om applikationen när ändringar gjorts.
text_scm_command_not_available: Scm-kommando är inte tillgängligt. Vänligen kontrollera inställningarna i administratörspanelen.
text_issue_conflict_resolution_overwrite: "Använd mina ändringar i alla fall (tidigare anteckningar kommer behållas men några ändringar kan bli överskrivna)"
text_issue_conflict_resolution_add_notes: "Lägg till mina anteckningar och kasta mina andra ändringar"
text_issue_conflict_resolution_cancel: "Kasta alla mina ändringar och visa igen %{link}"
text_account_destroy_confirmation: "Är du säker på att du vill fortsätta?\nDitt konto kommer tas bort permanent, utan möjlighet att återaktivera det."
- text_session_expiration_settings: "Varning: ändring av dessa inställningar kan få alla nuvarande sessioner, inklusive din egen, att gå ut."
+ text_session_expiration_settings: "Varning: ändring av dessa inställningar kan få alla nuvarande sessioner, inklusive din egen, att avslutas."
text_project_closed: Detta projekt är stängt och skrivskyddat.
text_turning_multiple_off: "Om du inaktiverar möjligheten till flera värden kommer endast ett värde per objekt behållas."
@@ -1090,14 +1090,14 @@ sv:
description_message_content: Meddelandeinnehåll
description_query_sort_criteria_attribute: Sorteringsattribut
description_query_sort_criteria_direction: Sorteringsriktning
- description_user_mail_notification: Mailnotifieringsinställningar
- description_available_columns: Tillgängliga Kolumner
- description_selected_columns: Valda Kolumner
+ description_user_mail_notification: Inställningar för e-postaviseringar
+ description_available_columns: Tillgängliga kolumner
+ description_selected_columns: Valda kolumner
description_all_columns: Alla kolumner
description_issue_category_reassign: Välj ärendekategori
description_wiki_subpages_reassign: Välj ny föräldersida
text_repository_identifier_info: 'Endast gemener (a-z), siffror, streck och understreck är tillåtna.<br />När identifieraren sparats kan den inte ändras.'
- notice_account_not_activated_yet: Du har inte aktiverat ditt konto än. Om du vill få ett nytt aktiveringsbrev, <a href="%{url}"> klicka på denna länk </a>.
+ notice_account_not_activated_yet: Du har inte aktiverat ditt konto än. Om du vill få ett nytt aktiveringsbrev, <a href="%{url}">klicka på denna länk</a>.
notice_account_locked: Ditt konto är låst.
label_hidden: Dold
label_visibility_private: endast för mig
@@ -1109,21 +1109,21 @@ sv:
text_convert_available: ImageMagick-konvertering tillgänglig (valbart)
label_link: Länk
label_only: endast
- label_drop_down_list: droppmeny
+ label_drop_down_list: rullgardinsmeny
label_checkboxes: kryssrutor
label_link_values_to: Länka värden till URL
setting_force_default_language_for_anonymous: Lås till förvalt språk för anonyma användare
setting_force_default_language_for_loggedin: Lås till förvalt språk för inloggade användare
label_custom_field_select_type: Välj den typ av objekt som det anpassade fältet skall användas för
- label_issue_assigned_to_updated: Tilldelad har uppdaterats
+ label_issue_assigned_to_updated: Tilldelning har uppdaterats
label_check_for_updates: Leta efter uppdateringar
label_latest_compatible_version: Senaste kompatibla version
label_unknown_plugin: Okänt tillägg
label_radio_buttons: alternativknappar
label_group_anonymous: Anonyma användare
label_group_non_member: Icke-medlemsanvändare
- label_add_projects: Addera projekt
- field_default_status: Default-status
+ label_add_projects: Lägg till projekt
+ field_default_status: Standardstatus
text_subversion_repository_note: 'Exempel: file:///, http://, https://, svn://, svn+[tunnelscheme]://'
field_users_visibility: Användares synlighet
label_users_visibility_all: Alla aktiva användare
@@ -1137,17 +1137,16 @@ sv:
label_search_attachments_only: Sök endast bilagor
label_search_open_issues_only: Endast öppna ärenden
field_address: Epost
- setting_max_additional_emails: Max antal ytterligare epost-adresser
- label_email_address_plural: Epost
- label_email_address_add: Addera epostadress
- label_enable_notifications: Aktivera underrättelser
- label_disable_notifications: Avaktivera underrättelser
+ setting_max_additional_emails: Maximalt antal extra e-postadresser
+ label_email_address_plural: E-post
+ label_email_address_add: Addera e-postadress
+ label_enable_notifications: Aktivera aviseringar
+ label_disable_notifications: Avaktivera aviseringar
setting_search_results_per_page: Sökresultat per sida
label_blank_value: blank
permission_copy_issues: Kopiera ärenden
- error_password_expired: Ditt lösenord har gått ut eller administratören kräver att
- du ändrar det.
- field_time_entries_visibility: Synlighet för tidsloggar
+ error_password_expired: Ditt lösenord har gått ut eller administratören kräver att du ändrar det.
+ field_time_entries_visibility: Synlighet för tidsstämplingar
setting_password_max_age: Kräv lösenordsändring efter
label_parent_task_attributes: Attribut för föräldraaktiviteter
label_parent_task_attributes_derived: Kalkylerad från underaktiviteter
@@ -1162,8 +1161,7 @@ sv:
notice_import_finished: "%{count} artiklar har importerats"
notice_import_finished_with_errors: "%{count} av %{total} artiklar kunde inte importeras"
error_invalid_file_encoding: Filen är inte en %{encoding}-kodad fil
- error_invalid_csv_file_or_settings: Filen är inte en CSV-fil eller stämmer inte med
- inställningarna nedan (%{value})
+ error_invalid_csv_file_or_settings: Filen är inte en CSV-fil eller stämmer inte med inställningarna nedan (%{value})
error_can_not_read_import_file: Fel vid läsning av fil att importera
permission_import_issues: Importera ärenden
label_import_issues: Importera ärenden
@@ -1172,26 +1170,26 @@ sv:
label_fields_wrapper: Fältomslag
label_encoding: Kodning
label_comma_char: Komma
- label_semi_colon_char: Semicolon
- label_quote_char: Citationstecken
- label_double_quote_char: Dubbla citationstecken
- label_fields_mapping: Kartläggning av fält
+ label_semi_colon_char: Semikolon
+ label_quote_char: Citattecken
+ label_double_quote_char: Dubbla citattecken
+ label_fields_mapping: Mappning av fält
label_file_content_preview: Förhandsvisning av filinnehåll
label_create_missing_values: Skapa saknade värden
button_import: Importera
- field_total_estimated_hours: Totalt uppskattad tid
+ field_total_estimated_hours: Totalt beräknad tid
label_api: API
label_total_plural: Totaler
label_assigned_issues: Tilldelade ärenden
label_field_format_enumeration: Nyckel/värde-lista
label_f_hour_short: '%{value} h'
- field_default_version: Defaultversion
- error_attachment_extension_not_allowed: Otillåten utökning av bilaga %{extension}
- setting_attachment_extensions_allowed: Tillåtna utökningar
- setting_attachment_extensions_denied: Otillåtna utökningar
+ field_default_version: Standardversion
+ error_attachment_extension_not_allowed: Otillåten filtyp av bilaga %{extension}
+ setting_attachment_extensions_allowed: Tillåtna filtyper
+ setting_attachment_extensions_denied: Otillåtna filtyper
label_any_open_issues: några öppna ärenden
label_no_open_issues: inga öppna ärenden
- label_default_values_for_new_users: Defaultvärden för nya användare
+ label_default_values_for_new_users: Standardvärden för nya användare
error_ldap_bind_credentials: Ogiltigt LDAP konto/lösenord
setting_sys_api_key: API-nyckel
setting_lost_password: Glömt lösenord
@@ -1200,25 +1198,21 @@ sv:
mail_body_security_notification_change_to: ! '%{field} ändrades till %{value}.'
mail_body_security_notification_add: ! '%{field} %{value} las till.'
mail_body_security_notification_remove: ! '%{field} %{value} togs bort.'
- mail_body_security_notification_notify_enabled: Epostadress %{value} kommer att få
- underrättelser.
- mail_body_security_notification_notify_disabled: Epostadress %{value} får inga fler
- underrättelser.
+ mail_body_security_notification_notify_enabled: E-postadress %{value} kommer att få aviseringar.
+ mail_body_security_notification_notify_disabled: E-postadress %{value} får inga fler aviseringar.
mail_body_settings_updated: ! 'Följande inställningar ändrades:'
field_remote_ip: IP-adress
label_wiki_page_new: Ny wiki-sida
label_relations: Relationer
- button_filter: Filter
+ button_filter: Filtrering
mail_body_password_updated: Ditt lösenord är ändrat.
label_no_preview: Ingen förhandsvisning tillgänglig
- error_no_tracker_allowed_for_new_issue_in_project: Projektet har ingen spårare
- som du kan skapa ett ärende för
- label_tracker_all: All spårare
+ error_no_tracker_allowed_for_new_issue_in_project: Projektet har ingen ärendetyp som du kan skapa ett ärende för
+ label_tracker_all: All ärendetyper
label_new_project_issue_tab_enabled: Visa fliken "Nya ärenden"
setting_new_item_menu_tab: Projektmenyflik för att skapa nya objekt
- label_new_object_tab_enabled: Visa "+" droppmeny
- error_no_projects_with_tracker_allowed_for_new_issue: Inga projekt med spårare
- tillåtna för nya ärenden
+ label_new_object_tab_enabled: Visa "+" rullgardinsmenyn
+ error_no_projects_with_tracker_allowed_for_new_issue: Inga projekt med ärendetyper tillåtna för nya ärenden
field_textarea_font: Font som används för textytor
label_font_default: Default font
label_font_monospace: Monospaced font
@@ -1226,94 +1220,81 @@ sv:
setting_timespan_format: Format för tidsintervall
label_table_of_contents: Innehållsförteckning
setting_commit_logs_formatting: Använd textformatering för meddelanden
- setting_mail_handler_enable_regex: Aktivera reguljära uttryck
+ setting_mail_handler_enable_regex: Aktivera reguljära uttryck (RegEx)
error_move_of_child_not_possible: 'Underaktivitet %{child} kunde inte flyttas till det nya
projektet: %{errors}'
- error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: Spenderad tid kan inte
- flyttas till ett ärende som är på väg att tas bort
- setting_timelog_required_fields: Nödvändiga fält för tidsangivelser
- label_attribute_of_object: '%{object_name}''s %{name}'
+ error_cannot_reassign_time_entries_to_an_issue_about_to_be_deleted: Spenderad tid kan inte flyttas till ett ärende som är på väg att tas bort
+ setting_timelog_required_fields: Nödvändiga fält för tidsstämplingar
+ label_attribute_of_object: '%{object_name}s %{name}'
label_user_mail_option_only_assigned: Bara för saker som jag följer eller är tilldelad
label_user_mail_option_only_owner: Bara för saker som jag följer eller äger
- warning_fields_cleared_on_bulk_edit: Ändringar kommer att orsaka automatiskt borttagande
- av värden från ett eller flera fält för de valda objekten
+ warning_fields_cleared_on_bulk_edit: Ändringar kommer att orsaka automatiskt borttagande av värden från ett eller flera fält för de valda objekten
field_updated_by: Uppdaterad av
field_last_updated_by: Senast uppdaterad av
field_full_width_layout: Layout med full bredd
label_last_notes: Senaste noteringar
field_digest: Checksumma
- field_default_assigned_to: Defaultmottagare
+ field_default_assigned_to: Standardmottagare
setting_show_custom_fields_on_registration: Visa anpassade fält vid registrering
permission_view_news: Visa nyheter
label_no_preview_alternative_html: Ingen förhandsvisning tillgänglig. %{link} filen istället.
- label_no_preview_download: Nerladdning
+ label_no_preview_download: Nedladdning
setting_close_duplicate_issues: Stäng duplicerade ärenden automatiskt
- error_exceeds_maximum_hours_per_day: Kan inte logga mer än %{max_hours} timmar
- samma dag (%{logged_hours} timmar har redan loggats)
- setting_time_entry_list_defaults: Defaults för tidsloggning
- setting_timelog_accept_0_hours: Acceptera tidsloggar med 0 timmar
- setting_timelog_max_hours_per_day: Max timmar som kan loggas per dag och användare
+ error_exceeds_maximum_hours_per_day: Kan inte stämpla mer än %{max_hours} timmar samma dag (%{logged_hours} timmar har redan stämplats)
+ setting_time_entry_list_defaults: Standard för tidsstämplingar
+ setting_timelog_accept_0_hours: Acceptera tidsstämplingar på 0 timmar
+ setting_timelog_max_hours_per_day: Max timmar som kan stämplas per dag och användare
label_x_revisions: "%{count} revisioner"
- error_can_not_delete_auth_source: Denna autenticeringsmetod är i bruk och kan inte
- tas bort.
+ error_can_not_delete_auth_source: Denna autentiseringsmetod är i bruk och kan inte tas bort.
button_actions: Handlingar
- mail_body_lost_password_validity: OBS att du endast kan ändra lösenordet en gång
- via denna länk.
- text_login_required_html: När autenticering inte krävs, är publika projekt och deras
- innehåll tillgängliga på nätverket. Du kan <a href="%{anonymous_role_path}"> ändra
- tillämpliga behörigheter</a>.
+ mail_body_lost_password_validity: Observera att du endast kan ändra lösenordet en gång via denna länk.
+ text_login_required_html: 'När autentisering inte krävs, är publika projekt och deras innehåll tillgängliga på nätverket. Du kan <a href="%{anonymous_role_path}"> ändra tillämpliga behörigheter</a>.'
label_login_required_yes: 'Ja'
label_login_required_no: Nej, tillåt anonym access till publika projekt
- text_project_is_public_non_member: Publika projekt och deras innehåll är tillgängliga
- för alla inloggade användare.
- text_project_is_public_anonymous: Publika projekt och deras innehåll är tillgängliga
- på nätverket.
+ text_project_is_public_non_member: Publika projekt och deras innehåll är tillgängliga för alla inloggade användare.
+ text_project_is_public_anonymous: Publika projekt och deras innehåll är tillgängliga på nätverket.
label_version_and_files: Versioner (%{count}) och filer
label_ldap: LDAP
label_ldaps_verify_none: LDAPS (utan certifikatkontroll)
label_ldaps_verify_peer: LDAPS
- label_ldaps_warning: Det rekommenderas att använda krypterad LDAPS-förbindelse med
- certifikatkontroll för att hindra manipulation av autenticeringsprocessen.
+ label_ldaps_warning: Det rekommenderas att använda krypterad LDAPS-koppling med certifikatkontroll för att hindra manipulation av autentiseringsprocessen.
label_nothing_to_preview: Ingenting att visa
error_token_expired: Lösenordslänken gäller inte längre, var god försök igen.
- error_spent_on_future_date: Kan inte logga tid på framtida datum
- setting_timelog_accept_future_dates: Acceptera tidsloggningar på framtida datum
+ error_spent_on_future_date: Kan inte stämpla tid på framtida datum
+ setting_timelog_accept_future_dates: Acceptera tidsstämplingar på framtida datum
label_delete_link_to_subtask: Ta bort relation
- error_not_allowed_to_log_time_for_other_users: Du får inte logga tid
- för andra användare
- permission_log_time_for_other_users: Logga spenderad tid för andra användare
+ error_not_allowed_to_log_time_for_other_users: Du får inte stämpla tid för andra användare
+ permission_log_time_for_other_users: Stämpla spenderad tid för andra användare
label_tomorrow: imorgon
label_next_week: nästa vecka
label_next_month: nästa månad
text_role_no_workflow: Inget arbetsflöde definierat för denna roll
- text_status_no_workflow: Ingen spårare använder denna status i arbetsflödet
- setting_mail_handler_preferred_body_part: Föredragen del av epost i flera delar(HTML)
- setting_show_status_changes_in_mail_subject: Visa statusändringar i ärendemailunderrättelsers
- ämne
+ text_status_no_workflow: Ingen arbetstyp använder denna status i arbetsflödet
+ setting_mail_handler_preferred_body_part: Föredragen del av epost i flera delar (HTML)
+ setting_show_status_changes_in_mail_subject: Visa statusändringar för ärenden i ämnet för e-postaviseringar
label_inherited_from_parent_project: Ärvd från föräldraprojekt
label_inherited_from_group: Ärvd från grupp%{name}
- label_trackers_description: Beskrivning för spårare
- label_open_trackers_description: Visa beskrivning för alla spårare
+ label_trackers_description: Beskrivning för ärendetyp
+ label_open_trackers_description: Visa beskrivning för alla ärendetyper
label_preferred_body_part_text: Text
label_preferred_body_part_html: HTML
field_parent_issue_subject: Ämne för föräldraärende
permission_edit_own_issues: Ändra egna ärenden
- text_select_apply_tracker: Välj spårare
- label_updated_issues: Updaterade ärenden
- text_avatar_server_config_html: Nuvarande avatar-server <a href="%{url}">%{url}</a>.
- Du kan konfigurera det i config/configuration.yml.
- setting_gantt_months_limit: Max antal månader på gantt-schemat
- permission_import_time_entries: Importera tidsangivelser
- label_import_notifications: Skicka underrättelser med epost under importen
- text_gs_available: ImageMagick PDF support tillgänglig (frivillig)
+ text_select_apply_tracker: Välj ärendetyp
+ label_updated_issues: Uppdaterade ärenden
+ text_avatar_server_config_html: 'Nuvarande avatar-server <a href="%{url}">%{url}</a>. Du kan konfigurera det i <strong>config/configuration.yml</strong>.'
+ setting_gantt_months_limit: Max antal månader på Gantt-schemat
+ permission_import_time_entries: Importera tidsstämplingar
+ label_import_notifications: Skicka aviseringar med e-post under importen
+ text_gs_available: ImageMagick PDF-support tillgänglig (frivillig)
field_recently_used_projects: Antal nyligen använda projekt
label_optgroup_bookmarks: Bokmärken
label_optgroup_recents: Nyligen använda
button_project_bookmark: Lägg till bokmärke
button_project_bookmark_delete: Ta bort bokmärke
- field_history_default_tab: Ärendes defaultflik för historik
+ field_history_default_tab: Ärendens standardflik för historik
label_issue_history_properties: Ändrade egenskaper
- label_issue_history_notes: Noter
+ label_issue_history_notes: Anteckningar
label_last_tab_visited: Senast besökta flik
field_unique_id: Unikt ID
text_no_subject: inget ämne
@@ -1323,215 +1304,197 @@ sv:
label_password_char_class_digits: siffror
label_password_char_class_special_chars: specialtecken
text_characters_must_contain: Måste innehålla %{character_classes}.
- label_starts_with: startar med
- label_ends_with: slutar med
+ label_starts_with: börjar på
+ label_ends_with: slutar på
label_issue_fixed_version_updated: Målversion uppdaterad
- setting_project_list_defaults: Projektlista defaults
+ setting_project_list_defaults: Projektlista standardinställningar
label_display_type: Visa resultat som
label_display_type_list: Lista
label_display_type_board: Anslagstavla
label_my_bookmarks: Mina bokmärken
- label_import_time_entries: Importera tidsangivelser
- notice_issue_not_closable_by_open_tasks: This issue cannot be closed because it has
- at least one open subtask.
- notice_issue_not_closable_by_blocking_issue: This issue cannot be closed because it
- is blocked by at least one open issue.
- notice_issue_not_reopenable_by_closed_parent_issue: This issue cannot be reopened
- because its parent issue is closed.
- error_attachments_too_many: This file cannot be uploaded because it exceeds the maximum
- number of files that can be attached simultaneously (%{max_number_of_files})
- error_bulk_download_size_too_big: These attachments cannot be bulk downloaded because
- the total file size exceeds the maximum allowed size (%{max_size})
- error_can_not_execute_macro_html: Error executing the <strong>%{name}</strong> macro
- (%{error})
- error_macro_does_not_accept_block: This macro does not accept a block of text
- error_childpages_macro_no_argument: With no argument, this macro can be called from
- wiki pages only
- error_circular_inclusion: Circular inclusion detected
- error_page_not_found: Page not found
- error_filename_required: Filename required
- error_invalid_size_parameter: Invalid size parameter
- error_attachment_not_found: Attachment %{name} not found
- field_passwd_changed_on: Password last changed
- field_toolbar_language_options: Code highlighting toolbar languages
- setting_bulk_download_max_size: Maximum total size for bulk download
- setting_email_domains_allowed: Allowed email domains
- setting_email_domains_denied: Disallowed email domains
- setting_twofa: Two-factor authentication
- permission_delete_project: Delete the project
- label_optional: optional
- label_user_mail_notify_about_high_priority_issues_html: Also notify me about issues
- with a priority of <em>%{prio}</em> or higher
- label_days_to_html: "%{days} days up to %{date}"
- label_required_lower: required
- label_download_all_attachments: Download all files
- label_relations_mapping: Relations mapping
- label_assign_to_me: Assign to me
- button_disable: Disable
- label_import_users: Import users
- twofa__totp__name: Authenticator app
- twofa__totp__text_pairing_info_html: Scan this QR code or enter the plain text key
- into a TOTP app (e.g. <a href="https://support.google.com/accounts/answer/1066447">Google
- Authenticator</a>, <a href="https://authy.com/download/">Authy</a>, <a href="https://guide.duo.com/third-party-accounts">Duo
- Mobile</a>) and enter the code in the field below to activate two-factor authentication.
- twofa__totp__label_plain_text_key: Plain text key
- twofa__totp__label_activate: Enable authenticator app
- twofa_currently_active: 'Currently active: %{twofa_scheme_name}'
- twofa_not_active: Not activated
- twofa_label_code: Code
- twofa_hint_disabled_html: Setting <strong>%{label}</strong> will deactivate and unpair
- two-factor authentication devices for all users.
- twofa_hint_required_html: Setting <strong>%{label}</strong> will require all users
- to set up two-factor authentication at their next login.
- twofa_label_setup: Enable two-factor authentication
- twofa_label_deactivation_confirmation: Disable two-factor authentication
- twofa_notice_select: 'Please select the two-factor scheme you would like to use:'
- twofa_warning_require: The administrator requires you to enable two-factor authentication.
- twofa_activated: Two-factor authentication successfully enabled. It is recommended
- to <a data-method="post" href="%{bc_path}">generate backup codes</a> for your account.
- twofa_deactivated: Two-factor authentication disabled.
- twofa_mail_body_security_notification_paired: Two-factor authentication successfully
- enabled using %{field}.
- twofa_mail_body_security_notification_unpaired: Two-factor authentication disabled
- for your account.
- twofa_mail_body_backup_codes_generated: New two-factor authentication backup codes
- generated.
- twofa_mail_body_backup_code_used: A two-factor authentication backup code has been
- used.
- twofa_invalid_code: Code is invalid or outdated.
- twofa_label_enter_otp: Please enter your two-factor authentication code.
- twofa_too_many_tries: Too many tries.
- twofa_resend_code: Resend code
- twofa_code_sent: An authentication code has been sent to you.
- twofa_generate_backup_codes: Generate backup codes
- twofa_text_generate_backup_codes_confirmation: This will invalidate all existing backup
- codes and generate new ones. Would you like to continue?
- twofa_notice_backup_codes_generated: Your backup codes have been generated.
- twofa_warning_backup_codes_generated_invalidated: New backup codes have been generated.
- Your existing codes from %{time} are now invalid.
- twofa_label_backup_codes: Two-factor authentication backup codes
- twofa_text_backup_codes_hint: Use these codes instead of a one-time password should
- you not have access to your second factor. Each code can only be used once. It is
- recommended to print and store them in a safe place.
- twofa_text_backup_codes_created_at: Backup codes generated %{datetime}.
- twofa_backup_codes_already_shown: Backup codes cannot be shown again, please <a data-method="post"
- href="%{bc_path}">generate new backup codes</a> if required.
- field_twofa_scheme: Two-factor authentication scheme
- text_user_destroy_confirmation: Are you sure you want to delete this user and remove
- all references to them? This cannot be undone. Often, locking a user instead of
- deleting them is the better solution. To confirm, please enter their login (%{login})
- below.
- text_project_destroy_enter_identifier: To confirm, please enter the project's identifier
- (%{identifier}) below.
- button_add_subtask: Add subtask
- notice_invalid_watcher: 'Invalid watcher: User will not receive any notifications
- because they do not have access to view this object.'
- button_fetch_changesets: Fetch commits
- permission_view_message_watchers: View message watchers list
- permission_add_message_watchers: Add message watchers
- permission_delete_message_watchers: Delete message watchers
- label_message_watchers: Watchers
- button_copy_link: Copy link
- error_invalid_authenticity_token: Invalid form authenticity token.
- error_query_statement_invalid: An error occurred while executing the query and has
- been logged. Please report this error to your Redmine administrator.
- permission_view_wiki_page_watchers: View wiki page watchers list
- permission_add_wiki_page_watchers: Add wiki page watchers
- permission_delete_wiki_page_watchers: Delete wiki page watchers
- label_wiki_page_watchers: Watchers
- label_attachment_description: File description
- error_no_data_in_file: The file does not contain any data
- field_twofa_required: Require two factor authentication
- twofa_hint_optional_html: Setting <strong>%{label}</strong> will let users set up
- two-factor authentication at will, unless it is required by one of their groups.
- twofa_text_group_required: This setting is only effective when the global two factor
- authentication setting is set to 'optional'. Currently, two factor authentication
- is required for all users.
- twofa_text_group_disabled: This setting is only effective when the global two factor
- authentication setting is set to 'optional'. Currently, two factor authentication
- is disabled.
- field_default_issue_query: Default issue query
+ label_import_time_entries: Importera tidsstämplingar
+ notice_issue_not_closable_by_open_tasks: Det här ärendet kan inte stängas eftersom det har minst en öppen underaktivitet.
+ notice_issue_not_closable_by_blocking_issue: Det här ärendet kan inte stängas eftersom det blockeras av minst ett öppet ärende.
+ notice_issue_not_reopenable_by_closed_parent_issue: Det här ärendet kan inte återöppnas eftersom dess överordnade ärende är stängt.
+ error_attachments_too_many: 'Den här filen kan inte laddas upp eftersom den överskriderdet maximala antalet filer som kan bifogas samtidigt (%{max_number_of_files}).'
+ error_bulk_download_size_too_big: 'Dessa bilagor kan inte laddas ner samtidigt eftersom den totala filstorleken överskrider den maximalt tillåtna storleken (%{max_size}).'
+ error_can_not_execute_macro_html: 'Fel vid körning av makrot <strong>%{name}</strong> (%{error}).'
+ error_macro_does_not_accept_block: Det här makrot accepterar inte ett textblock
+ error_childpages_macro_no_argument: Utan argument kan det här makrot endast anropas från wikisidor
+ error_circular_inclusion: Cirkulär inbäddning upptäckt
+ error_page_not_found: Sidan kunde inte hittas
+ error_filename_required: Filnamn krävs
+ error_invalid_size_parameter: Ogiltig storleksparameter
+ error_attachment_not_found: Bilagan %{name} kunde inte hittas
+ field_passwd_changed_on: Lösenordet ändrades
+ field_toolbar_language_options: Språk för verktygsfält för kodmarkering
+ setting_bulk_download_max_size: Maximal totalstorlek för samtidig nedladdning
+ setting_email_domains_allowed: Tillåtna e-postdomäner
+ setting_email_domains_denied: Otillåtna e-postdomäner
+ setting_twofa: Tvåfaktorsautentisering
+ permission_delete_project: Ta bort projektet
+ label_optional: valfritt
+ label_user_mail_notify_about_high_priority_issues_html: 'Avisera mig också om ärenden med en prioritet på <em>%{prio}</em> eller högre'
+ label_days_to_html: "%{days} dagar fram till %{date}"
+ label_required_lower: obligatorisk
+ label_download_all_attachments: Ladda ner samtliga filer
+ label_relations_mapping: Relationsmappning
+ label_assign_to_me: Tilldela mig
+ button_disable: Inaktivera
+ label_import_users: Importera användare
+ twofa__totp__name: Autentiseringsapp
+ twofa__totp__text_pairing_info_html: 'Skanna den här QR-koden eller ange nyckeln i en autentiseringsapp (t.ex. <a href="https://support.google.com/accounts/answer/1066447">Google Authenticator</a>, <a href="https://authy.com/download/">Authy</a>, <a href="https://guide.duo.com/third-party-accounts"> Duo Mobile</a>) och ange sedan koden i fältet nedan för att aktivera tvåfaktorsautentisering.'
+ twofa__totp__label_plain_text_key: Nyckel i klartext
+ twofa__totp__label_activate: Aktivera autentiseringsapp
+ twofa_currently_active: 'För närvarande aktiv: %{twofa_scheme_name}'
+ twofa_not_active: Inte aktiverad
+ twofa_label_code: Kod
+ twofa_hint_disabled_html: Inställningen <strong>%{label}</strong> kommer att inaktivera och koppla bort tvåfaktorsautentiseringsenheter för alla användare.
+ twofa_hint_required_html: Inställningen <strong>%{label}</strong> kommer att kräva att alla användare konfigurerar tvåfaktorsautentisering vid sin nästa inloggning.
+ twofa_label_setup: Aktivera tvåfaktorsautentisering
+ twofa_label_deactivation_confirmation: Inaktivera tvåfaktorsautentisering
+ twofa_notice_select: 'Välj det tvåfaktorsalternativ du vill använda:'
+ twofa_warning_require: Administratören kräver att du aktiverar tvåfaktorsautentisering.
+ twofa_activated: 'Tvåfaktorsautentisering har aktiverats framgångsrikt. Det rekommenderas att du <a data-method="post" href="%{bc_path}">genererar reservkoder</a> för ditt konto.'
+ twofa_deactivated: Tvåfaktorsautentisering inaktiverad.
+ twofa_mail_body_security_notification_paired: 'Tvåfaktorsautentisering har framgångsrikt aktiverats med hjälp av %{field}.'
+ twofa_mail_body_security_notification_unpaired: Tvåfaktorsautentisering har inaktiverats för ditt konto.
+ twofa_mail_body_backup_codes_generated: Nya reservkoder för tvåfaktorsautentisering har genererats.
+ twofa_mail_body_backup_code_used: En reservkod för tvåfaktorsautentisering har använts.
+ twofa_invalid_code: Koden är ogiltig eller föråldrad.
+ twofa_label_enter_otp: Ange din kod för tvåfaktorsautentisering.
+ twofa_too_many_tries: För många försök.
+ twofa_resend_code: Skicka kod på nytt
+ twofa_code_sent: En autentiseringskod har skickats till dig.
+ twofa_generate_backup_codes: Generera reservkoder
+ twofa_text_generate_backup_codes_confirmation: Detta kommer att göra alla befintliga reservkoder obrukbara och generera nya. Vill du fortsätta?
+ twofa_notice_backup_codes_generated: Dina reservkoder har genererats
+ twofa_warning_backup_codes_generated_invalidated: 'Nya reservkoder har genererats. Dina befintliga koder från %{time} är nu obrukbara.'
+ twofa_label_backup_codes: Reservkoder för tvåfaktorsautentisering.
+ twofa_text_backup_codes_hint: Använd dessa koder istället för en engångslösenkod om du inte har tillgång till din tvåfaktorsautentiseringsapp. Varje kod kan endast användas en gång. Det rekommenderas att skriva ut dem och förvara dem på en säker plats.
+ twofa_text_backup_codes_created_at: Reservkoder genererades %{datetime}.
+ twofa_backup_codes_already_shown: 'Reservkoder kan inte visas igen, men du kan <a data-method="post" href="%{bc_path}">generera nya reservkoder</a> om så behövs.'
+ field_twofa_scheme: Tvåfaktorsautentiseringssystem
+ text_user_destroy_confirmation: 'Är du säker på att du vill ta bort den här användaren och ta bort alla referenser till denne? Detta kan inte ångras. Ofta är det bättre att låsa en användare istället för att ta bort den. För att bekräfta, vänligen ange deras inloggning (%{login}) nedan.'
+ text_project_destroy_enter_identifier: 'För att bekräfta, vänligen ange projektets identifierare (%{identifier}) nedan.'
+ button_add_subtask: Lägg till underaktivitet
+ notice_invalid_watcher: 'Ogiltig bevakare: Användaren kommer inte att ta emot några aviseringar eftersom de inte har åtkomst att visa detta objekt.'
+ button_fetch_changesets: Hämta commits
+ permission_view_message_watchers: Visa lista över meddelandebevakare
+ permission_add_message_watchers: Lägg till meddelandebevakare
+ permission_delete_message_watchers: Ta bort meddelandebevakare
+ label_message_watchers: Bevakare
+ button_copy_link: Kopiera länk
+ error_invalid_authenticity_token: Ogiltig autentiseringstoken för formuläret.
+ error_query_statement_invalid: Ett fel inträffade vid körning av frågan och har loggats. Vänligen rapportera detta fel till din Redmine-administratör.
+ permission_view_wiki_page_watchers: Visa lista över bevakare för wikisida
+ permission_add_wiki_page_watchers: Lägg till bevakare för wikisida
+ permission_delete_wiki_page_watchers: Ta bort bevakare för wikisida
+ label_wiki_page_watchers: Bevakare
+ label_attachment_description: Filbeskrivning
+ error_no_data_in_file: Filen innehåller inga data
+ field_twofa_required: Kräv tvåfaktorsautentisering
+ twofa_hint_optional_html: Inställningen <strong>%{label}</strong> kommer att låta användare konfigurera tvåfaktorsautentisering efter eget önskemål, om det inte krävs av någon av deras grupper.
+ twofa_text_group_required: Den här inställningen är endast giltig när den globala inställningen för tvåfaktorsautentisering är inställd på "valfritt". För närvarande krävs tvåfaktorsautentisering för alla användare.
+ twofa_text_group_disabled: Den här inställningen är endast giltig när den globala inställningen för tvåfaktorsautentisering är inställd på "valfritt". För närvarande är tvåfaktorsautentisering inaktiverad.
+ field_default_issue_query: Standardfiltrering för ärenden
label_default_queries:
- for_all_projects: For all projects
- for_current_project: For current project
- for_all_users: For all users
- for_this_user: For this user
- text_allowed_queries_to_select: Public (to any users) queries only selectable
- text_all_migrations_have_been_run: All database migrations have been run
- button_save_object: Save %{object_name}
- button_edit_object: Edit %{object_name}
- button_delete_object: Delete %{object_name}
- text_setting_config_change: You can configure the behaviour in config/configuration.yml.
- Please restart the application after editing it.
- label_bulk_edit: Bulk edit
- button_create_and_follow: Create and follow
- label_subtask: Subtask
- label_default_query: Default query
- field_default_project_query: Default project query
- label_required_administrators: required for administrators
- twofa_hint_required_administrators_html: Setting <strong>%{label}</strong> behaves
- like optional, but will require all users with administration rights to set up two-factor
- authentication at their next login.
- label_auto_watch_on: Auto watch
- label_auto_watch_on_issue_contributed_to: Issues I contributed to
- text_project_close_confirmation: Are you sure you want to close the '%{value}' project
- to make it read-only?
- text_project_reopen_confirmation: Are you sure you want to reopen the '%{value}' project?
- text_project_archive_confirmation: Are you sure you want to archive the '%{value}'
- project?
- mail_destroy_project_failed: Project %{value} could not be deleted.
- mail_destroy_project_successful: Project %{value} was deleted successfully.
- mail_destroy_project_with_subprojects_successful: Project %{value} and its subprojects
- were deleted successfully.
- project_status_scheduled_for_deletion: scheduled for deletion
- text_projects_bulk_destroy_confirmation: Are you sure you want to delete the selected
- projects and related data?
- text_projects_bulk_destroy_head: |
- You are about to permanently delete the following projects, including possible subprojects and any related data.
- Please review the information below and confirm that this is indeed what you want to do.
- This action cannot be undone.
- text_projects_bulk_destroy_confirm: To confirm, please enter "%{yes}" in the box below.
- text_subprojects_bulk_destroy: 'including its subproject(s): %{value}'
- field_current_password: Current password
- sudo_mode_new_info_html: "<strong>What's happening?</strong> You need to reconfirm
- your password before taking any administrative actions, this ensures your account
- stays protected."
- label_edited: Edited
- label_time_by_author: "%{time} by %{author}"
- field_default_time_entry_activity: Default spent time activity
- field_is_member_of_group: Member of group
- text_users_bulk_destroy_head: You are about to delete the following users and remove
- all references to them. This cannot be undone. Often, locking users instead of deleting
- them is the better solution.
- text_users_bulk_destroy_confirm: To confirm, please enter "%{yes}" below.
- permission_select_project_publicity: Set project public or private
- label_auto_watch_on_issue_created: Issues I created
- field_any_searchable: Any searchable text
- label_contains_any_of: contains any of
- button_apply_issues_filter: Apply issues filter
- label_view_previous_annotation: View annotation prior to this change
- label_has_been: has been
- label_has_never_been: has never been
- label_changed_from: changed from
- label_issue_statuses_description: Issue statuses description
- label_open_issue_statuses_description: View all issue statuses description
- text_select_apply_issue_status: Select issue status
- field_name_or_email_or_login: Name, email or login
- text_default_active_job_queue_changed: Default queue adapter which is well suited
- only for dev/test changed
- label_option_auto_lang: auto
- label_issue_attachment_added: Attachment added
- field_estimated_remaining_hours: Estimated remaining time
- field_last_activity_date: Last activity
- setting_issue_done_ratio_interval: Done ratio options interval
- setting_copy_attachments_on_issue_copy: Copy attachments on copy
- field_thousands_delimiter: Thousands delimiter
- label_involved_principals: Author / Previous assignee
+ for_all_projects: För alla projekt
+ for_current_project: För aktivt projekt
+ for_all_users: För alla användare
+ for_this_user: För den här användaren
+ text_allowed_queries_to_select: Endast publika (för alla användare) filtreringar kan väljas
+ text_all_migrations_have_been_run: Alla databas-migrationer har körts
+ button_save_object: 'Spara %{object_name}'
+ button_edit_object: 'Redigera %{object_name}'
+ button_delete_object: 'Ta bort %{object_name}'
+ text_setting_config_change: 'Du kan konfigurera beteendet i <strong>config/configuration.yml</strong>. Vänligen starta om applikationen efter att du har redigerat den.'
+ label_bulk_edit: Massredigera
+ button_create_and_follow: Skapa och öppna
+ label_subtask: Underaktivitet
+ label_default_query: Standardfiltrering
+ field_default_project_query: Standardfiltrering för projekt
+ label_required_administrators: obligatoriskt för administratörer
+ twofa_hint_required_administrators_html: 'Inställningen <strong>%{label}</strong> fungerar som valfri, men kommer att kräva att alla användare med administratörsrättigheter konfigurerar tvåfaktorsautentisering vid sin nästa inloggning.'
+ label_auto_watch_on: Automatisk bevakning
+ label_auto_watch_on_issue_contributed_to: Ärenden jag varit delaktig i
+ text_project_close_confirmation: 'Är du säker på att du vill stänga projektet "%{value}" för att göra det skrivskyddat?'
+ text_project_reopen_confirmation: 'Är du säker på att du vill återöppna projektet "%{value}"?'
+ text_project_archive_confirmation: 'Är du säker på att du vill arkivera projektet "%{value}"?'
+ mail_destroy_project_failed: 'Projektet %{value} kunde inte raderas.'
+ mail_destroy_project_successful: 'Projektet %{value} raderades framgångsrikt.'
+ mail_destroy_project_with_subprojects_successful: 'Projektet %{value} och dess underprojekt raderades framgångsrikt.'
+ project_status_scheduled_for_deletion: schemalagd för radering
+ text_projects_bulk_destroy_confirmation: Är du säker på att du vill radera de valda projekten och relaterad data?
+ text_projects_bulk_destroy_head: Du är på väg att permanent radera följande projekt, inklusive eventuella underprojekt och relaterad data. Vänligen granska informationen nedan och bekräfta att detta verkligen är vad du vill göra. Denna åtgärd kan inte ångras.
+ text_projects_bulk_destroy_confirm: 'För att bekräfta, vänligen ange "%{yes}" i rutan nedan.'
+ text_subprojects_bulk_destroy: 'inklusive dess underprojekt: %{value}'
+ field_current_password: Nuvarande lösenord
+ sudo_mode_new_info_html: '<strong>Vad händer?</strong> Du måste bekräfta ditt lösenord igen innan du vidtar några administrativa åtgärder, detta säkerställer att ditt konto förblir skyddat.'
+ label_edited: Redigerad
+ label_time_by_author: '%{time} av %{author}'
+ field_default_time_entry_activity: Standardaktivitet för spenderad tid
+ field_is_member_of_group: Medlem i grupp
+ text_users_bulk_destroy_head: Du är på väg att ta bort följande användare och ta bort alla referenser till dem. Detta kan inte ångras. Ofta är det bättre att låsa användare istället för att ta bort dem.
+ text_users_bulk_destroy_confirm: 'För att bekräfta, vänligen ange "%{yes}" nedan.'
+ permission_select_project_publicity: Ställ in projektet som publikt eller privat
+ label_auto_watch_on_issue_created: Ärenden jag har skapat
+ field_any_searchable: All sökbart text
+ label_contains_any_of: innehåller någon av
+ button_apply_issues_filter: Tillämpa filter för ärenden
+ label_view_previous_annotation: Visa kommentar före denna ändring
+ label_has_been: har varit
+ label_has_never_been: har aldrig varit
+ label_changed_from: ändrad från
+ label_issue_statuses_description: Beskrivning av ärendestatusar
+ label_open_issue_statuses_description: Visa alla beskrivningar av ärendestatusar
+ text_select_apply_issue_status: Välj ärendestatus
+ field_name_or_email_or_login: Namn, e-post eller inloggning
+ text_default_active_job_queue_changed: Standardköadapter som är lämpad endast för utveckling/test har ändrats
+ label_option_auto_lang: automatisk
+ label_issue_attachment_added: Bilaga tillagd
+ field_estimated_remaining_hours: Beräknad återstående tid
+ field_last_activity_date: Senaste aktivitet
+ setting_issue_done_ratio_interval: 'Alternativ för intervall för "% Klar"'
+ setting_copy_attachments_on_issue_copy: Kopiera bilagor vid kopiering
+ field_thousands_delimiter: Tusenavgränsare
+ label_involved_principals: 'Skapare / Tidigare tilldelad'
label_attachment_summary:
- zero: "%{filename}"
- one: "%{filename} and 1 file"
- other: "%{filename} and %{count} files"
+ zero: '%{filename}'
+ one: '%{filename} och 1 fil'
+ other: '%{filename} och %{count} filer'
setting_wiki_tablesort_enabled: Javascript based table sorting in wiki content
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/ta-IN.yml b/config/locales/ta-IN.yml
index b0950a130..70284cec6 100644
--- a/config/locales/ta-IN.yml
+++ b/config/locales/ta-IN.yml
@@ -99,8 +99,9 @@ ta-IN:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "மற்றும்"
- skip_last_comma: false
+ last_word_connector: ", மற்றும் "
+ two_words_connector: " மற்றும் "
+ words_connector: ", "
activerecord:
errors:
@@ -1457,3 +1458,34 @@ ta-IN:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 4bbb726fe..8ada77934 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -93,8 +93,9 @@ th:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "and"
- skip_last_comma: false
+ last_word_connector: ", และ "
+ two_words_connector: " และ "
+ words_connector: ", "
activerecord:
errors:
@@ -1498,3 +1499,34 @@ th:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index 07f1b9e4c..15b00e007 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -108,8 +108,9 @@ tr:
support:
array:
- sentence_connector: "ve"
- skip_last_comma: true
+ last_word_connector: " ve "
+ two_words_connector: " ve "
+ words_connector: ", "
activerecord:
errors:
@@ -1501,3 +1502,34 @@ tr:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 2ae710697..e1e890a44 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -109,8 +109,9 @@ uk:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "і"
- skip_last_comma: false
+ last_word_connector: " та "
+ two_words_connector: " і "
+ words_connector: ", "
activerecord:
errors:
@@ -1490,3 +1491,34 @@ uk:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index b53331816..119c94652 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -1507,3 +1507,34 @@ vi:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml
index dc511f8b0..6dc0d66b0 100644
--- a/config/locales/zh-TW.yml
+++ b/config/locales/zh-TW.yml
@@ -42,8 +42,6 @@
words_connector: ", "
two_words_connector: " 和 "
last_word_connector: ", 和 "
- sentence_connector: "且"
- skip_last_comma: false
number:
# 使用於 number_with_delimiter()
@@ -371,7 +369,7 @@
field_priority: 優先權
field_fixed_version: 版本
field_user: 用戶
- field_principal: User or Group
+ field_principal: 用戶或群組
field_role: 角色
field_homepage: 網站首頁
field_is_public: 公開
@@ -1519,3 +1517,32 @@
label_progressbar: 進度條
error_spent_on_closed_issue: 無法在已結束的議題上記錄工時
setting_timelog_accept_closed_issues: 允許在已結束的議題上紀錄工時
+ setting_related_issues_default_columns: 預設顯示於相關議題與子任務清單的欄位
+ setting_display_related_issues_table_headers: 顯示表格標題欄位
+ error_can_not_remove_role_reason_members_html: "<p>下列專案中有成員
+ 屬於此角色:<br>%{projects}</p>"
+ setting_reactions_enabled: 啟用表情回應功能
+ reaction_text_x_other_users:
+ one: 1 位其他用戶
+ other: "%{count} 位其他用戶"
+ text_setting_gravatar_default_initials_html: 用戶姓名的首字母縮寫將被送往 <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ 用以產生其大頭貼.
+ permission_view_project: 檢視專案
+ permission_search_project: 搜尋專案
+ permission_view_members: 檢視專案成員
+ label_oauth_permission_admin: 管理此 Redmine
+ label_oauth_admin_access: 管理權限
+ label_oauth_application_plural: 應用程式
+ label_oauth_authorized_application_plural: 已授權之應用程式
+ text_oauth_admin_permission: 完整的系統管理存取權限。經由管理者授權後,
+ 本應用程式將可讀取與寫入所有資料,並能模擬其他使用者操作。
+ text_oauth_admin_permission_info: 此應用程式要求取得完整的系統管理存取權限。
+ 若您目前(或未來具備)管理者身分,它將可代表您讀取及寫入所有資料,並能模擬其他使用者操作。
+ 如欲避免此情況,請改以「一般使用者」(無管理者權限)身分進行授權。
+ text_oauth_copy_secret_now: 請立即將密鑰複製到安全的地方保存,之後將無法再次顯示。
+ text_oauth_implicit_permissions: 檢視您的姓名、登入帳號及主要電子郵件地址
+ text_oauth_info_scopes: 請選擇此應用程式可要求存取的權限範圍。
+ 應用程式僅能執行您在此選擇的權限,無法超越此範圍。
+ 此外,其存取權限也將始終受限於授權使用者的角色與所屬專案成員資格。
+ label_position: Position
+ label_message: Message
diff --git a/config/locales/zh.yml b/config/locales/zh.yml
index 85c123bb1..bcde1116d 100644
--- a/config/locales/zh.yml
+++ b/config/locales/zh.yml
@@ -99,8 +99,9 @@ zh:
# Used in array.to_sentence.
support:
array:
- sentence_connector: "和"
- skip_last_comma: false
+ words_connector: ", "
+ two_words_connector: " 和 "
+ last_word_connector: ", 和 "
activerecord:
errors:
@@ -1438,3 +1439,34 @@ zh:
label_progressbar: Progress bar
error_spent_on_closed_issue: Cannot log time on a closed issue
setting_timelog_accept_closed_issues: Accept time logs on closed issues
+ setting_related_issues_default_columns: Related and sub issues list defaults
+ setting_display_related_issues_table_headers: Show table headers
+ error_can_not_remove_role_reason_members_html: "<p>The following projects have members
+ with this role:<br>%{projects}</p>"
+ setting_reactions_enabled: Enable reactions
+ reaction_text_x_other_users:
+ one: 1 other
+ other: "%{count} others"
+ text_setting_gravatar_default_initials_html: Users' initials are sent to <a href="https://www.gravatar.com">https://www.gravatar.com</a>
+ to generate their avatars.
+ permission_view_project: View projects
+ permission_search_project: Search projects
+ permission_view_members: View project members
+ label_oauth_permission_admin: Administrate this Redmine
+ label_oauth_admin_access: Administration
+ label_oauth_application_plural: Applications
+ label_oauth_authorized_application_plural: Authorized applications
+ text_oauth_admin_permission: Full administrative access. When authorized by an Administrator,
+ this application will be able to read and write all data and impersonate other users.
+ text_oauth_admin_permission_info: This application requests full administrative access.
+ If you are an Administrator (or become one in the future), it will be able to read
+ and write all data and impersonate other users on your behalf. If you want to avoid
+ this, authorize it as a user without Administrator privileges instead.
+ text_oauth_copy_secret_now: Copy the secret to a safe place now, it will not be shown
+ again.
+ text_oauth_implicit_permissions: View your name, login and primary email address
+ text_oauth_info_scopes: Select the scopes this application may request. The application
+ will not be allowed to do more than what is selected here. It will also always be
+ limited by the roles and project memberships of the user who authorized it.
+ label_position: Position
+ label_message: Message
diff --git a/config/routes.rb b/config/routes.rb
index 89927bee3..52c95c6a4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,6 +18,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Rails.application.routes.draw do
+ use_doorkeeper do
+ controllers :applications => 'oauth2_applications'
+ end
+
+ root :to => 'welcome#index'
root :to => 'welcome#index', :as => 'home'
match 'login', :to => 'account#login', :as => 'signin', :via => [:get, :post]
@@ -61,6 +66,8 @@ Rails.application.routes.draw do
end
end
+ resources :reactions, only: [:create, :destroy]
+
get '/projects/:project_id/issues/gantt', :to => 'gantts#show', :as => 'project_gantt'
get '/issues/gantt', :to => 'gantts#show'
diff --git a/config/settings.yml b/config/settings.yml
index a0c256cdd..b1217fc0a 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -362,4 +362,6 @@ timelog_accept_closed_issues:
show_status_changes_in_mail_subject:
default: 1
wiki_tablesort_enabled:
+ default: 0
+reactions_enabled:
default: 1
diff --git a/db/migrate/017_create_settings.rb b/db/migrate/017_create_settings.rb
index 5768ca24c..f86a1c2c3 100644
--- a/db/migrate/017_create_settings.rb
+++ b/db/migrate/017_create_settings.rb
@@ -8,6 +8,7 @@ class CreateSettings < ActiveRecord::Migration[4.2]
# Persist default settings for new installations
Setting.create!(name: 'default_notification_option', value: Setting.default_notification_option)
Setting.create!(name: 'text_formatting', value: Setting.text_formatting)
+ Setting.create!(name: 'wiki_tablesort_enabled', value: Setting.wiki_tablesort_enabled)
end
def self.down
diff --git a/db/migrate/20250423065135_create_reactions.rb b/db/migrate/20250423065135_create_reactions.rb
new file mode 100644
index 000000000..56f345e1b
--- /dev/null
+++ b/db/migrate/20250423065135_create_reactions.rb
@@ -0,0 +1,11 @@
+class CreateReactions < ActiveRecord::Migration[7.2]
+ def change
+ create_table :reactions do |t|
+ t.references :reactable, polymorphic: true, null: false
+ t.references :user, null: false
+ t.timestamps null: false
+ end
+ add_index :reactions, [:reactable_type, :reactable_id, :user_id], unique: true
+ add_index :reactions, [:reactable_type, :reactable_id, :id]
+ end
+end
diff --git a/db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb b/db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb
new file mode 100644
index 000000000..b64a42dbb
--- /dev/null
+++ b/db/migrate/20250530185658_ensure_wiki_tablesort_setting_is_stored_in_db.rb
@@ -0,0 +1,8 @@
+class EnsureWikiTablesortSettingIsStoredInDb < ActiveRecord::Migration[7.2]
+ def change
+ unless Setting.where(name: "wiki_tablesort_enabled").exists?
+ setting = Setting.new(:name => "wiki_tablesort_enabled", :value => 1)
+ setting.save!
+ end
+ end
+end
diff --git a/db/migrate/20250611092155_create_doorkeeper_tables.rb b/db/migrate/20250611092155_create_doorkeeper_tables.rb
new file mode 100644
index 000000000..9e8096be3
--- /dev/null
+++ b/db/migrate/20250611092155_create_doorkeeper_tables.rb
@@ -0,0 +1,68 @@
+class CreateDoorkeeperTables < ActiveRecord::Migration[7.2]
+ def change
+ create_table :oauth_applications do |t|
+ t.string :name, null: false
+ t.string :uid, null: false
+ t.string :secret, null: false
+ t.text :redirect_uri, null: false
+ t.text :scopes, null: false
+ t.boolean :confidential, null: false, default: true
+ t.timestamps null: false
+ end
+
+ add_index :oauth_applications, :uid, unique: true
+
+ create_table :oauth_access_grants do |t|
+ t.integer :resource_owner_id, null: false
+ t.references :application, null: false
+ t.string :token, null: false
+ t.integer :expires_in, null: false
+ t.text :redirect_uri, null: false
+ t.datetime :created_at, null: false
+ t.datetime :revoked_at
+ t.text :scopes
+ end
+
+ add_index :oauth_access_grants, :token, unique: true
+ add_foreign_key(
+ :oauth_access_grants,
+ :oauth_applications,
+ column: :application_id
+ )
+ add_foreign_key(
+ :oauth_access_grants,
+ :users,
+ column: :resource_owner_id
+ )
+
+ create_table :oauth_access_tokens do |t|
+ t.integer :resource_owner_id
+ t.references :application
+
+ t.string :token, null: false
+
+ t.string :refresh_token
+ t.integer :expires_in
+ t.datetime :revoked_at
+ t.datetime :created_at, null: false
+ t.text :scopes
+
+ t.string :previous_refresh_token, null: false, default: ""
+ end
+
+ add_index :oauth_access_tokens, :token, unique: true
+ add_index :oauth_access_tokens, :resource_owner_id
+ add_index :oauth_access_tokens, :refresh_token, unique: true
+
+ add_foreign_key(
+ :oauth_access_tokens,
+ :oauth_applications,
+ column: :application_id
+ )
+ add_foreign_key(
+ :oauth_access_tokens,
+ :users,
+ column: :resource_owner_id
+ )
+ end
+end
diff --git a/db/migrate/20250611092227_enable_pkce.rb b/db/migrate/20250611092227_enable_pkce.rb
new file mode 100644
index 000000000..cfc666f50
--- /dev/null
+++ b/db/migrate/20250611092227_enable_pkce.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class EnablePkce < ActiveRecord::Migration[7.2]
+ def change
+ add_column :oauth_access_grants, :code_challenge, :string, null: true
+ add_column :oauth_access_grants, :code_challenge_method, :string, null: true
+ end
+end
diff --git a/doc/CHANGELOG b/doc/CHANGELOG
index 78b93cb07..4a7ed0745 100644
--- a/doc/CHANGELOG
+++ b/doc/CHANGELOG
@@ -4,6 +4,161 @@ Redmine - project management software
Copyright (C) 2006- Jean-Philippe Lang
https://www.redmine.org/
+== 2025-07-07 v6.0.6
+
+=== [Attachments]
+
+* Defect #42920: Missing icon in attachments upload form
+
+=== [Code cleanup/refactoring]
+
+* Defect #42687: Fix random failures in several system tests with Chrome 133 and later
+* Patch #42422: Use Capybara's assert_current_path in "log_user" steps to wait for page in ApplicationSystemTestCase
+* Patch #42600: Suppress "Change your password" popup for stable system tests
+* Patch #42756: Update tests for rails-dom-testing 2.3.0 whitespace collapsing
+
+=== [Database]
+
+* Defect #42622: Joining both atom_token and api_token on the User model causes an error due to the ambiguous column name "action"
+
+=== [Documentation]
+
+* Defect #42644: Noto Sans fonts are not applied on Wiki syntax help pages
+* Defect #42657: Update documentation links and fix config flag typo
+* Patch #42618: Add missing allowed CSS properties to the CommonMark Markdown help
+
+=== [Email receiving]
+
+* Defect #42962: Mail handler fails to create issues from emails over 4MB on Rack >= 3.1.14
+
+=== [Gems support]
+
+* Defect #42606: RuboCop warning about deprecated `EnsureNode#body` with rubocop-ast >= 1.41
+
+=== [I18n]
+
+* Defect #42815: Limit available locales to those defined by Redmine itself no longer works
+
+=== [Issues list]
+
+* Defect #42807: Progress Bar in issues list has a border above it
+
+=== [Issues workflow]
+
+* Defect #42875: "Page not found" error when saving workflows with many statuses on Rack >= 3.1.14
+
+=== [No category]
+
+* Patch #42688: Run system tests on GitHub CI
+
+=== [Performance]
+
+* Defect #42933: Fix N+1 query issue in Wiki history page when loading authors of Wiki content versions
+
+=== [SCM]
+
+* Defect #42839: Downloading .js files from the repository browser fails with a 422 error due to ActionController::InvalidCrossOriginRequest
+* Patch #42597: Skip some Mercurial tests when using Mercurial 5.1 or later in Redmine 6.0 or 5.1
+
+=== [Security]
+
+* Patch #42662: Require net-imap gem 0.2.5, 0.3.9, 0.4.20, 0.5.7, or later to address CVE-2025-43857
+
+=== [Text formatting]
+
+* Defect #42332: "Edit this section" button is missing for headings rendered as multiline HTML
+* Defect #42648: Wiki/CommonMark: Broken references for multiple footnote usage
+
+=== [Translations]
+
+* Patch #42739: Persian translate update for 6.0-stable
+* Patch #42754: Swedish translation update for 6.0-stable
+
+=== [UI]
+
+* Defect #42640: Query totals overlaps query buttons when an RTL language is used
+* Defect #42654: Text in project jump box is vertically misaligned
+* Defect #42773: Padding in the Preview tab is larger than in the actual rendered content
+* Defect #42786: "Clear" button for custom queries has incorrect styling inside the flyout menu
+* Defect #42797: Loading and waiting icons use legacy icons and overlap filename when attaching files
+* Defect #42953: Replace legacy other download icon with SVG icon
+* Patch #42641: Improve SVG contrast when an item is selected in administration sidebar
+* Patch #42794: Hide irrelevant information when printing
+
+=== [Wiki]
+
+* Defect #42558: JPEG images are not shown in exported PDF files
+
+== 2025-04-20 v6.0.5
+
+=== [Administration]
+
+* Defect #42584: NoMethodError when creating a user with an invalid email address and domain restrictions are enabled
+
+=== [Attachments]
+
+* Defect #42394: Inconsistent behaviour between attachment download routes with and without filename
+
+=== [Code cleanup/refactoring]
+
+* Patch #42562: Fix random test failure in ProjectAdminQueryTest due to missing language setting
+* Patch #42572: Fix random test failure in MemberTest#test_update_roles_with_inherited_roles due to non-deterministic ordering
+
+=== [Custom fields]
+
+* Defect #42342: Missing thousands separator in Integer and Float custom field totals
+* Patch #41935: Add "editable" attribute in the custom fields API response
+
+=== [Gantt]
+
+* Defect #42145: MiniMagick (> 5) removed cli_path, result crash when supplied imagemagick_convert_command
+
+=== [Issues]
+
+* Defect #42458: "For all projects" checkbox should be disabled when editing an existing query in which the checkbox is already checked
+
+=== [Performance]
+
+* Defect #40728: Slow loading of global spent time list in MySQL
+* Feature #42574: Optimize autocomplete issue listing triggered by typing "##" by eager loading trackers
+
+=== [Plugin API]
+
+* Defect #42509: Plugin activity icons broken when multiple plugins are loaded
+
+=== [Projects]
+
+* Patch #42440: Fix project selector focus by explicitly targeting the first selected item
+
+=== [SCM]
+
+* Patch #42500: Skip repository tests when the SCM client command is unavailable
+
+=== [Text formatting]
+
+* Defect #42545: Commit message in issue history might be rendered in incorrect context
+
+=== [UI]
+
+* Defect #41828: In mobile view, delete relation svg icon in 'Related Issues' on an issue page, overflow text
+* Defect #41833: Tabs left / right buttons use legacy icons
+* Defect #41947: Collapse arrow shows the wrong direction at /workflows/edit
+* Defect #41952: Flash notice icons use the legacy icons
+* Defect #41967: Replace SCM action legacy icons with SVG icons in the tree view of the repository browser
+* Defect #42181: Project jump box uses legacy caret icons
+* Defect #42285: Icon expanded for closed fixed versions missing
+* Defect #42286: Context menu right arrow uses the legacy icon
+* Defect #42369: Expander icons not switch in Collapse all/Expand all
+* Defect #42465: Improve SVG icon compatibility with RTL languages
+* Defect #42487: Improve SVG contrast when a row is selected on table list
+* Defect #42520: PNG icon displayed instead of SVG in subtasks list when viewing all tasks
+* Defect #42532: Expander icon not working in repository tree
+* Defect #42575: Fix sidebar switch button display in RTL language
+* Defect #42576: Newly attached files are displayed using the legacy icons
+* Patch #42497: Adjust the position of the news comment delete button
+* Patch #42577: Replace legacy Atom icon with SVG icon
+* Patch #42596: Do not show user icon in add watchers modal when gravatar is disabled
+
== 2025-03-10 v6.0.4
=== [Administration]
diff --git a/doc/INSTALL b/doc/INSTALL
index 89c85479d..f229fa509 100644
--- a/doc/INSTALL
+++ b/doc/INSTALL
@@ -7,7 +7,7 @@ https://www.redmine.org/
== Requirements
-* Ruby 3.1, 3.2, 3.3
+* Ruby 3.2, 3.3, 3.4
* A database:
* MySQL (tested with MySQL 8)
@@ -67,10 +67,10 @@ The current version of Firefox, Safari, Chrome, Chromium and Microsoft Edge.
using:
bundle exec rake assets:precompile RAILS_ENV="production"
-
+
If deploying to a sub-uri, set the relative URL root as follows:
bundle exec rake assets:precompile RAILS_ENV="production" RAILS_RELATIVE_URL_ROOT=/sub-uri
-
+
If you experience issues with missing assets in the browser, try
removing the public/assets directory before re-running the precompile:
bundle exec rake assets:clobber RAILS_ENV="production"
@@ -136,10 +136,10 @@ Please do not enter your SMTP settings in environment.rb.
== References
-* https://www.redmine.org/wiki/redmine/RedmineInstall
-* https://www.redmine.org/wiki/redmine/EmailConfiguration
-* https://www.redmine.org/wiki/redmine/RedmineSettings
-* https://www.redmine.org/wiki/redmine/RedmineRepositories
-* https://www.redmine.org/wiki/redmine/RedmineReceivingEmails
-* https://www.redmine.org/wiki/redmine/RedmineReminderEmails
-* https://www.redmine.org/wiki/redmine/RedmineLDAP
+* https://www.redmine.org/projects/redmine/wiki/RedmineInstall
+* https://www.redmine.org/projects/redmine/wiki/EmailConfiguration
+* https://www.redmine.org/projects/redmine/wiki/RedmineSettings
+* https://www.redmine.org/projects/redmine/wiki/RedmineRepositories
+* https://www.redmine.org/projects/redmine/wiki/RedmineReceivingEmails
+* https://www.redmine.org/projects/redmine/wiki/RedmineReminderEmails
+* https://www.redmine.org/projects/redmine/wiki/RedmineLDAP
diff --git a/doc/UPGRADING b/doc/UPGRADING
index 091b86283..b990ddba2 100644
--- a/doc/UPGRADING
+++ b/doc/UPGRADING
@@ -62,7 +62,7 @@ https://www.redmine.org/
directory for web server delivery.
By default, Redmine automatically recompiles assets in production mode when the application starts.
- This behavior can be controlled using the "config.assets.redmine_detect_update flag" from configuration file.
+ This behavior can be controlled using the "config.assets.redmine_detect_update" flag from configuration file.
To manually compile assets or if automatic compilation is disabled:
@@ -101,4 +101,4 @@ https://www.redmine.org/
== References
-* https://www.redmine.org/wiki/redmine/RedmineUpgrade
+* https://www.redmine.org/projects/redmine/wiki/RedmineUpgrade
diff --git a/lib/plugins/gravatar/lib/gravatar.rb b/lib/plugins/gravatar/lib/gravatar.rb
index 4dc27db52..43820008f 100644
--- a/lib/plugins/gravatar/lib/gravatar.rb
+++ b/lib/plugins/gravatar/lib/gravatar.rb
@@ -32,7 +32,7 @@ module GravatarHelper
:title => '',
# The class to assign to the img tag for the gravatar.
- :class => 'gravatar',
+ :class => 'gravatar avatar',
}
# The methods that will be made available to your views.
@@ -69,7 +69,7 @@ module GravatarHelper
options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
gravatar_api_url(email_hash).tap do |url|
opts = []
- [:rating, :size, :default].each do |opt|
+ [:rating, :size, :default, :initials].each do |opt|
unless options[opt].nil?
value = h(options[opt])
opts << [opt, value].join('=')
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 95b3b7f3f..78a1a6d8c 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -24,11 +24,6 @@ begin
rescue LoadError
# MiniMagick is not available
end
-begin
- require 'commonmarker' unless Object.const_defined?(:Commonmarker)
-rescue LoadError
- # CommonMarker is not available
-end
module Redmine
end
diff --git a/lib/redmine/activity.rb b/lib/redmine/activity.rb
index 826b81c9e..c972eeee2 100644
--- a/lib/redmine/activity.rb
+++ b/lib/redmine/activity.rb
@@ -19,11 +19,11 @@
module Redmine
module Activity
- mattr_accessor :available_event_types, :default_event_types, :plugins_event_types, :providers
+ mattr_accessor :available_event_types, :default_event_types, :plugins_event_classes, :providers
@@available_event_types = []
@@default_event_types = []
- @@plugins_event_types = {}
+ @@plugins_event_classes = {}
@@providers = Hash.new {|h, k| h[k]=[]}
class << self
@@ -41,19 +41,22 @@ module Redmine
@@available_event_types << event_type unless @@available_event_types.include?(event_type)
@@default_event_types << event_type unless options[:default] == false
- @@plugins_event_types = { event_type => options[:plugin].to_s } unless options[:plugin].nil?
+ if options[:plugin]
+ providers.each do |provider|
+ @@plugins_event_classes[provider] = options[:plugin].to_s
+ end
+ end
@@providers[event_type] += providers
end
def delete(event_type)
@@available_event_types.delete event_type
@@default_event_types.delete event_type
- @@plugins_event_types.delete(event_type)
@@providers.delete(event_type)
end
- def plugin_name(event_type)
- @@plugins_event_types[event_type]
+ def plugin_name(class_name)
+ @@plugins_event_classes[class_name.to_s]
end
end
end
diff --git a/lib/redmine/core_ext/string/conversions.rb b/lib/redmine/core_ext/string/conversions.rb
index e98e400e4..bbf57e363 100644
--- a/lib/redmine/core_ext/string/conversions.rb
+++ b/lib/redmine/core_ext/string/conversions.rb
@@ -39,7 +39,7 @@ module Redmine
end
# 2,5 => 2.5
s.tr!(',', '.')
- begin; Kernel.Float(s); rescue; nil; end
+ Kernel.Float(s, exception: false)
end
end
end
diff --git a/lib/redmine/database.rb b/lib/redmine/database.rb
index b3cbdc661..13c92b8a4 100644
--- a/lib/redmine/database.rb
+++ b/lib/redmine/database.rb
@@ -58,7 +58,7 @@ module Redmine
# Returns true if the database is MySQL
def mysql?
- /mysql/i.match?(ActiveRecord::Base.connection.adapter_name)
+ /mysql|trilogy/i.match?(ActiveRecord::Base.connection.adapter_name)
end
def mysql_version
diff --git a/lib/redmine/diff.rb b/lib/redmine/diff.rb
index 40c444a42..c925d463a 100644
--- a/lib/redmine/diff.rb
+++ b/lib/redmine/diff.rb
@@ -76,13 +76,9 @@ module Redmine
def line_to_html_raw(line, offsets)
if offsets
s = +''
- unless offsets.first == 0
- s << CGI.escapeHTML(line[0..offsets.first-1])
- end
+ s << CGI.escapeHTML(line[0..(offsets.first - 1)]) unless offsets.first == 0
s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
- unless offsets.last == -1
- s << CGI.escapeHTML(line[offsets.last+1..-1])
- end
+ s << CGI.escapeHTML(line[(offsets.last + 1)..-1]) unless offsets.last == -1
s
else
CGI.escapeHTML(line)
diff --git a/lib/redmine/field_format.rb b/lib/redmine/field_format.rb
index c4fd1b592..39b21c874 100644
--- a/lib/redmine/field_format.rb
+++ b/lib/redmine/field_format.rb
@@ -110,8 +110,8 @@ module Redmine
end
private_class_method :add
- def self.field_attributes(*args)
- CustomField.store_accessor :format_store, *args
+ def self.field_attributes(*)
+ CustomField.store_accessor(:format_store, *)
end
field_attributes :url_pattern, :full_width_layout
@@ -1086,8 +1086,15 @@ module Redmine
class ProgressbarFormat < Numeric
add 'progressbar'
- self.form_partial = nil
+ self.form_partial = 'custom_fields/formats/progressbar'
self.totalable_supported = false
+ field_attributes :ratio_interval
+
+ # Take the default value from Setting.issue_done_ratio_interval.to_i
+ # in order to have a consistent behaviour for default ratio interval.
+ def self.default_ratio_interval
+ Setting.issue_done_ratio_interval.to_i
+ end
def label
"label_progressbar"
@@ -1112,11 +1119,19 @@ module Redmine
order_statement(custom_field)
end
+ def before_custom_field_save(custom_field)
+ super
+
+ if custom_field.ratio_interval.blank?
+ custom_field.ratio_interval = self.class.default_ratio_interval
+ end
+ end
+
def edit_tag(view, tag_id, tag_name, custom_value, options={})
view.select_tag(
tag_name,
view.options_for_select(
- (0..100).step(Setting.issue_done_ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]},
+ (0..100).step(custom_value.custom_field.ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]},
custom_value.value
),
options.merge(id: tag_id, style: "width: 75px;")
@@ -1124,17 +1139,17 @@ module Redmine
end
def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
- opts = view.options_for_select([[l(:label_no_change_option), '']] + (0..100).step(Setting.issue_done_ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]})
+ opts = view.options_for_select([[l(:label_no_change_option), '']] + (0..100).step(custom_field.ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]})
view.select_tag(tag_name, opts, options.merge(id: tag_id, style: "width: 75px;")) +
bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
end
def formatted_value(view, custom_field, value, customized=nil, html=false)
- text = "#{value}%"
if html
+ text = "#{value}%"
view.progress_bar(value.to_i, legend: (text if view.action_name == 'show'))
else
- text
+ value.to_s
end
end
end
diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb
index 76490f286..d23c40b38 100644
--- a/lib/redmine/helpers/gantt.rb
+++ b/lib/redmine/helpers/gantt.rb
@@ -198,12 +198,18 @@ module Redmine
# Returns the distinct versions of the issues that belong to +project+
def project_versions(project)
- project_issues(project).filter_map(&:fixed_version).uniq
+ @project_versions ||= {}
+ @project_versions[project&.id] ||= begin
+ ids = project_issues(project).filter_map(&:fixed_version_id).uniq
+ Version.where(id: ids).to_a
+ end
end
# Returns the issues that belong to +project+ and are assigned to +version+
def version_issues(project, version)
- project_issues(project).select {|issue| issue.fixed_version == version}
+ @version_issues ||= {}
+ @version_issues[[project&.id, version&.id]] ||=
+ project_issues(project).select {|issue| issue.fixed_version_id == version&.id}
end
def render(options={})
@@ -232,7 +238,7 @@ module Redmine
render_object_row(project, options)
increment_indent(options) do
# render issue that are not assigned to a version
- issues = project_issues(project).select {|i| i.fixed_version.nil?}
+ issues = project_issues(project).select {|i| i.fixed_version_id.nil?}
render_issues(issues, options)
# then render project versions and their issues
versions = project_versions(project)
@@ -400,7 +406,9 @@ module Redmine
MiniMagick.cli_path = File.dirname(Redmine::Configuration['imagemagick_convert_command'])
else
Rails.logger.warn(
- 'imagemagick_convert_command option is ignored because MiniMagick has removed the option to define a custom path for the binary. Please ensure the convert binary is available in your PATH.'
+ 'imagemagick_convert_command option is ignored ' \
+ 'because MiniMagick has removed the option to define a custom path for the binary. ' \
+ 'Please ensure the convert binary is available in your PATH.'
)
end
end
@@ -500,7 +508,7 @@ module Redmine
lines(:image => gc, :top => top, :zoom => zoom,
:subject_width => subject_width, :format => :image)
# today red line
- if User.current.today >= @date_from and User.current.today <= date_to
+ if User.current.today.between?(@date_from, date_to)
gc.stroke('red')
x = (User.current.today - @date_from + 1) * zoom + subject_width
gc.draw('line %g,%g %g,%g' % [
@@ -723,7 +731,7 @@ module Redmine
css_classes = +''
css_classes << ' issue-overdue' if issue.overdue?
css_classes << ' issue-behind-schedule' if issue.behind_schedule?
- css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
+ css_classes << ' icon icon-issue' unless issue.assigned_to
css_classes << ' issue-closed' if issue.closed?
if issue.start_date && issue.due_before && issue.done_ratio
progress_date = calc_progress_date(issue.start_date,
@@ -732,8 +740,8 @@ module Redmine
css_classes << ' over-end-date' if progress_date > self.date_to && issue.done_ratio > 0
end
s = (+"").html_safe
- s << view.sprite_icon('issue').html_safe unless Setting.gravatar_enabled? && issue.assigned_to
- s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar')
+ s << view.sprite_icon('issue').html_safe unless issue.assigned_to
+ s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-avatar')
s << view.link_to_issue(issue).html_safe
s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]',
:value => issue.id, :style => 'display:none;',
@@ -746,7 +754,7 @@ module Redmine
html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " "
html_class << (version.overdue? ? 'version-overdue' : '')
html_class << ' version-closed' unless version.open?
- if version.start_date && version.due_date && version.visible_fixed_issues.completed_percent
+ if version.due_date && version.start_date && version.visible_fixed_issues.completed_percent
progress_date = calc_progress_date(version.start_date,
version.due_date, version.visible_fixed_issues.completed_percent)
html_class << ' behind-start-date' if progress_date < self.date_from
@@ -776,10 +784,14 @@ module Redmine
tag_options[:id] = "issue-#{object.id}"
tag_options[:class] = "issue-subject hascontextmenu"
tag_options[:title] = object.subject
- children = object.leaf? ? [] : object.children & project_issues(object.project)
has_children =
- children.present? &&
- children.collect(&:fixed_version).uniq.intersect?([object.fixed_version])
+ if object.leaf?
+ false
+ else
+ children = object.children & project_issues(object.project)
+ fixed_version_id = object.fixed_version_id
+ children.any? {|child| child.fixed_version_id == fixed_version_id}
+ end
when Version
tag_options[:id] = "version-#{object.id}"
tag_options[:class] = "version-name"
diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb
index dc59819a2..0b31cb235 100644
--- a/lib/redmine/i18n.rb
+++ b/lib/redmine/i18n.rb
@@ -65,9 +65,9 @@ module Redmine
end
# Localizes the given args with user's language
- def lu(user, *args)
+ def lu(user, *)
lang = user.try(:language).presence || Setting.default_language
- ll(lang, *args)
+ ll(lang, *)
end
def format_date(date)
@@ -152,7 +152,7 @@ module Redmine
languages_options :cache => false
end
end
- options.map {|name, lang| [name.force_encoding("UTF-8"), lang.force_encoding("UTF-8")]}
+ options.map {|name, lang| [(+name).force_encoding("UTF-8"), (+lang).force_encoding("UTF-8")]}
end
def find_language(lang)
@@ -173,24 +173,5 @@ module Redmine
def current_language
::I18n.locale
end
-
- # Custom backend based on I18n::Backend::Simple with the following changes:
- # * available_locales are determined by looking at translation file names
- class Backend < ::I18n::Backend::Simple
- module Implementation
- # Get available locales from the translations filenames
- def available_locales
- @available_locales ||= begin
- redmine_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym }
- super & redmine_locales
- end
- end
- end
-
- # Adds custom pluralization rules
- include ::I18n::Backend::Pluralization
- # Adds fallback to default locale for untranslated strings
- include ::I18n::Backend::Fallbacks
- end
end
end
diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb
index 223b3927a..74ae4efaf 100644
--- a/lib/redmine/plugin.rb
+++ b/lib/redmine/plugin.rb
@@ -250,7 +250,7 @@ module Redmine
)
end
elsif req.is_a?(Range)
- unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0
+ unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 # rubocop:disable Style/ComparableBetween
raise PluginRequirementError.new(
"#{id} plugin requires a Redmine version between #{req.first} " \
"and #{req.last} but current is #{current.join('.')}"
@@ -406,8 +406,8 @@ module Redmine
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
#
# Note that :view_scrums permission is required to view these events in the activity view.
- def activity_provider(*args)
- Redmine::Activity.register(*args)
+ def activity_provider(*)
+ Redmine::Activity.register(*)
end
# Registers a wiki formatter.
@@ -423,8 +423,8 @@ module Redmine
# Examples:
# wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter")
#
- def wiki_format_provider(name, *args)
- Redmine::WikiFormatting.register(name, *args)
+ def wiki_format_provider(name, *)
+ Redmine::WikiFormatting.register(name, *)
end
# Register plugin models that use acts_as_attachable.
diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb
index 822662e11..a7387f5dc 100644
--- a/lib/redmine/preparation.rb
+++ b/lib/redmine/preparation.rb
@@ -280,6 +280,11 @@ module Redmine
{:controller => 'auth_sources', :action => 'index'},
:icon => 'server-authentication',
:html => {:class => 'icon icon-server-authentication'}
+ menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'},
+ :if => Proc.new { Setting.rest_api_enabled? },
+ :caption => :'doorkeeper.layouts.admin.nav.applications',
+ :icon => 'apps',
+ :html => {:class => 'icon icon-applications'}
menu.push :plugins, {:controller => 'admin', :action => 'plugins'},
:last => true,
:icon => 'plugins',
@@ -408,9 +413,7 @@ module Redmine
WikiFormatting.map do |format|
format.register :textile
- if Object.const_defined?(:Commonmarker)
- format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)'
- end
+ format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)'
end
ActionView::Template.register_template_handler :rsb, Views::ApiTemplateHandler
diff --git a/lib/redmine/quote_reply.rb b/lib/redmine/quote_reply.rb
index 05737c079..f6d7821cd 100644
--- a/lib/redmine/quote_reply.rb
+++ b/lib/redmine/quote_reply.rb
@@ -20,21 +20,18 @@
module Redmine
module QuoteReply
module Helper
- def javascripts_for_quote_reply_include_tag
- javascript_include_tag 'turndown-7.2.0.min', 'quote_reply'
- end
-
- def quote_reply(url, selector_for_content, icon_only: false)
- quote_reply_function = "quoteReply('#{j url}', '#{j selector_for_content}', '#{j Setting.text_formatting}')"
+ def quote_reply_button(url:, icon_only: false)
+ button_params = {
+ data: {
+ action: 'quote-reply#quote',
+ quote_reply_url_param: url,
+ quote_reply_text_formatting_param: Setting.text_formatting
+ },
+ class: "#{icon_only ? "icon-only" : "icon"} icon-quote"
+ }
+ button_params[:title] = l(:button_quote) if icon_only
- html_options = { class: 'icon icon-comment' }
- html_options[:title] = l(:button_quote) if icon_only
-
- link_to_function(
- sprite_icon('comment', l(:button_quote), icon_only: icon_only),
- quote_reply_function,
- html_options
- )
+ link_to sprite_icon('quote-filled', l(:button_quote), icon_only: icon_only, style: :filled), '#', button_params
end
end
diff --git a/lib/redmine/reaction.rb b/lib/redmine/reaction.rb
new file mode 100644
index 000000000..09fb78ef8
--- /dev/null
+++ b/lib/redmine/reaction.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+module Redmine
+ module Reaction
+ # Types of objects that can have reactions
+ REACTABLE_TYPES = %w(Journal Issue Message News Comment)
+
+ # Returns true if the user can view the reaction of the object
+ def self.visible?(object, user = User.current)
+ Setting.reactions_enabled? && object.visible?(user)
+ end
+
+ # Returns true if the user can add/remove a reaction to/from the object
+ def self.editable?(object, user = User.current)
+ user.logged? && visible?(object, user) && object&.project&.active?
+ end
+
+ module Reactable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :reactions, as: :reactable, dependent: :delete_all
+
+ attr_writer :reaction_detail
+ end
+
+ class_methods do
+ # Preloads reaction details for a collection of objects
+ def preload_reaction_details(objects)
+ return unless Setting.reactions_enabled?
+
+ details = ::Reaction.build_detail_map_for(objects, User.current)
+
+ objects.each do |object|
+ object.reaction_detail = details.fetch(object.id) { ::Reaction::Detail.new }
+ end
+ end
+ end
+
+ def reaction_detail
+ # Loads and returns reaction details if they are not already loaded.
+ # This is intended for cases where explicit preloading is unnecessary,
+ # such as retrieving reactions for a single issue on its detail page.
+ load_reaction_detail unless defined?(@reaction_detail)
+ @reaction_detail
+ end
+
+ def load_reaction_detail
+ self.class.preload_reaction_details([self])
+ end
+ end
+ end
+end
diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb
index 28a1922ca..562dd59d5 100644
--- a/lib/redmine/scm/adapters/mercurial_adapter.rb
+++ b/lib/redmine/scm/adapters/mercurial_adapter.rb
@@ -50,7 +50,10 @@ module Redmine
end
def client_available
- client_version_above?([5, 1])
+ client_version_above?([5, 1]) &&
+ # Redmine >= 6.1 has dropped support for Python 2.7, and
+ # Mercurial has never supported Python 3.0 to 3.4
+ (python_version <=> [3, 5]) >= 0
end
def hgversion
@@ -67,6 +70,23 @@ module Redmine
shellout("#{sq_bin} --version") {|io| io.read}.to_s
end
+ def python_version
+ @@python_version ||= begin
+ debuginstall = hgdebuginstall_from_command_line
+ if (m = debuginstall.match(/checking Python version \(([\d.]+)\)/))
+ m[1].scan(%r{\d+})
+ .collect(&:to_i)
+ .presence
+ else
+ nil
+ end
+ end
+ end
+
+ def hgdebuginstall_from_command_line
+ shellout("#{sq_bin} debuginstall") {|io| io.read}.to_s
+ end
+
def template_path
@@template_path ||= template_path_for(client_version)
end
diff --git a/lib/redmine/sort_criteria.rb b/lib/redmine/sort_criteria.rb
index 461cd3ac1..01cb95871 100644
--- a/lib/redmine/sort_criteria.rb
+++ b/lib/redmine/sort_criteria.rb
@@ -48,8 +48,8 @@ module Redmine
normalize!
end
- def add(*args)
- self.class.new(self).add!(*args)
+ def add(*)
+ self.class.new(self).add!(*)
end
def first_key
diff --git a/lib/redmine/subclass_factory.rb b/lib/redmine/subclass_factory.rb
index 0905f907a..53db6696b 100644
--- a/lib/redmine/subclass_factory.rb
+++ b/lib/redmine/subclass_factory.rb
@@ -38,10 +38,10 @@ module Redmine
end
# Returns an instance of the given subclass name
- def new_subclass_instance(class_name, *args)
+ def new_subclass_instance(class_name, *)
klass = get_subclass(class_name)
if klass
- klass.new(*args)
+ klass.new(*)
end
end
end
diff --git a/lib/redmine/syntax_highlighting.rb b/lib/redmine/syntax_highlighting.rb
index b5785f837..9ae5fed44 100644
--- a/lib/redmine/syntax_highlighting.rb
+++ b/lib/redmine/syntax_highlighting.rb
@@ -125,6 +125,7 @@ module Redmine
'java_script' => 'javascript',
'xhtml' => 'html'
}
+ private_constant :LANG_ALIASES
def find_lexer(language)
::Rouge::Lexer.find(language) ||
diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb
index 31d80381d..79e1b5984 100644
--- a/lib/redmine/version.rb
+++ b/lib/redmine/version.rb
@@ -24,7 +24,7 @@ module Redmine
module VERSION
MAJOR = 6
MINOR = 0
- TINY = 4
+ TINY = 6
# Branch values:
# * official release: nil
diff --git a/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb
new file mode 100644
index 000000000..27429d778
--- /dev/null
+++ b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+module Redmine
+ module WikiFormatting
+ module CommonMark
+ # Defines the mapping from alert type (from CSS class) to SVG icon name.
+ # These icon names must correspond to IDs in your SVG sprite sheet (e.g., icons.svg).
+ ALERT_TYPE_TO_ICON_NAME = {
+ 'note' => 'help',
+ 'tip' => 'bulb',
+ 'warning' => 'warning',
+ 'caution' => 'alert-circle',
+ 'important' => 'message-report',
+ }.freeze
+
+ class AlertsIconsFilter < HTML::Pipeline::Filter
+ def call
+ doc.search("p.markdown-alert-title").each do |node|
+ parent_node = node.parent
+ parent_class_attr = parent_node['class'] # e.g., "markdown-alert markdown-alert-note"
+ next unless parent_class_attr
+
+ # Extract the specific alert type (e.g., "note", "tip", "warning")
+ # from the parent div's classes.
+ match_data = parent_class_attr.match(/markdown-alert-(\w+)/)
+ next unless match_data && match_data[1] # Ensure a type is found
+
+ alert_type = match_data[1]
+
+ # Get the corresponding icon name from our map.
+ icon_name = ALERT_TYPE_TO_ICON_NAME[alert_type]
+ next unless icon_name # Skip if no specific icon is defined for this alert type
+
+ icon_html = ApplicationController.helpers.sprite_icon(icon_name, node.text)
+
+ if icon_html
+ # Replace the existing text node with the icon HTML and label (text).
+ node.children.first.replace(icon_html)
+ end
+ end
+ doc
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb
index aab8eed8b..8b7a18394 100644
--- a/lib/redmine/wiki_formatting/common_mark/formatter.rb
+++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb
@@ -18,7 +18,6 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require 'html/pipeline'
-require 'task_list/filter'
module Redmine
module WikiFormatting
@@ -32,6 +31,10 @@ module Redmine
tagfilter: true,
autolink: true,
footnotes: true,
+ header_ids: nil,
+ tasklist: true,
+ shortcodes: false,
+ alerts: true,
}.freeze,
# https://github.com/gjtorikian/commonmarker#parse-options
@@ -41,7 +44,9 @@ module Redmine
# https://github.com/gjtorikian/commonmarker#render-options
commonmarker_render_options: {
unsafe: true,
+ github_pre_lang: false,
hardbreaks: Redmine::Configuration['common_mark_enable_hardbreaks'] == true,
+ tasklist_classes: true,
}.freeze,
commonmarker_plugins: {
syntax_highlighter: nil
@@ -54,7 +59,7 @@ module Redmine
SyntaxHighlightFilter,
FixupAutoLinksFilter,
ExternalLinksFilter,
- TaskList::Filter
+ AlertsIconsFilter
], PIPELINE_CONFIG
class Formatter
diff --git a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
index cdefc372b..af72adc32 100644
--- a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
+++ b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb
@@ -68,6 +68,26 @@ module Redmine
end
}
+ # Allow class on div and p tags only for alert blocks
+ # (must be exactly: "markdown-alert markdown-alert-*" for div, and "markdown-alert-title" for p)
+ (allowlist[:attributes]["div"] ||= []) << "class"
+ (allowlist[:attributes]["p"] ||= []) << "class"
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+ return unless node.element?
+
+ case node.name
+ when 'div'
+ unless /\Amarkdown-alert markdown-alert-[a-z]+\z/.match?(node['class'])
+ node.remove_attribute('class')
+ end
+ when 'p'
+ unless node['class'] == 'markdown-alert-title'
+ node.remove_attribute('class')
+ end
+ end
+ }
+
# Allow table cell alignment by style attribute
#
# Only necessary if we used the TABLE_PREFER_STYLE_ATTRIBUTES
@@ -78,20 +98,58 @@ module Redmine
# allowlist[:attributes]["td"] = %w(style)
# allowlist[:css] = { properties: ["text-align"] }
- # Allow `id` in a and li elements for footnotes
- # and remove any `id` properties not matching for footnotes
+ # Allow `id` in a elements for footnotes
allowlist[:attributes]["a"].push "id"
- allowlist[:attributes]["li"] = %w(id)
+ # Remove any `id` property not matching for footnotes
allowlist[:transformers].push lambda{|env|
node = env[:node]
- return unless node.name == "a" || node.name == "li"
+ return unless node.name == "a"
return unless node.has_attribute?("id")
- return if node.name == "a" && node["id"] =~ /\Afnref-\d+\z/
- return if node.name == "li" && node["id"] =~ /\Afn-\d+\z/
+ return if node.name == "a" && node["id"] =~ /\Afnref(-\d+){1,2}\z/
node.remove_attribute("id")
}
+ # allow `id` in li element for footnotes
+ # allow `class` in li element for task list items
+ allowlist[:attributes]["li"] = %w(id class)
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+ return unless node.name == "li"
+
+ if node.has_attribute?("id") && !(node["id"] =~ /\Afn-\d+\z/)
+ node.remove_attribute("id")
+ end
+
+ if node.has_attribute?("class") && node["class"] != "task-list-item"
+ node.remove_attribute("class")
+ end
+ }
+
+ # allow input type = "checkbox" with class "task-list-item-checkbox"
+ # for task list items
+ allowlist[:elements].push('input')
+ allowlist[:attributes]["input"] = %w(class type)
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+
+ return unless node.name == "input"
+ return if node['type'] == "checkbox" && node['class'] == "task-list-item-checkbox"
+
+ node.replace(node.children)
+ }
+
+ # allow class "contains-task-list" on ul for task list items
+ allowlist[:attributes]["ul"] = %w(class)
+ allowlist[:transformers].push lambda{|env|
+ node = env[:node]
+
+ return unless node.name == "ul"
+ return if node["class"] == "contains-task-list"
+
+ node.remove_attribute("class")
+ }
+
# https://github.com/rgrove/sanitize/issues/209
allowlist[:protocols].delete("a")
allowlist[:transformers].push lambda{|env|
diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb
index ef1135cb6..16abcb429 100644
--- a/lib/redmine/wiki_formatting/macros.rb
+++ b/lib/redmine/wiki_formatting/macros.rb
@@ -248,7 +248,7 @@ module Redmine
hide_label = args[1] || args[0] || l(:button_hide)
js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
out = ''.html_safe
- out << link_to_function(sprite_icon('angle-right', show_label), js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible')
+ out << link_to_function(sprite_icon('angle-right', show_label, rtl: true), js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible')
out <<
link_to_function(
sprite_icon('angle-down', hide_label), js,
diff --git a/lib/tasks/icons.rake b/lib/tasks/icons.rake
index e50c450a1..269ef43e2 100644
--- a/lib/tasks/icons.rake
+++ b/lib/tasks/icons.rake
@@ -16,7 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
unless Rails.env.production?
- ICON_RELEASE_VERSION = "v3.19.0"
+ ICON_RELEASE_VERSION = "v3.33.0"
ICON_DEFAULT_STYLE = "outline"
SOURCE = URI.parse("https://raw.githubusercontent.com/tabler/tabler-icons/#{ICON_RELEASE_VERSION}/icons")
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index 4a6fd0d30..38d69e7c8 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -43,6 +43,11 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driver_option.add_preference 'download.default_directory', DOWNLOADS_PATH.gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
driver_option.add_preference 'download.prompt_for_download', false
driver_option.add_preference 'plugins.plugins_disabled', ["Chrome PDF Viewer"]
+ # Disable "Change your password" popup shown after login due to leak detection
+ driver_option.add_preference 'profile.password_manager_leak_detection', false
+ # Disable password saving prompts
+ driver_option.add_preference 'profile.password_manager_enabled', false
+ driver_option.add_preference 'credentials_enable_service', false
end
setup do
@@ -68,13 +73,13 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# using default browser locale which depend on system locale for "real" browsers drivers
def log_user(login, password)
visit '/my/page'
- assert_equal '/login', current_path
+ assert_current_path '/login', :ignore_query => true
within('#login-form form') do
fill_in 'username', :with => login
fill_in 'password', :with => password
find('input[name=login]').click
end
- assert_equal '/my/page', current_path
+ assert_current_path '/my/page', :ignore_query => true
end
def wait_for_ajax
diff --git a/test/fixtures/changesets.yml b/test/fixtures/changesets.yml
index 247dda375..8eaca6788 100644
--- a/test/fixtures/changesets.yml
+++ b/test/fixtures/changesets.yml
@@ -102,3 +102,15 @@ changesets_010:
user_id: 3
repository_id: 10
committer: dlopper
+changesets_011:
+ commit_date: "2025-04-07"
+ comments: |-
+ This commit references an issue and a [[wiki]] page
+ Refs #2
+ committed_on: 2025-04-07 19:00:00
+ revision: "11"
+ id: 110
+ scmid:
+ user_id: 3
+ repository_id: 10
+ committer: dlopper
diff --git a/test/fixtures/reactions.yml b/test/fixtures/reactions.yml
new file mode 100644
index 000000000..d8fcbfc1b
--- /dev/null
+++ b/test/fixtures/reactions.yml
@@ -0,0 +1,51 @@
+---
+reaction_001:
+ id: 1
+ reactable_type: Issue
+ reactable_id: 1
+ user_id: 1
+reaction_002:
+ id: 2
+ reactable_type: Issue
+ reactable_id: 1
+ user_id: 2
+reaction_003:
+ id: 3
+ reactable_type: Issue
+ reactable_id: 1
+ user_id: 3
+reaction_004:
+ id: 4
+ reactable_type: Journal
+ reactable_id: 1
+ user_id: 2
+reaction_005:
+ id: 5
+ reactable_type: Issue
+ reactable_id: 6
+ user_id: 2
+reaction_006:
+ id: 6
+ reactable_type: Journal
+ reactable_id: 4
+ user_id: 2
+reaction_007:
+ id: 7
+ reactable_type: News
+ reactable_id: 1
+ user_id: 1
+reaction_008:
+ id: 8
+ reactable_type: Comment
+ reactable_id: 1
+ user_id: 2
+reaction_009:
+ id: 9
+ reactable_type: Message
+ reactable_id: 7
+ user_id: 2
+reaction_010:
+ id: 10
+ reactable_type: News
+ reactable_id: 3
+ user_id: 2
diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb
index 04fdb15d2..c2e7e2f7b 100644
--- a/test/functional/attachments_controller_test.rb
+++ b/test/functional/attachments_controller_test.rb
@@ -42,7 +42,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response :success
assert_equal 'text/html', @response.media_type
- assert_select 'th.filename', :text => /issues_controller.rb\t\(révision 1484\)/
+ assert_select 'th.filename', :text => /issues_controller\.rb \(révision 1484\)/
assert_select 'td.line-code', :text => /Demande créée avec succès/
end
end
@@ -61,7 +61,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response :success
assert_equal 'text/html', @response.media_type
- assert_select 'th.filename', :text => /issues_controller.rb\t\(r\?vision 1484\)/
+ assert_select 'th.filename', :text => /issues_controller\.rb \(r\?vision 1484\)/
assert_select 'td.line-code', :text => /Demande cr\?\?e avec succ\?s/
end
end
@@ -81,7 +81,7 @@ class AttachmentsControllerTest < Redmine::ControllerTest
assert_response :success
assert_equal 'text/html', @response.media_type
- assert_select 'th.filename', :text => /issues_controller.rb\t\(révision 1484\)/
+ assert_select 'th.filename', :text => /issues_controller\.rb \(révision 1484\)/
assert_select 'td.line-code', :text => /Demande créée avec succès/
end
end
diff --git a/test/functional/calendars_controller_test.rb b/test/functional/calendars_controller_test.rb
index 227919435..202c076ac 100644
--- a/test/functional/calendars_controller_test.rb
+++ b/test/functional/calendars_controller_test.rb
@@ -57,7 +57,7 @@ class CalendarsControllerTest < Redmine::ControllerTest
) do
assert_select 'a.issue[href=?]', '/issues/2', :text => 'Feature request #2'
assert_select 'span.tip' do
- assert_select 'img[class="gravatar"]'
+ assert_select 'img[class="gravatar avatar"]'
end
assert_select 'input[name=?][type=?][value=?]', 'ids[]', 'checkbox', '2'
end
diff --git a/test/functional/custom_fields_controller_test.rb b/test/functional/custom_fields_controller_test.rb
index 0706a2eda..fdd2a4148 100644
--- a/test/functional/custom_fields_controller_test.rb
+++ b/test/functional/custom_fields_controller_test.rb
@@ -282,6 +282,23 @@ class CustomFieldsControllerTest < Redmine::ControllerTest
assert_select '[name=?]', 'custom_field[full_width_layout]', 0
end
+ def test_setting_ratio_interval_should_be_present_only_for_progressbar_format
+ get(
+ :new,
+ :params => {
+ :type => 'IssueCustomField',
+ :custom_field => {
+ :field_format => 'progressbar'
+ }
+ }
+ )
+ assert_response :success
+ assert_select '[name=?]', 'custom_field[ratio_interval]' do
+ assert_select 'option[value=?]', '5'
+ assert_select 'option[value=?][selected=?]', '10', 'selected'
+ end
+ end
+
def test_new_js
get(
:new,
diff --git a/test/functional/documents_controller_test.rb b/test/functional/documents_controller_test.rb
index b59ecdc81..944f0b30f 100644
--- a/test/functional/documents_controller_test.rb
+++ b/test/functional/documents_controller_test.rb
@@ -113,9 +113,9 @@ class DocumentsControllerTest < Redmine::ControllerTest
# adds a long description to the first document
doc = documents(:documents_001)
doc.update(:description => <<~LOREM)
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut egestas, mi vehicula varius varius, ipsum massa fermentum orci, eget tristique ante sem vel mi. Nulla facilisi. Donec enim libero, luctus ac sagittis sit amet, vehicula sagittis magna. Duis ultrices molestie ante, eget scelerisque sem iaculis vitae. Etiam fermentum mauris vitae metus pharetra condimentum fermentum est pretium. Proin sollicitudin elementum quam quis pharetra. Aenean facilisis nunc quis elit volutpat mollis. Aenean eleifend varius euismod. Ut dolor est, congue eget dapibus eget, elementum eu odio. Integer et lectus neque, nec scelerisque nisi. EndOfLineHere
- Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Vestibulum non velit mi. Aliquam scelerisque libero ut nulla fringilla a sollicitudin magna rhoncus. Praesent a nunc lorem, ac porttitor eros. Sed ac diam nec neque interdum adipiscing quis quis justo. Donec arcu nunc, fringilla eu dictum at, venenatis ac sem. Vestibulum quis elit urna, ac mattis sapien. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
LOREM
get(:index, :params => {:project_id => 'ecookbook'})
assert_response :success
diff --git a/test/functional/gantts_controller_test.rb b/test/functional/gantts_controller_test.rb
index daba816b8..73a74ba65 100644
--- a/test/functional/gantts_controller_test.rb
+++ b/test/functional/gantts_controller_test.rb
@@ -58,7 +58,7 @@ class GanttsControllerTest < Redmine::ControllerTest
# Assert context menu on issues subject and gantt bar
assert_select 'div[class=?]', 'issue-subject hascontextmenu'
assert_select 'div.tooltip.hascontextmenu' do
- assert_select 'img[class="gravatar"]'
+ assert_select 'img[class="gravatar avatar"]'
end
assert_select "form[data-cm-url=?]", '/issues/context_menu'
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index 5af5b2797..4b9b44537 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -1737,7 +1737,7 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'td.last_notes[colspan="4"]', :text => 'Some notes with Redmine links: #2, r2.'
assert_select(
'td.last_notes[colspan="4"]',
- :text => 'A comment with inline image: and a reference to #1 and r2.'
+ :text => 'A comment with inline image: and a reference to #1 and r2.'
)
get(
:index,
@@ -2485,7 +2485,7 @@ class IssuesControllerTest < Redmine::ControllerTest
end
assert_select 'div#tab-content-history' do
assert_select 'div[id=?]', "change-#{Issue.find(1).journals.last.id}" do
- assert_select 'ul.details', :text => "Subtask ##{issue.id} added"
+ assert_select 'ul.journal-details', :text => "Subtask ##{issue.id} added"
end
end
end
@@ -2816,7 +2816,7 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0}
end
- def test_show_should_display_watchers_with_gravatars
+ def test_show_should_display_watchers_with_avatars
@request.session[:user_id] = 2
issue = Issue.find(1)
issue.add_watcher User.find(2)
@@ -2824,9 +2824,10 @@ class IssuesControllerTest < Redmine::ControllerTest
with_settings :gravatar_enabled => '1' do
get(:show, :params => {:id => 1})
end
+
assert_select 'div#watchers ul' do
assert_select 'li.user-2' do
- assert_select 'img.gravatar[title=?]', 'John Smith'
+ assert_select '.avatar[title=?]', 'John Smith'
assert_select 'a[href="/users/2"]'
assert_select 'a[class*=delete]'
end
@@ -3265,6 +3266,22 @@ class IssuesControllerTest < Redmine::ControllerTest
end
end
+ def test_show_render_changeset_comments_in_original_context
+ issue = Issue.find(9)
+ issue.changeset_ids = [110]
+ issue.save!
+
+ @request.session[:user_id] = 2
+ get :issue_tab, params: {id: issue.id, name: 'changesets', format: 'js'}, xhr: true
+
+ assert_select 'div#changeset-110' do
+ # assert_select 'div.tabs a[id=?]', 'tab-changesets', text: 'unicorns'
+ assert_select 'div.changeset-comments' do
+ assert_select 'a[href=?]', '/projects/ecookbook/wiki/Wiki', text: 'wiki'
+ end
+ end
+ end
+
def test_show_should_display_spent_time_tab_for_issue_with_time_entries
@request.session[:user_id] = 1
get :show, :params => {:id => 3}
@@ -3289,7 +3306,7 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'a[title=?][href=?]', 'Edit', '/time_entries/3/edit'
assert_select 'a[title=?][href=?]', 'Delete', '/time_entries/3'
- assert_select 'ul[class=?]', 'details', :text => /1.00 h/
+ assert_select 'ul[class=?]', 'journal-details', :text => /1.00 h/
end
end
@@ -3315,6 +3332,42 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'span.badge.badge-private', text: 'Private'
end
+ def test_show_should_display_reactions
+ current_user = User.generate!
+
+ User.add_to_project(current_user, projects(:projects_001),
+ Role.generate!(users_visibility: 'members_of_visible_projects', permissions: [:view_issues]))
+
+ @request.session[:user_id] = current_user.id
+
+ get :show, params: { id: 1 }
+
+ assert_response :success
+
+ assert_select 'span[data-reaction-button-id=reaction_issue_1]' do
+ # The current_user can only see members who belong to projects that the current_user has access to.
+ # Since the Redmine Admin user does not belong to any projects visible to the current_user,
+ # the Redmine Admin user's name is not displayed in the reaction user list. Instead, "1 other" is shown.
+ assert_select 'a.reaction-button[title=?]', 'Dave Lopper and John Smith' do
+ assert_select 'span.icon-label', '2'
+ end
+ end
+
+ assert_select 'span[data-reaction-button-id=reaction_journal_1]' do
+ assert_select 'a.reaction-button[title=?]', 'John Smith'
+ end
+ assert_select 'span[data-reaction-button-id=reaction_journal_2] a.reaction-button'
+ end
+
+ def test_should_not_display_reactions_when_reactions_feature_is_disabled
+ with_settings reactions_enabled: '0' do
+ get :show, params: { id: 1 }
+
+ assert_response :success
+ assert_select 'span[data-reaction-button-id]', false
+ end
+ end
+
def test_show_should_not_display_edit_attachment_icon_for_user_without_edit_issue_permission_on_tracker
role = Role.find(2)
role.set_permission_trackers 'edit_issues', [2, 3]
@@ -5980,6 +6033,16 @@ class IssuesControllerTest < Redmine::ControllerTest
end
end
+ def test_get_edit_with_custom_field_progress_bar
+ cf = IssueCustomField.generate!(:tracker_ids => [1], :is_for_all => true, :field_format => 'progressbar')
+
+ @request.session[:user_id] = 1
+ get(:edit, :params => {:id => 1})
+ assert_response :success
+
+ assert_select "select[id=?]", "issue_custom_field_values_#{cf.id}", 1
+ end
+
def test_get_edit_with_me_assigned_to_id
@request.session[:user_id] = 2
get(
@@ -8635,7 +8698,7 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'div#tab-content-history' do
assert_select 'div[id=?]', "change-#{parent.journals.last.id}" do
- assert_select 'ul.details', :text => "Subtask deleted (##{child.id})"
+ assert_select 'ul.journal-details', :text => "Subtask deleted (##{child.id})"
end
end
end
@@ -8724,31 +8787,27 @@ class IssuesControllerTest < Redmine::ControllerTest
assert_select 'a[href=?][onclick=?]', "/issues/1", "", :text => 'Cancel'
end
- def test_show_should_display_author_gravatar_only_when_not_assigned
+ def test_show_should_display_author_avatar_only_when_not_assigned
issue = Issue.find(1)
assert_nil issue.assigned_to_id
@request.session[:user_id] = 1
- with_settings :gravatar_enabled => '1' do
- get :show, :params => {:id => issue.id}
- assert_select 'div.gravatar-with-child' do
- assert_select 'img.gravatar', 1
- end
+ get :show, :params => {:id => issue.id}
+ assert_select 'div.avatar-with-child' do
+ assert_select '.avatar', 1
end
end
- def test_show_should_display_author_and_assignee_gravatars_when_assigned
+ def test_show_should_display_author_and_assignee_avatars_when_assigned
issue = Issue.find(1)
issue.assigned_to_id = 2
issue.save!
@request.session[:user_id] = 1
- with_settings :gravatar_enabled => '1' do
- get :show, :params => {:id => issue.id}
- assert_select 'div.gravatar-with-child' do
- assert_select 'img.gravatar', 2
- assert_select 'img.gravatar-child', 1
- end
+ get :show, :params => {:id => issue.id}
+ assert_select 'div.avatar-with-child' do
+ assert_select '.avatar', 2
+ assert_select '.avatar-child', 1
end
end
diff --git a/test/functional/issues_custom_fields_visibility_test.rb b/test/functional/issues_custom_fields_visibility_test.rb
index 3955ef861..6bcf2ad4f 100644
--- a/test/functional/issues_custom_fields_visibility_test.rb
+++ b/test/functional/issues_custom_fields_visibility_test.rb
@@ -101,9 +101,9 @@ class IssuesCustomFieldsVisibilityTest < Redmine::ControllerTest
get(:show, :params => {:id => @issue.id})
@fields.each_with_index do |field, i|
if fields.include?(field)
- assert_select 'ul.details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change"
+ assert_select 'ul.journal-details i', {:text => "Value#{i}", :count => 1}, "User #{user.id} was not able to view #{field.name} change"
else
- assert_select 'ul.details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change"
+ assert_select 'ul.journal-details i', {:text => "Value#{i}", :count => 0}, "User #{user.id} was able to view #{field.name} change"
end
end
end
diff --git a/test/functional/messages_controller_test.rb b/test/functional/messages_controller_test.rb
index 74b9a3070..cec58ff5f 100644
--- a/test/functional/messages_controller_test.rb
+++ b/test/functional/messages_controller_test.rb
@@ -28,7 +28,7 @@ class MessagesControllerTest < Redmine::ControllerTest
get(:show, :params => {:board_id => 1, :id => 1})
assert_response :success
- assert_select 'h2', :text => 'First post'
+ assert_select 'h2', :text => "RAFirst post"
end
def test_show_should_contain_reply_field_tags_for_quoting
@@ -123,6 +123,27 @@ class MessagesControllerTest < Redmine::ControllerTest
assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0}
end
+ def test_show_should_display_reactions
+ @request.session[:user_id] = 2
+
+ get :show, params: { board_id: 1, id: 4 }
+
+ assert_response :success
+ assert_select 'span[data-reaction-button-id=reaction_message_4] a.reaction-button' do
+ assert_select 'svg use[href*=thumb-up]'
+ end
+ assert_select 'span[data-reaction-button-id=reaction_message_5] a.reaction-button'
+ assert_select 'span[data-reaction-button-id=reaction_message_6] a.reaction-button'
+
+ # Should not display reactions when reactions feature is disabled.
+ with_settings reactions_enabled: '0' do
+ get :show, params: { board_id: 1, id: 4 }
+
+ assert_response :success
+ assert_select 'span[data-reaction-button-id]', false
+ end
+ end
+
def test_get_new
@request.session[:user_id] = 2
get(:new, :params => {:board_id => 1})
diff --git a/test/functional/news_controller_test.rb b/test/functional/news_controller_test.rb
index f1ddfff71..686fada25 100644
--- a/test/functional/news_controller_test.rb
+++ b/test/functional/news_controller_test.rb
@@ -75,7 +75,7 @@ class NewsControllerTest < Redmine::ControllerTest
get(:show, :params => {:id => 1})
assert_response :success
assert_select 'p.breadcrumb a[href=?]', '/projects/ecookbook/news', :text => 'News'
- assert_select 'h2', :text => 'eCookbook first release !'
+ assert_select 'h2', :text => 'JS eCookbook first release !'
end
def test_show_should_show_attachments
@@ -106,6 +106,23 @@ class NewsControllerTest < Redmine::ControllerTest
assert_response :not_found
end
+ def test_show_should_display_reactions
+ @request.session[:user_id] = 1
+
+ get :show, params: { id: 1 }
+ assert_response :success
+ assert_select 'span[data-reaction-button-id=reaction_news_1] a.reaction-button.reacted'
+ assert_select 'span[data-reaction-button-id=reaction_comment_1] a.reaction-button'
+
+ # Should not display reactions when reactions feature is disabled.
+ with_settings reactions_enabled: '0' do
+ get :show, params: { id: 1 }
+
+ assert_response :success
+ assert_select 'span[data-reaction-button-id]', false
+ end
+ end
+
def test_get_new_with_project_id
@request.session[:user_id] = 2
get(:new, :params => {:project_id => 1})
diff --git a/test/functional/reactions_controller_test.rb b/test/functional/reactions_controller_test.rb
new file mode 100644
index 000000000..b65794969
--- /dev/null
+++ b/test/functional/reactions_controller_test.rb
@@ -0,0 +1,394 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../test_helper'
+
+class ReactionsControllerTest < Redmine::ControllerTest
+ setup do
+ Setting.reactions_enabled = '1'
+ # jsmith
+ @request.session[:user_id] = users(:users_002).id
+ end
+
+ teardown do
+ Setting.clear_cache
+ end
+
+ test 'create for issue' do
+ issue = issues(:issues_002)
+
+ assert_difference(
+ ->{ Reaction.count } => 1,
+ ->{ issue.reactions.by(users(:users_002)).count } => 1
+ ) do
+ post :create, params: {
+ object_type: 'Issue',
+ object_id: issue.id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'create for journal' do
+ journal = journals(:journals_005)
+
+ assert_difference(
+ ->{ Reaction.count } => 1,
+ ->{ journal.reactions.by(users(:users_002)).count } => 1
+ ) do
+ post :create, params: {
+ object_type: 'Journal',
+ object_id: journal.id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'create for news' do
+ news = news(:news_002)
+
+ assert_difference(
+ ->{ Reaction.count } => 1,
+ ->{ news.reactions.by(users(:users_002)).count } => 1
+ ) do
+ post :create, params: {
+ object_type: 'News',
+ object_id: news.id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'create reaction for comment' do
+ comment = comments(:comments_002)
+
+ assert_difference(
+ ->{ Reaction.count } => 1,
+ ->{ comment.reactions.by(users(:users_002)).count } => 1
+ ) do
+ post :create, params: {
+ object_type: 'Comment',
+ object_id: comment.id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'create for message' do
+ message = messages(:messages_001)
+
+ assert_difference(
+ ->{ Reaction.count } => 1,
+ ->{ message.reactions.by(users(:users_002)).count } => 1
+ ) do
+ post :create, params: {
+ object_type: 'Message',
+ object_id: message.id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'destroy for issue' do
+ reaction = reactions(:reaction_005)
+
+ assert_difference 'Reaction.count', -1 do
+ delete :destroy, params: {
+ id: reaction.id,
+ # Issue (id=6)
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ assert_not Reaction.exists?(reaction.id)
+ end
+
+ test 'destroy for journal' do
+ reaction = reactions(:reaction_006)
+
+ assert_difference 'Reaction.count', -1 do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ assert_not Reaction.exists?(reaction.id)
+ end
+
+ test 'destroy for news' do
+ # For News(id=3)
+ reaction = reactions(:reaction_010)
+
+ assert_difference 'Reaction.count', -1 do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ assert_not Reaction.exists?(reaction.id)
+ end
+
+ test 'destroy for comment' do
+ # For Comment(id=1)
+ reaction = reactions(:reaction_008)
+
+ assert_difference 'Reaction.count', -1 do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ assert_not Reaction.exists?(reaction.id)
+ end
+
+ test 'destroy for message' do
+ reaction = reactions(:reaction_009)
+
+ assert_difference 'Reaction.count', -1 do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ assert_not Reaction.exists?(reaction.id)
+ end
+
+ test 'create should respond with 403 when feature is disabled' do
+ Setting.reactions_enabled = '0'
+ # admin
+ @request.session[:user_id] = users(:users_001).id
+
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Issue',
+ object_id: issues(:issues_002).id
+ }, xhr: true
+ end
+ assert_response :forbidden
+ end
+
+ test 'destroy should respond with 403 when feature is disabled' do
+ Setting.reactions_enabled = '0'
+ # admin
+ @request.session[:user_id] = users(:users_001).id
+
+ reaction = reactions(:reaction_001)
+ assert_no_difference 'Reaction.count' do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+ assert_response :forbidden
+ end
+
+ test 'create by anonymou user should respond with 401 when feature is disabled' do
+ Setting.reactions_enabled = '0'
+ @request.session[:user_id] = nil
+
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Issue',
+ object_id: issues(:issues_002).id
+ }, xhr: true
+ end
+ assert_response :unauthorized
+ end
+
+ test 'create by anonymous user should respond with 401' do
+ @request.session[:user_id] = nil
+
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Issue',
+ # Issue(id=1) is an issue in a public project
+ object_id: issues(:issues_001).id
+ }, xhr: true
+ end
+
+ assert_response :unauthorized
+ end
+
+ test 'destroy by anonymous user should respond with 401' do
+ @request.session[:user_id] = nil
+
+ reaction = reactions(:reaction_002)
+ assert_no_difference 'Reaction.count' do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :unauthorized
+ end
+
+ test 'create when reaction already exists should not create a new reaction and succeed' do
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Comment',
+ # user(jsmith) has already reacted to Comment(id=1)
+ object_id: comments(:comments_001).id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'destroy another user reaction should not destroy the reaction and succeed' do
+ # admin user's reaction
+ reaction = reactions(:reaction_001)
+
+ assert_no_difference 'Reaction.count' do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'destroy nonexistent reaction' do
+ # For Journal(id=4)
+ reaction = reactions(:reaction_006)
+ reaction.destroy!
+
+ assert_not Reaction.exists?(reaction.id)
+
+ assert_no_difference 'Reaction.count' do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :success
+ end
+
+ test 'create with invalid object type should respond with 403' do
+ # admin
+ @request.session[:user_id] = users(:users_001).id
+
+ post :create, params: {
+ object_type: 'InvalidType',
+ object_id: 1
+ }, xhr: true
+
+ assert_response :forbidden
+ end
+
+ test 'create without permission to view should respond with 403' do
+ # dlopper
+ @request.session[:user_id] = users(:users_003).id
+
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Issue',
+ # dlopper is not a member of the project where the issue (id=4) belongs.
+ object_id: issues(:issues_004).id
+ }, xhr: true
+ end
+
+ assert_response :forbidden
+ end
+
+ test 'destroy without permission to view should respond with 403' do
+ # dlopper
+ @request.session[:user_id] = users(:users_003).id
+
+ # For Issue(id=6)
+ reaction = reactions(:reaction_005)
+
+ assert_no_difference 'Reaction.count' do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :forbidden
+ end
+
+ test 'create should respond with 404 for non-JS requests' do
+ issue = issues(:issues_002)
+
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Issue',
+ object_id: issue.id
+ } # Sending an HTML request by omitting xhr: true
+ end
+
+ assert_response :not_found
+ end
+
+ test 'create should respond with 403 when project is closed' do
+ issue = issues(:issues_010)
+ issue.project.update!(status: Project::STATUS_CLOSED)
+
+ assert_no_difference 'Reaction.count' do
+ post :create, params: {
+ object_type: 'Issue',
+ object_id: issue.id
+ }, xhr: true
+ end
+
+ assert_response :forbidden
+ end
+
+ test 'destroy should respond with 403 when project is closed' do
+ reaction = reactions(:reaction_005)
+ reaction.reactable.project.update!(status: Project::STATUS_CLOSED)
+
+ assert_no_difference 'Reaction.count' do
+ delete :destroy, params: {
+ id: reaction.id,
+ object_type: reaction.reactable_type,
+ object_id: reaction.reactable_id
+ }, xhr: true
+ end
+
+ assert_response :forbidden
+ end
+end
diff --git a/test/functional/repositories_bazaar_controller_test.rb b/test/functional/repositories_bazaar_controller_test.rb
index 2122bf3b9..34e70348d 100644
--- a/test/functional/repositories_bazaar_controller_test.rb
+++ b/test/functional/repositories_bazaar_controller_test.rb
@@ -37,6 +37,7 @@ class RepositoriesBazaarControllerTest < Redmine::RepositoryControllerTest
:log_encoding => 'UTF-8'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
if File.directory?(REPOSITORY_PATH)
diff --git a/test/functional/repositories_controller_test.rb b/test/functional/repositories_controller_test.rb
index beae44f68..7937e59e6 100644
--- a/test/functional/repositories_controller_test.rb
+++ b/test/functional/repositories_controller_test.rb
@@ -186,6 +186,7 @@ class RepositoriesControllerTest < Redmine::RepositoryControllerTest
def test_show_without_main_repository_should_display_first_repository
skip unless repository_configured?('subversion')
+ skip unless Repository::Subversion.scm_available
project = Project.find(1)
repos = project.repositories
@@ -208,6 +209,7 @@ class RepositoriesControllerTest < Redmine::RepositoryControllerTest
def test_show_should_show_diff_button_depending_on_browse_repository_permission
skip unless repository_configured?('subversion')
+ skip unless Repository::Subversion.scm_available
@request.session[:user_id] = 2
role = Role.find(1)
diff --git a/test/functional/repositories_cvs_controller_test.rb b/test/functional/repositories_cvs_controller_test.rb
index 558e58d0a..bb30ebc19 100644
--- a/test/functional/repositories_cvs_controller_test.rb
+++ b/test/functional/repositories_cvs_controller_test.rb
@@ -40,6 +40,7 @@ class RepositoriesCvsControllerTest < Redmine::RepositoryControllerTest
:url => MODULE_NAME,
:log_encoding => 'UTF-8')
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
if File.directory?(REPOSITORY_PATH)
diff --git a/test/functional/repositories_git_controller_test.rb b/test/functional/repositories_git_controller_test.rb
index 32fa90e29..cb8788a03 100644
--- a/test/functional/repositories_git_controller_test.rb
+++ b/test/functional/repositories_git_controller_test.rb
@@ -41,6 +41,7 @@ class RepositoriesGitControllerTest < Redmine::RepositoryControllerTest
:path_encoding => 'ISO-8859-1'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
def test_create_and_update
diff --git a/test/functional/repositories_mercurial_controller_test.rb b/test/functional/repositories_mercurial_controller_test.rb
index 4af2fcf99..f5f0e034f 100644
--- a/test/functional/repositories_mercurial_controller_test.rb
+++ b/test/functional/repositories_mercurial_controller_test.rb
@@ -37,6 +37,8 @@ class RepositoriesMercurialControllerTest < Redmine::RepositoryControllerTest
:path_encoding => 'ISO-8859-1'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
+
@diff_c_support = true
end
diff --git a/test/functional/repositories_subversion_controller_test.rb b/test/functional/repositories_subversion_controller_test.rb
index 60dd213e4..0a430317f 100644
--- a/test/functional/repositories_subversion_controller_test.rb
+++ b/test/functional/repositories_subversion_controller_test.rb
@@ -34,6 +34,7 @@ class RepositoriesSubversionControllerTest < Redmine::RepositoryControllerTest
@repository = Repository::Subversion.create(:project => @project,
:url => self.class.subversion_repository_url)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
if repository_configured?('subversion')
@@ -357,6 +358,27 @@ class RepositoriesSubversionControllerTest < Redmine::RepositoryControllerTest
assert_equal "attachment; filename=\"helloworld.c\"; filename*=UTF-8''helloworld.c", @response.headers['Content-Disposition']
end
+ def test_entry_should_return_text_plain_for_js_files
+ # JavaScript files should be served as 'text/plain' instead of
+ # 'application/javascript' to avoid
+ # ActionController::InvalidCrossOriginRequest exception
+ assert_equal 0, @repository.changesets.count
+ @repository.fetch_changesets
+ @project.reload
+ assert_equal NUM_REV, @repository.changesets.count
+ get(
+ :raw,
+ :params => {
+ :id => PRJ_ID,
+ :repository_id => @repository.id,
+ :path => repository_path_hash(['subversion_test', 'foo.js'])[:param]
+ }
+ )
+ assert_response :success
+ assert_equal 'text/plain', @response.media_type
+ assert_match /attachment/, @response.headers['Content-Disposition']
+ end
+
def test_directory_entry
assert_equal 0, @repository.changesets.count
@repository.fetch_changesets
diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb
index c343b9bd6..cbb2da3de 100644
--- a/test/functional/roles_controller_test.rb
+++ b/test/functional/roles_controller_test.rb
@@ -239,11 +239,33 @@ class RolesControllerTest < Redmine::ControllerTest
assert_nil Role.find_by_id(r.id)
end
- def test_destroy_role_in_use
- delete :destroy, :params => {:id => 1}
- assert_redirected_to '/roles'
- assert_equal 'This role is in use and cannot be deleted.', flash[:error]
- assert_not_nil Role.find_by_id(1)
+ def test_destroy_role_with_members
+ role = Role.find(2) # Developer, has members
+
+ delete :destroy, params: { id: role.id }
+
+ assert_redirected_to roles_path
+ assert Role.find_by(id: role.id)
+
+ assert flash[:error].present?
+ assert_includes flash[:error], I18n.t(:error_can_not_remove_role)
+
+ expected_dependency_projects = Project.where(identifier: ['ecookbook', 'onlinestore', 'private-child'])
+ expected_dependency_projects.each do |project|
+ assert_includes flash[:error], project.name
+ assert_includes flash[:error], settings_project_path(project, tab: 'members')
+ end
+ end
+
+ def test_destroy_builtin
+ role = Role.anonymous
+
+ delete :destroy, params: { id: role.id }
+
+ assert_redirected_to roles_path
+ assert Role.find_by(id: role.id)
+ assert flash[:error].present?
+ assert_equal flash[:error], I18n.t(:error_can_not_remove_role)
end
def test_permissions
diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb
index 1a624c3c7..5e32e5656 100644
--- a/test/functional/search_controller_test.rb
+++ b/test/functional/search_controller_test.rb
@@ -66,16 +66,18 @@ class SearchControllerTest < Redmine::ControllerTest
assert_response :success
assert_select '#search-results' do
- assert_select 'dt.issue a', :text => /Feature request #2/
+ assert_select 'dt.issue a', :text => /Bug #1/
assert_select 'dt.issue a', :text => /Bug #5/
assert_select 'dt.changeset a', :text => /Revision 1/
- assert_select 'dt.issue a', :text => /Add ingredients categories/
- assert_select 'dd', :text => /should be classified by categories/
+ assert_select 'dt.issue a', :text => /Cannot print recipes/
+ assert_select 'dd', :text => /Unable to print/
end
assert_select '#search-results-counts' do
- assert_select 'a', :text => 'Changesets (5)'
+ assert_select 'a', :text => 'Changesets (6)'
+ assert_select 'a', :text => 'Issues (5)'
+ assert_select 'a', :text => 'Projects (4)'
end
end
diff --git a/test/functional/workflows_controller_test.rb b/test/functional/workflows_controller_test.rb
index b30559d80..dcdc8d5bb 100644
--- a/test/functional/workflows_controller_test.rb
+++ b/test/functional/workflows_controller_test.rb
@@ -211,6 +211,45 @@ class WorkflowsControllerTest < Redmine::ControllerTest
assert w.assignee
end
+ def test_post_edit_with_large_number_of_statuses
+ # This test ensures that workflows with many statuses can be saved.
+ # Without setting `ENV['RACK_QUERY_PARSER_PARAMS_LIMIT']`, this raises
+ # ActionController::BadRequest exception due to exceeding the default
+ # query parameter limit of 4096.
+ WorkflowTransition.delete_all
+
+ num_statuses = 40
+ transitions_data = {}
+
+ # Allowed statuses for a new issue (status_id = 0)
+ transitions_data['0'] = {}
+ (1..num_statuses).each do |status_id|
+ transitions_data['0'][status_id.to_s] = {'always' => '1'}
+ end
+
+ # Status transitions between statuses
+ (1..num_statuses).each do |status_id_from| # rubocop:disable RuboCopStyle/CombinableLoops
+ transitions_data[status_id_from.to_s] = {}
+ (1..num_statuses).each do |status_id_to|
+ # skip self-transitions
+ next if status_id_from == status_id_to
+
+ transitions_data[status_id_from.to_s][status_id_to.to_s] = {
+ 'always' => '1', 'author' => '1', 'assignee' => '1'
+ }
+ end
+ end
+
+ assert_nothing_raised do
+ patch :update, :params => {
+ :role_id => 2,
+ :tracker_id => 1,
+ :transitions => transitions_data
+ }
+ end
+ assert_response :found
+ end
+
def test_get_permissions
get :permissions
diff --git a/test/generators/controller_generator_test.rb b/test/generators/controller_generator_test.rb
new file mode 100644
index 000000000..46aeac88e
--- /dev/null
+++ b/test/generators/controller_generator_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../test_helper'
+require_relative '../../lib/generators/redmine_plugin_controller/redmine_plugin_controller_generator'
+
+class ControllerGeneratorTest < Rails::Generators::TestCase
+ TMP_DIR = Rails.root / 'tmp/test/generators'
+
+ tests RedminePluginControllerGenerator
+ destination TMP_DIR
+ setup :prepare_destination
+
+ setup do
+ @plugin_directory = Redmine::Plugin.directory
+ Redmine::Plugin.directory = TMP_DIR
+ end
+
+ teardown do
+ Redmine::Plugin.directory = @plugin_directory
+ end
+
+ def test_generates_files_from_templates
+ g = generator ['ControllerDemo', 'Todo']
+
+ assert_name g, 'Todo', :controller
+
+ capture(:stdout) do
+ g.copy_templates
+ end
+
+ controller_path_names = (Redmine::Plugin.directory / 'controller_demo/app/controllers')
+ .glob('*.rb')
+ assert_equal 1, controller_path_names.count
+ assert_equal 'todo_controller.rb', controller_path_names.first.basename.to_s
+
+ helper_path_names = (Redmine::Plugin.directory / 'controller_demo/app/helpers')
+ .glob('*.rb')
+ assert_equal 1, helper_path_names.count
+ assert_equal 'todo_helper.rb', helper_path_names.first.basename.to_s
+
+ test_path_names = (Redmine::Plugin.directory / 'controller_demo/test/functional')
+ .glob('*.rb')
+ assert_equal 1, test_path_names.count
+ assert_equal 'todo_controller_test.rb', test_path_names.first.basename.to_s
+ end
+
+ private
+
+ def assert_name(generator, value, method)
+ assert_equal value, generator.send(method)
+ end
+end
diff --git a/test/generators/model_generator_test.rb b/test/generators/model_generator_test.rb
new file mode 100644
index 000000000..f030de6f7
--- /dev/null
+++ b/test/generators/model_generator_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../test_helper'
+require_relative '../../lib/generators/redmine_plugin_model/redmine_plugin_model_generator'
+
+class ModelGeneratorTest < Rails::Generators::TestCase
+ TMP_DIR = Rails.root / 'tmp/test/generators'
+
+ tests RedminePluginModelGenerator
+ destination TMP_DIR
+ setup :prepare_destination
+
+ setup do
+ @plugin_directory = Redmine::Plugin.directory
+ Redmine::Plugin.directory = TMP_DIR
+ end
+
+ teardown do
+ Redmine::Plugin.directory = @plugin_directory
+ end
+
+ def test_generates_files_from_templates
+ g = generator ['ModelDemo', 'TodoModel']
+
+ assert_name g, 'TodoModel', :model
+
+ capture(:stdout) do
+ g.copy_templates
+ end
+
+ model_path_names = (Redmine::Plugin.directory / 'model_demo/app/models')
+ .glob('*.rb')
+ assert_equal 1, model_path_names.count
+ assert_equal 'todo_model.rb', model_path_names.first.basename.to_s
+
+ test_path_names = (Redmine::Plugin.directory / 'model_demo/test/unit')
+ .glob('*.rb')
+ assert_equal 1, test_path_names.count
+ assert_equal 'todo_model_test.rb', test_path_names.first.basename.to_s
+
+ migration_path_names = (Redmine::Plugin.directory / 'model_demo/db/migrate')
+ .glob('*.rb')
+ assert_equal 1, migration_path_names.count
+ assert_match(/\d+_create_todo_models\.rb/, migration_path_names.first.basename.to_s)
+ end
+
+ private
+
+ def assert_name(generator, value, method)
+ assert_equal value, generator.send(method)
+ end
+end
diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb
index f959744e2..1f60bbbe2 100644
--- a/test/helpers/application_helper_test.rb
+++ b/test/helpers/application_helper_test.rb
@@ -1732,6 +1732,46 @@ class ApplicationHelperTest < Redmine::HelperTest
end
end
+ def test_section_edit_links_with_multiline_heading
+ raw = <<~RAW
+ # Wiki
+
+ ## `Foo` Bar
+
+ The heading above generates multiline HTML.
+ Don't assume heading tags are always single-line.
+
+ ```
+ <h2>
+ <code>Foo</code> Bar</h2>
+ ```
+ RAW
+ @project = Project.find(1)
+ set_language_if_valid 'en'
+ with_settings :text_formatting => 'common_mark' do
+ result =
+ textilizable(
+ raw,
+ :edit_section_links =>
+ {:controller => 'wiki', :action => 'edit',
+ :project_id => '1', :id => 'Test'}
+ ).delete("\n")
+
+ assert_match(
+ Regexp.new(
+ '<div class="contextual heading-2" title="Edit this section" id="section-2">' \
+ '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=2">' \
+ '<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-.*\.svg#icon--edit"></use></svg>' \
+ '<span class="icon-label">Edit this section</span>' \
+ '</a></div>' \
+ '<a name="Foo-Bar"></a>' \
+ '<h2 ><code>Foo</code> Bar<a href="#Foo-Bar" class="wiki-anchor">&para;</a></h2>'
+ ),
+ result
+ )
+ end
+ end
+
def test_default_formatter
with_settings :text_formatting => 'unknown' do
text = 'a *link*: http://www.example.net/'
@@ -2013,20 +2053,6 @@ class ApplicationHelperTest < Redmine::HelperTest
end
end
- def test_principals_check_box_tag_without_avatar
- principals = [User.find(1), Group.find(10)]
- Setting.gravatar_enabled = '1'
- avatar_tags = principals.collect{|p| avatar(p, :size => 16)}
-
- with_settings :gravatar_enabled => '0' do
- tags = principals_check_box_tags(name, principals)
- principals.each_with_index do |principal, i|
- assert_not_include avatar_tags[i], tags
- assert_include content_tag('span', principal_icon(principal), :class => "name icon icon-#{principal.class.name.downcase}"), tags
- end
- end
- end
-
def test_principals_options_for_select_with_users
User.current = nil
users = [User.find(2), User.find(4)]
@@ -2363,6 +2389,14 @@ class ApplicationHelperTest < Redmine::HelperTest
assert_equal expected, format_activity_description(text)
end
+ def test_render_flash_messages_should_ignore_non_string_values
+ flash[:array_value] = ['1', '2']
+ flash[:hash_value] = { foo: 'bar' }
+
+ result = render_flash_messages
+ assert_equal '', result
+ end
+
private
def wiki_links_with_special_characters
diff --git a/test/helpers/avatars_helper_test.rb b/test/helpers/avatars_helper_test.rb
index f407ae09e..6b426bc98 100644
--- a/test/helpers/avatars_helper_test.rb
+++ b/test/helpers/avatars_helper_test.rb
@@ -63,14 +63,26 @@ class AvatarsHelperTest < Redmine::HelperTest
end
def test_avatar_css_class
- # The default class of the img tag should be gravatar
- assert_include 'class="gravatar"', avatar('jsmith <jsmith@somenet.foo>')
- assert_include 'class="gravatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture')
+ # The default classes of the img tag should be gravatar and avatar
+ assert_include 'class="gravatar avatar"', avatar('jsmith <jsmith@somenet.foo>')
+ assert_include 'class="gravatar avatar picture"', avatar('jsmith <jsmith@somenet.foo>', :class => 'picture')
end
- def test_avatar_disabled
+ def test_avatar_with_initials
+ with_settings :gravatar_default => 'initials' do
+ assert_include 'initials="RA"', avatar(User.find(1))
+ end
+ end
+
+ def test_avatar_should_reject_initials_if_default_is_not_initials
+ with_settings :gravatar_default => 'identicon' do
+ assert_not_include 'initials="RA"', avatar(User.find(1))
+ end
+ end
+
+ def test_avatar_disabled_should_display_user_initials
with_settings :gravatar_enabled => '0' do
- assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
+ assert_equal "<span role=\"img\" class=\"avatar-color-2 avatar s24\">JS</span>", avatar(User.find_by_mail('jsmith@somenet.foo'))
end
end
diff --git a/test/helpers/icons_helper_test.rb b/test/helpers/icons_helper_test.rb
index ab0b58db4..7ef071f86 100644
--- a/test/helpers/icons_helper_test.rb
+++ b/test/helpers/icons_helper_test.rb
@@ -71,6 +71,13 @@ class IconsHelperTest < Redmine::HelperTest
assert_match expected, icon
end
+ def test_sprite_icon_should_return_svg_with_filled_class_when_style_is_filled
+ expected = %r{<svg class="s18 icon-svg icon-svg-filled" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--edit"></use></svg>$}
+ icon = sprite_icon('edit', style: :filled)
+
+ assert_match expected, icon
+ end
+
def test_file_icon_should_return_folder_icon_for_directory
entry = stub(:is_dir? => true)
expected = %r{<svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg#icon--folder"></use></svg><span class="icon-label">folder_name</span>}
diff --git a/test/helpers/journals_helper_test.rb b/test/helpers/journals_helper_test.rb
index 355d5ec6f..5c78761ef 100644
--- a/test/helpers/journals_helper_test.rb
+++ b/test/helpers/journals_helper_test.rb
@@ -47,10 +47,27 @@ class JournalsHelperTest < Redmine::HelperTest
journals = issue.visible_journals_with_index # add indice
journal_actions = render_journal_actions(issue, journals.first, {reply_links: true})
- assert_select_in journal_actions, 'a[title=?][class="icon icon-comment"]', 'Quote'
+ assert_select_in journal_actions, 'a[title=?][class="icon-only icon-quote"]', 'Quote'
assert_select_in journal_actions, 'a[title=?][class="icon-only icon-edit"]', 'Edit'
assert_select_in journal_actions, 'div[class="drdn-items"] a[class="icon icon-del"]'
assert_select_in journal_actions, 'div[class="drdn-items"] a[class="icon icon-copy-link"]'
+ assert_select_in journal_actions, 'span.reaction-button-wrapper'
+ end
+
+ def test_render_journal_actions_with_journal_without_notes
+ User.current = User.find(1)
+ issue = Issue.find(1)
+ issue.journals.first.update!(notes: '')
+
+ journals = issue.visible_journals_with_index
+
+ journal_actions = render_journal_actions(issue, journals.first, reply_links: true)
+
+ assert_select_in journal_actions, 'span.reaction-button-wrapper'
+ assert_select_in journal_actions, 'span.drdn'
+
+ assert_select_in journal_actions, 'a[class="icon-only icon-quote"]', false
+ assert_select_in journal_actions, 'a[class="icon-only icon-edit"]', false
end
def test_journal_thumbnail_attachments_should_be_in_the_same_order_as_the_journal_details
diff --git a/test/helpers/reactions_helper_test.rb b/test/helpers/reactions_helper_test.rb
new file mode 100644
index 000000000..1c5c82418
--- /dev/null
+++ b/test/helpers/reactions_helper_test.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../test_helper'
+
+class ReactionsHelperTest < ActionView::TestCase
+ include ReactionsHelper
+
+ setup do
+ User.current = users(:users_002)
+ Setting.reactions_enabled = '1'
+ end
+
+ teardown do
+ Setting.clear_cache
+ end
+
+ test 'reaction_id_for generates a DOM id' do
+ assert_equal "reaction_issue_1", reaction_id_for(issues(:issues_001))
+ end
+
+ test 'reaction_button returns nil when feature is disabled' do
+ Setting.reactions_enabled = '0'
+
+ assert_nil reaction_button(issues(:issues_004))
+ end
+
+ test 'reaction_button returns nil when object not visible' do
+ User.current = users(:users_003)
+
+ assert_nil reaction_button(issues(:issues_004))
+ end
+
+ test 'reaction_button for anonymous users shows readonly button' do
+ User.current = nil
+
+ result = reaction_button(journals(:journals_001))
+
+ assert_select_in result, 'span.reaction-button.readonly[title=?]', 'John Smith'
+ assert_select_in result, 'a.reaction-button', false
+ end
+
+ test 'reaction_button for inactive projects shows readonly button' do
+ issue6 = issues(:issues_006)
+ issue6.project.update!(status: Project::STATUS_CLOSED)
+
+ result = reaction_button(issue6)
+
+ assert_select_in result, 'span.reaction-button.readonly[title=?]', 'John Smith'
+ assert_select_in result, 'a.reaction-button', false
+ end
+
+ test 'reaction_button includes no tooltip when the object has no reactions' do
+ issue = issues(:issues_002) # Issue without reactions
+ result = reaction_button(issue)
+
+ assert_select_in result, 'a.reaction-button[title]', false
+ end
+
+ test 'reaction_button includes tooltip with all usernames when reactions are 10 or fewer' do
+ issue = issues(:issues_002)
+
+ reactions = build_reactions(10)
+ issue.reactions += reactions
+
+ result = with_locale 'en' do
+ reaction_button(issue)
+ end
+
+ # The tooltip should display usernames in order of newest reactions.
+ expected_tooltip = 'Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, Bob5 Doe, ' \
+ 'Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and Bob0 Doe'
+
+ assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip
+ end
+
+ test 'reaction_button includes tooltip with 10 usernames and others count when reactions exceed 10' do
+ issue = issues(:issues_002)
+
+ reactions = build_reactions(11)
+ issue.reactions += reactions
+
+ result = with_locale 'en' do
+ reaction_button(issue)
+ end
+
+ expected_tooltip = 'Bob10 Doe, Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, ' \
+ 'Bob5 Doe, Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and 1 other'
+
+ assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip
+ end
+
+ test 'reaction_button should be label less when no reactions' do
+ issue = issues(:issues_002)
+
+ result = with_locale('en') do
+ reaction_button(issue)
+ end
+ assert_select_in result, 'a.reaction-button' do
+ assert_select 'span.icon-label', false
+ end
+
+ # readonly
+ User.current = nil
+ result = with_locale('en') do
+ reaction_button(issue)
+ end
+ assert_select_in result, 'span.reaction-button.readonly' do
+ assert_select 'span.icon-label', false
+ end
+ end
+
+ test 'reaction_button should not count and display non-visible users' do
+ issue2 = issues(:issues_002)
+
+ issue2.reaction_detail = Reaction::Detail.new(
+ visible_users: users(:users_002, :users_003)
+ )
+
+ result = with_locale('en') do
+ reaction_button(issue2)
+ end
+
+ assert_select_in result, 'a.reaction-button[title=?]', 'John Smith and Dave Lopper'
+
+ # When all users are non-visible users
+ issue2.reaction_detail = Reaction::Detail.new(
+ visible_users: []
+ )
+
+ result = with_locale('en') do
+ reaction_button(issue2)
+ end
+
+ assert_select_in result, 'a.reaction-button[title]', false
+ assert_select_in result, 'a.reaction-button' do
+ assert_select 'span.icon-label', false
+ end
+ end
+
+ test 'reaction_button formats the tooltip content based on the support.array settings of each locale' do
+ result = with_locale('ja') do
+ reaction_button(issues(:issues_001))
+ end
+
+ assert_select_in result, 'a.reaction-button[title=?]', 'Dave Lopper、John Smith、Redmine Admin'
+ end
+
+ test 'reaction_button for reacted object' do
+ User.current = users(:users_002)
+
+ issue = issues(:issues_001)
+
+ result = with_locale('en') do
+ reaction_button(issue)
+ end
+ tooltip = 'Dave Lopper, John Smith, and Redmine Admin'
+
+ assert_select_in result, 'span.reaction-button-wrapper[data-reaction-button-id=?]', 'reaction_issue_1' do
+ href = reaction_path(issue.reaction_detail.user_reaction, object_type: 'Issue', object_id: 1)
+
+ assert_select 'a.icon.reaction-button.reacted[href=?]', href do
+ assert_select 'use[href*=?]', 'thumb-up-filled'
+ assert_select 'span.icon-label', '3'
+ end
+
+ assert_select 'span.reaction-button', false
+ end
+ end
+
+ test 'reaction_button for non-reacted object' do
+ User.current = users(:users_004)
+
+ issue = issues(:issues_001)
+
+ result = with_locale('en') do
+ reaction_button(issue)
+ end
+ tooltip = 'Dave Lopper, John Smith, and Redmine Admin'
+
+ assert_select_in result, 'span.reaction-button-wrapper[data-reaction-button-id=?]', 'reaction_issue_1' do
+ href = reactions_path(object_type: 'Issue', object_id: 1)
+
+ assert_select 'a.icon.reaction-button[href=?]', href do
+ assert_select 'use[href*=?]', 'thumb-up'
+ assert_select 'span.icon-label', '3'
+ end
+
+ assert_select 'span.reaction-button', false
+ end
+ end
+
+ private
+
+ def build_reactions(count)
+ Array.new(count) do |i|
+ Reaction.new(user: User.generate!(firstname: "Bob#{i}"))
+ end
+ end
+end
diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb
index 23641b53a..4145fb969 100644
--- a/test/integration/api_test/authentication_test.rb
+++ b/test/integration/api_test/authentication_test.rb
@@ -127,20 +127,23 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base
assert_response :unauthorized
end
+ # TODO: check why this test does not use the API endpoint
def test_api_should_accept_switch_user_header_for_admin_user
user = User.find(1)
su = User.find(4)
get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login}
assert_response :success
- assert_select 'h2', :text => su.name
+ assert_select 'h2', :text => "#{su.initials} #{su.name}"
end
+ # TODO: check why this test does not use the API endpoint
def test_api_should_respond_with_412_when_trying_to_switch_to_a_invalid_user
get '/users/current', :headers => {'X-Redmine-API-Key' => User.find(1).api_key, 'X-Redmine-Switch-User' => 'foobar'}
assert_response :precondition_failed
end
+ # TODO: check why this test does not use the API endpoint
def test_api_should_respond_with_412_when_trying_to_switch_to_a_locked_user
user = User.find(5)
assert user.locked?
@@ -149,12 +152,13 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base
assert_response :precondition_failed
end
+ # TODO: check why this test does not use the API endpoint
def test_api_should_not_accept_switch_user_header_for_non_admin_user
user = User.find(2)
su = User.find(4)
get '/users/current', :headers => {'X-Redmine-API-Key' => user.api_key, 'X-Redmine-Switch-User' => su.login}
assert_response :success
- assert_select 'h2', :text => user.name
+ assert_select 'h2', :text => "#{user.initials} #{user.name}"
end
end
diff --git a/test/integration/api_test/custom_fields_test.rb b/test/integration/api_test/custom_fields_test.rb
index 0df56e59a..4fb06636e 100644
--- a/test/integration/api_test/custom_fields_test.rb
+++ b/test/integration/api_test/custom_fields_test.rb
@@ -37,6 +37,8 @@ class Redmine::ApiTest::CustomFieldsTest < Redmine::ApiTest::Base
end
assert_select 'trackers[type=array]'
assert_select 'roles[type=array]'
+ assert_select 'visible', :text => 'true'
+ assert_select 'editable', :text => 'true'
end
end
end
diff --git a/test/integration/api_test/news_test.rb b/test/integration/api_test/news_test.rb
index bd9f2bb6d..399b2b347 100644
--- a/test/integration/api_test/news_test.rb
+++ b/test/integration/api_test/news_test.rb
@@ -62,7 +62,7 @@ class Redmine::ApiTest::NewsTest < Redmine::ApiTest::Base
assert_select "author[id=2][name=\"John Smith\"]"
assert_select 'title', 'eCookbook first release !'
assert_select 'summary', 'First version was released...'
- assert_select 'description', "eCookbook 1.0 has been released.\n\nVisit http://ecookbook.somenet.foo/"
+ assert_select 'description', 'eCookbook 1.0 has been released. Visit http://ecookbook.somenet.foo/'
assert_select 'created_on', News.find(1).created_on.iso8601
end
end
diff --git a/test/integration/issues_test.rb b/test/integration/issues_test.rb
index ac8d432f1..d0763091e 100644
--- a/test/integration/issues_test.rb
+++ b/test/integration/issues_test.rb
@@ -355,7 +355,7 @@ class IssuesTest < Redmine::IntegrationTest
end
# Issue view
follow_redirect!
- assert_select 'ul.details li', :text => "Tester changed from #{tester} to #{new_tester}"
+ assert_select 'ul.journal-details li', :text => "Tester changed from #{tester} to #{new_tester}"
end
end
diff --git a/test/integration/repositories_git_test.rb b/test/integration/repositories_git_test.rb
index 20d643449..793b49458 100644
--- a/test/integration/repositories_git_test.rb
+++ b/test/integration/repositories_git_test.rb
@@ -35,6 +35,7 @@ class RepositoriesGitTest < Redmine::IntegrationTest
:path_encoding => 'ISO-8859-1'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
if File.directory?(REPOSITORY_PATH)
diff --git a/test/system/copy_pre_content_to_clipboard_test.rb b/test/system/copy_pre_content_to_clipboard_test.rb
new file mode 100644
index 000000000..32ffd4e3e
--- /dev/null
+++ b/test/system/copy_pre_content_to_clipboard_test.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+require_relative '../application_system_test_case'
+class CopyPreContentToClipboardSystemTest < ApplicationSystemTestCase
+ def test_copy_issue_pre_content_to_clipboard_if_common_mark
+ issue = Issue.find(1)
+ issue.update(description: "```\ntest\ncommon mark\n```")
+ assert_copied_pre_content_matches(issue_id: issue.id, expected_value: "test\ncommon mark")
+ end
+
+ def test_copy_issue_code_content_to_clipboard_if_common_mark
+ issue = Issue.find(1)
+ issue.update(description: "```ruby\nputs 'Hello, World.'\ncommon mark\n```")
+ assert_copied_pre_content_matches(issue_id: issue.id, expected_value: "puts 'Hello, World.'\ncommon mark")
+ end
+
+ def test_copy_issue_pre_content_to_clipboard_if_textile
+ issue = Issue.find(1)
+ issue.update(description: "<pre>\ntest\ntextile\n</pre>")
+ with_settings text_formatting: :textile do
+ assert_copied_pre_content_matches(issue_id: issue.id, expected_value: "test\ntextile")
+ end
+ end
+
+ def test_copy_issue_code_content_to_clipboard_if_textile
+ issue = Issue.find(1)
+ issue.update(description: "<pre><code class=\"ruby\">\nputs 'Hello, World.'\ntextile\n</code></pre>")
+ with_settings text_formatting: :textile do
+ assert_copied_pre_content_matches(issue_id: issue.id, expected_value: "puts 'Hello, World.'\ntextile")
+ end
+ end
+
+ private
+
+ def modifier_key
+ modifier = osx? ? 'command' : 'control'
+ modifier.to_sym
+ end
+
+ def assert_copied_pre_content_matches(issue_id:, expected_value:)
+ visit "/issues/#{issue_id}"
+ # A button appears when hovering over the <pre> tag
+ find("#issue_description_wiki div.pre-wrapper:first-of-type").hover
+ assert_selector('#issue_description_wiki div.pre-wrapper:first-of-type .copy-pre-content-link')
+
+ # Copy pre content to Clipboard
+ find("#issue_description_wiki div.pre-wrapper:first-of-type .copy-pre-content-link").click
+
+ # Paste the value copied to the clipboard into the textarea to get and test
+ first('.icon-edit').click
+ find('textarea#issue_notes').set('')
+ find('textarea#issue_notes').send_keys([modifier_key, 'v'])
+ assert_equal find('textarea#issue_notes').value, expected_value
+ end
+end
diff --git a/test/system/issues_reply_test.rb b/test/system/issues_reply_test.rb
index 86cd21f16..76ed485a5 100644
--- a/test/system/issues_reply_test.rb
+++ b/test/system/issues_reply_test.rb
@@ -93,7 +93,7 @@ class IssuesReplyTest < ApplicationSystemTestCase
# Select the entire details of the note#1 and the part of the note#1's text.
page.execute_script <<-JS
const range = document.createRange();
- range.setStartBefore(document.querySelector('#change-1 .details'));
+ range.setStartBefore(document.querySelector('#change-1 .journal-details'));
// Select only the text "Journal" from the text "Journal notes" in the note-1.
range.setEnd(document.querySelector('#change-1 .wiki > p').childNodes[0], 7);
diff --git a/test/system/issues_test.rb b/test/system/issues_test.rb
index 80ef25e0c..c161538e7 100644
--- a/test/system/issues_test.rb
+++ b/test/system/issues_test.rb
@@ -34,6 +34,8 @@ class IssuesSystemTest < ApplicationSystemTestCase
find('input[name=commit]').click
end
+ assert_text /Issue #\d+ created./
+
# find created issue
issue = Issue.find_by_subject("new test issue")
assert_kind_of Issue, issue
@@ -86,6 +88,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
fill_in field2.name, :with => 'CF2 value'
assert_difference 'Issue.count' do
page.first(:button, 'Create').click
+ assert_text /Issue #\d+ created./
end
issue = Issue.order('id desc').first
@@ -125,6 +128,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
end
assert_difference 'Issue.count' do
find('input[name=commit]').click
+ assert_text /Issue #\d+ created./
end
issue = Issue.order('id desc').first
@@ -141,6 +145,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
attach_file 'attachments[dummy][file]', Rails.root.join('test/fixtures/files/testfile.txt')
fill_in 'attachments[1][description]', :with => 'Some description'
click_on 'Create'
+ assert_text /Issue #\d+ created./
end
assert_equal 1, issue.attachments.count
assert_equal 'Some description', issue.attachments.first.description
@@ -163,6 +168,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
attach_file 'attachments[dummy][file]', Rails.root.join('test/fixtures/files/testfile.txt')
fill_in 'attachments[1][description]', :with => 'Some description'
click_on 'Create'
+ assert_text /Issue #\d+ created./
end
assert_equal 1, issue.attachments.count
assert_equal 'Some description', issue.attachments.first.description
@@ -181,6 +187,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
click_on 'Create'
end
click_on 'Create'
+ assert_text /Issue #\d+ created./
end
end
@@ -200,6 +207,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
end
assert_difference 'Issue.count' do
click_button('Create')
+ assert_text /Issue #\d+ created./
end
issue = Issue.order('id desc').first
@@ -230,6 +238,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
fill_in 'Form update CF', :with => 'CF value'
assert_no_difference 'Issue.count' do
page.first(:button, 'Submit').click
+ assert_text 'Successful update.'
end
assert page.has_css?('#flash_notice')
issue = Issue.find(1)
@@ -245,6 +254,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
page.find("#issue_status_id").select("Closed")
assert_no_difference 'Issue.count' do
page.first(:button, 'Submit').click
+ assert_text 'Successful update.'
end
assert page.has_css?('#flash_notice')
assert_equal 5, issue.reload.status.id
@@ -267,6 +277,7 @@ class IssuesSystemTest < ApplicationSystemTestCase
click_on 'Submit'
+ assert_text 'Successful update.'
assert_equal 3, Issue.find(2).attachments.count
end
diff --git a/test/system/messages_test.rb b/test/system/messages_test.rb
index 66a29a3a7..ac074ac1a 100644
--- a/test/system/messages_test.rb
+++ b/test/system/messages_test.rb
@@ -22,7 +22,7 @@ require_relative '../application_system_test_case'
class MessagesTest < ApplicationSystemTestCase
def test_reply_to_topic_message
with_text_formatting 'common_mark' do
- within '#content > .contextual' do
+ within '#content > [data-controller="quote-reply"]' do
click_link 'Quote'
end
@@ -64,7 +64,7 @@ class MessagesTest < ApplicationSystemTestCase
window.getSelection().addRange(range);
JS
- within '#content > .contextual' do
+ within '#content > [data-controller="quote-reply"]' do
click_link 'Quote'
end
diff --git a/test/system/oauth_provider_test.rb b/test/system/oauth_provider_test.rb
new file mode 100644
index 000000000..364ed4c94
--- /dev/null
+++ b/test/system/oauth_provider_test.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require_relative '../application_system_test_case'
+require 'oauth2'
+require 'rack'
+require 'puma'
+
+class OauthProviderSystemTest < ApplicationSystemTestCase
+ fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles,
+ :trackers, :projects_trackers, :enabled_modules, :issue_statuses, :issues,
+ :enumerations, :custom_fields, :custom_values, :custom_fields_trackers,
+ :watchers, :journals, :journal_details, :versions,
+ :workflows
+
+ test 'application creation and authorization' do
+ #
+ # admin creates the application, granting permissions and generating a uuid
+ # and secret.
+ #
+ log_user 'admin', 'admin'
+ with_settings rest_api_enabled: 1 do
+ visit '/admin'
+ within 'div#admin-menu ul' do
+ click_link 'Applications'
+ end
+ click_link 'New Application'
+ fill_in 'Name', with: 'Oauth Test'
+
+ # as per https://tools.ietf.org/html/rfc8252#section-7.3, the port can be
+ # anything when the redirect URI's host is 127.0.0.1.
+ fill_in 'Redirect URI', with: 'http://127.0.0.1'
+
+ check 'View Issues'
+ click_button 'Create'
+
+ assert_text "Application created."
+ end
+
+ assert app = Doorkeeper::Application.find_by_name('Oauth Test')
+
+ find 'h2', visible: true, text: /Oauth Test/
+ find 'p code', visible: true, text: app.uid
+ find 'p strong', visible: true, text: /will not be shown again/
+ find 'p code', visible: true, text: /View Issues/
+
+ # scrape the clear text secret from the page
+ app_secret = all(:css, 'p code')[1].text
+
+ click_link 'Sign out'
+
+ #
+ # regular user authorizes the application
+ #
+ client = OAuth2::Client.new(app.uid, app_secret, site: "http://127.0.0.1:#{test_port}/")
+
+ # set up a dummy http listener to handle the redirect
+ port = rand 10000..20000
+ redirect_uri = "http://127.0.0.1:#{port}"
+ # the request handler below will set this to the auth token
+ token = nil
+
+ # launches webrick, listening for the redirect with the auth code.
+ launch_client_app(port: port) do |req, res|
+ # get access code from code url param
+ if code = req.params['code'].presence
+ # exchange it for token
+ token = client.auth_code.get_token(code, redirect_uri: redirect_uri)
+ res.body = ["<html><body><p>Authorization succeeded, you may close this window now.</p></body></html>"]
+ end
+ end
+
+ log_user 'jsmith', 'jsmith'
+ with_settings rest_api_enabled: 1 do
+ visit '/my/account'
+ click_link 'Authorized applications'
+ find 'p.nodata', visible: true
+
+ # an oauth client would send the user to this url to request permission
+ url = client.auth_code.authorize_url redirect_uri: redirect_uri, scope: 'view_issues view_project'
+ uri = URI.parse url
+ visit uri.path + '?' + uri.query
+
+ find 'h2', visible: true, text: 'Authorization required'
+ find 'p', visible: true, text: /Authorize Oauth Test/
+ find '.oauth-permissions', visible: true, text: /View Issues/
+ find '.oauth-permissions', visible: true, text: /View project/
+
+ click_button 'Authorize'
+
+ assert grant = app.access_grants.last
+ assert_equal 'view_issues view_project', grant.scopes.to_s
+
+ # check for output defined above in the request handler
+ find 'p', visible: true, text: /Authorization succeeded/
+ assert token.present?
+
+ visit '/my/account'
+ click_link 'Authorized applications'
+ find 'td', visible: true, text: /Oauth Test/
+ click_link 'Sign out'
+
+ # Now, use the token for some API requests
+ assert_raise(RestClient::Unauthorized) do
+ RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json"
+ end
+
+ headers = { 'Authorization' => "Bearer #{token.token}" }
+ r = RestClient.get "http://localhost:#{test_port}/projects/onlinestore/issues.json", headers
+ issues = JSON.parse(r.body)['issues']
+ assert issues.any?
+
+ # time entries access is not part of the granted scopes
+ assert_raise(RestClient::Forbidden) do
+ RestClient.get "http://localhost:#{test_port}/projects/onlinestore/time_entries.json", headers
+ end
+ end
+ end
+
+ private
+
+ def launch_client_app(port: 12345, path: '/', &block)
+ app = ->(env) do
+ req = Rack::Request.new(env)
+ res = Rack::Response.new
+ yield(req, res)
+ res.finish
+ end
+
+ server = Puma::Server.new app
+ server.add_tcp_listener '127.0.0.1', port
+ Thread.new { server.run }
+ end
+
+ def test_port
+ Capybara.current_session.server.port
+ end
+end
diff --git a/test/system/reactions_test.rb b/test/system/reactions_test.rb
new file mode 100644
index 000000000..96dd4cf81
--- /dev/null
+++ b/test/system/reactions_test.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../application_system_test_case'
+
+class ReactionsSystemTest < ApplicationSystemTestCase
+ def test_react_to_issue
+ log_user('jsmith', 'jsmith')
+
+ issue = issues(:issues_002)
+
+ with_settings(reactions_enabled: '1') do
+ visit '/issues/2'
+ reaction_button = find("div.issue.details [data-reaction-button-id=\"reaction_issue_#{issue.id}\"]")
+ assert_reaction_add_and_remove(reaction_button, issue)
+ end
+ end
+
+ def test_react_to_journal
+ log_user('jsmith', 'jsmith')
+
+ journal = journals(:journals_002)
+
+ with_settings(reactions_enabled: '1') do
+ visit '/issues/1'
+ reaction_button = find("[data-reaction-button-id=\"reaction_journal_#{journal.id}\"]")
+ assert_reaction_add_and_remove(reaction_button, journal.reload)
+ end
+ end
+
+ def test_react_to_forum_reply
+ log_user('jsmith', 'jsmith')
+
+ reply_message = messages(:messages_002) # reply to message_001
+
+ with_settings(reactions_enabled: '1') do
+ visit 'boards/1/topics/1'
+ reaction_button = find("[data-reaction-button-id=\"reaction_message_#{reply_message.id}\"]")
+ assert_reaction_add_and_remove(reaction_button, reply_message)
+ end
+ end
+
+ def test_react_to_forum_message
+ log_user('jsmith', 'jsmith')
+
+ message = messages(:messages_001)
+
+ with_settings(reactions_enabled: '1') do
+ visit 'boards/1/topics/1'
+ reaction_button = find("[data-reaction-button-id=\"reaction_message_#{message.id}\"]")
+ assert_reaction_add_and_remove(reaction_button, message)
+ end
+ end
+
+ def test_react_to_news
+ log_user('jsmith', 'jsmith')
+
+ with_settings(reactions_enabled: '1') do
+ visit '/news/2'
+ reaction_button = find("[data-reaction-button-id=\"reaction_news_2\"]")
+ assert_reaction_add_and_remove(reaction_button, news(:news_002))
+ end
+ end
+
+ def test_react_to_comment
+ log_user('jsmith', 'jsmith')
+
+ comment = comments(:comments_002)
+
+ with_settings(reactions_enabled: '1') do
+ visit '/news/1'
+ reaction_button = find("[data-reaction-button-id=\"reaction_comment_#{comment.id}\"]")
+ assert_reaction_add_and_remove(reaction_button, comment)
+ end
+ end
+
+ def test_reactions_disabled
+ log_user('jsmith', 'jsmith')
+
+ with_settings(reactions_enabled: '0') do
+ visit '/issues/1'
+ assert_no_selector('[data-reaction-button-id="reaction_issue_1"]')
+ end
+ end
+
+ def test_reaction_button_is_visible_but_not_clickable_for_not_logged_in_user
+ with_settings(reactions_enabled: '1') do
+ visit '/issues/1'
+
+ # visible
+ reaction_button = find('div.issue.details [data-reaction-button-id="reaction_issue_1"]')
+ within(reaction_button) { assert_selector('span.reaction-button') }
+ assert_equal "3", reaction_button.text
+
+ # not clickable
+ within(reaction_button) { assert_no_selector('a.reaction-button') }
+ end
+ end
+
+ def test_reaction_button_is_visible_on_property_changes_tab
+ # Create a journal with no notes
+ journal_without_notes = Journal.generate!(journalized: issues(:issues_001), notes: '', details: [JournalDetail.new])
+
+ log_user('jsmith', 'jsmith')
+
+ visit '/issues/1?tab=properties'
+
+ # Scroll to the history content
+ click_link '#1'
+
+ assert_selector '#tab-properties.selected'
+
+ within('#change-1') do
+ assert_selector 'a.reaction-button'
+
+ assert_no_selector 'a.icon-quote'
+ assert_no_selector 'span.drdn'
+ end
+ within("#change-#{journal_without_notes.id}") do
+ assert_selector 'a.reaction-button'
+
+ assert_no_selector '.drdn'
+ end
+
+ click_link 'History'
+
+ within('#change-1') do
+ assert_selector 'a.reaction-button'
+
+ assert_selector 'a.icon-quote'
+ assert_selector 'span.drdn'
+ end
+ within("#change-#{journal_without_notes.id}") do
+ assert_selector 'a.reaction-button'
+ assert_selector 'span.drdn'
+
+ assert_no_selector 'a.icon-quote'
+ end
+ end
+
+ private
+
+ def assert_reaction_add_and_remove(reaction_button, expected_subject)
+ # Add a reaction
+ within(reaction_button) { find('a.reaction-button').click }
+ find('body').hover # Hide tooltip
+ within(reaction_button) { assert_selector('a.reaction-button.reacted[title="John Smith"]') }
+ assert_equal "1", reaction_button.text
+ assert_equal 1, expected_subject.reactions.count
+
+ # Remove the reaction
+ within(reaction_button) { find('a.reacted').click }
+ within(reaction_button) { assert_selector('a.reaction-button:not(.reacted)') }
+ assert_equal "", reaction_button.text
+ assert_equal 0, expected_subject.reactions.count
+ end
+end
diff --git a/test/system/sticky_issue_header_test.rb b/test/system/sticky_issue_header_test.rb
new file mode 100644
index 000000000..5c67b36f3
--- /dev/null
+++ b/test/system/sticky_issue_header_test.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+require_relative '../application_system_test_case'
+class StickyIssueHeaderSystemTest < ApplicationSystemTestCase
+ test "sticky issue header is hidden by default" do
+ issue = Issue.find(1)
+ visit issue_path(issue)
+
+ assert_no_selector "#sticky-issue-header", text: issue.subject
+ end
+
+ test "sticky issue header appears on scroll" do
+ issue = Issue.find(2)
+ visit issue_path(issue)
+
+ page.execute_script("window.scrollTo(0, 1000)")
+ assert_selector "#sticky-issue-header.is-visible", text: issue.subject
+
+ page.execute_script("window.scrollTo(0, 0)")
+ assert_no_selector "#sticky-issue-header", text: issue.subject
+ end
+end
diff --git a/test/system/sudo_mode_test.rb b/test/system/sudo_mode_test.rb
index 73e755acd..307d465ff 100644
--- a/test/system/sudo_mode_test.rb
+++ b/test/system/sudo_mode_test.rb
@@ -48,7 +48,6 @@ class SudoModeSystemTest < ApplicationSystemTestCase
find('input[name=commit]').click
end
- assert_equal '/users', current_path
assert page.has_content?("Confirm your password to continue")
assert page.has_css?('form#sudo-form')
@@ -56,6 +55,8 @@ class SudoModeSystemTest < ApplicationSystemTestCase
fill_in 'Password', :with => 'admin'
click_button 'Submit'
end
+
+ assert_text /User johnpaul created./
end
end
diff --git a/test/system/timelog_test.rb b/test/system/timelog_test.rb
index 57c521096..38c3ae19c 100644
--- a/test/system/timelog_test.rb
+++ b/test/system/timelog_test.rb
@@ -49,6 +49,8 @@ class TimelogTest < ApplicationSystemTestCase
select 'QA', :from => 'Activity'
page.first(:button, 'Submit').click
+ assert_text 'Successful update.'
+
entries = TimeEntry.where(:id => [1, 2, 3]).to_a
assert entries.all? {|entry| entry.hours == 8.5}
assert entries.all? {|entry| entry.activity.name == 'QA'}
@@ -89,6 +91,7 @@ class TimelogTest < ApplicationSystemTestCase
select 'Tracker', :from => 'Available Columns'
page.first('input[type=button].move-right').click
click_on 'Save'
+ assert_text 'Successful update.'
# Display the list with updated settings
visit '/time_entries'
diff --git a/test/unit/changeset_test.rb b/test/unit/changeset_test.rb
index ca1e010e3..3ad8b1cbf 100644
--- a/test/unit/changeset_test.rb
+++ b/test/unit/changeset_test.rb
@@ -479,7 +479,7 @@ class ChangesetTest < ActiveSupport::TestCase
end
def test_next_nil
- changeset = Changeset.find_by_revision('10')
+ changeset = Changeset.find_by_revision('11')
assert_nil changeset.next
end
diff --git a/test/unit/email_address_test.rb b/test/unit/email_address_test.rb
index 9d57beb97..923df897a 100644
--- a/test/unit/email_address_test.rb
+++ b/test/unit/email_address_test.rb
@@ -63,6 +63,12 @@ class EmailAddressTest < ActiveSupport::TestCase
end
end
+ def test_domain_in_should_not_raise_exception_when_domain_is_nil
+ assert_nothing_raised do
+ assert_not EmailAddress.domain_in?(nil, 'example.com')
+ end
+ end
+
def test_should_reject_invalid_email
assert_not EmailAddress.new(address: 'invalid,email@example.com').valid?
end
diff --git a/test/unit/lib/redmine/field_format/progressbar_format_test.rb b/test/unit/lib/redmine/field_format/progressbar_format_test.rb
index d507f9ddb..6e0df724d 100644
--- a/test/unit/lib/redmine/field_format/progressbar_format_test.rb
+++ b/test/unit/lib/redmine/field_format/progressbar_format_test.rb
@@ -21,7 +21,7 @@ require_relative '../../../../test_helper'
require 'redmine/field_format'
module Redmine::FieldFormat
- class ProgressbarFormatTest < ActiveSupport::TestCase
+ class ProgressbarFormatTest < ActionView::TestCase
def setup
@field = IssueCustomField.new(name: 'ProgressbarTest', field_format: 'progressbar')
@format = Redmine::FieldFormat::ProgressbarFormat.instance
@@ -81,5 +81,52 @@ module Redmine::FieldFormat
options = @format.query_filter_options(@field, nil)
assert_equal :integer, options[:type]
end
+
+ def test_default_ratio_interval_should_be_default_issue_done_ratio_interval
+ @field.save
+ assert_equal 10, @field.ratio_interval
+ end
+
+ def test_ratio_interval
+ @field.update(ratio_interval: 5)
+ assert_equal 5, @field.ratio_interval
+ end
+
+ def test_edit_tag_possible_values_with_ratio_interval
+ [5, 10].each do |ratio_interval|
+ @field.update(ratio_interval: ratio_interval)
+ value = CustomValue.new(custom_field: @field, value: '90')
+
+ tag = @field.format.edit_tag(self, 'id', 'name', value)
+ assert_select_in tag, 'select' do
+ assert_select 'option', 100 / ratio_interval + 1
+ end
+ end
+ end
+
+ def test_bulk_edit_tag_possible_values_with_ratio_interval
+ [5, 10].each do |ratio_interval|
+ @field.update(ratio_interval: ratio_interval)
+ value = CustomValue.new(custom_field: @field, value: '90')
+ objects = [Issue.new, Issue.new]
+
+ tag = @field.format.bulk_edit_tag(self, 'id', 'name', @field, objects, value)
+ assert_select_in tag, 'select' do |select|
+ assert_select select.first, 'option', 100 / ratio_interval + 2
+ end
+ end
+ end
+
+ def test_formatted_value_with_html_true
+ expected = progress_bar(50)
+ formatted = @format.formatted_value(self, @field, 50, Issue.new, true)
+ assert_equal expected, formatted
+ assert formatted.html_safe?
+ end
+
+ def test_formatted_value_with_html_false
+ formatted = @format.formatted_value(self, @field, 50, Issue.new, false)
+ assert_equal '50', formatted
+ end
end
end
diff --git a/test/unit/lib/redmine/quote_reply_helper_test.rb b/test/unit/lib/redmine/quote_reply_helper_test.rb
index 43adb521b..d5d13d4f8 100644
--- a/test/unit/lib/redmine/quote_reply_helper_test.rb
+++ b/test/unit/lib/redmine/quote_reply_helper_test.rb
@@ -23,18 +23,18 @@ class QuoteReplyHelperTest < ActionView::TestCase
include ERB::Util
include Redmine::QuoteReply::Helper
- def test_quote_reply
+ def test_quote_reply_button
with_locale 'en' do
url = quoted_issue_path(issues(:issues_001))
- a_tag = quote_reply(url, '#issue_description_wiki')
- assert_includes a_tag, %|onclick="#{h "quoteReply('/issues/1/quoted', '#issue_description_wiki', 'common_mark'); return false;"}"|
- assert_includes a_tag, %|class="icon icon-comment"|
- assert_not_includes a_tag, 'title='
+ html = quote_reply_button(url: url)
+ assert_select_in html,
+ 'a[data-quote-reply-url-param=?][data-quote-reply-text-formatting-param=?]:not([title])',
+ url, Setting.text_formatting
# When icon_only is true
- a_tag = quote_reply(url, '#issue_description_wiki', icon_only: true)
- assert_includes a_tag, %|title="Quote"|
+ html = quote_reply_button(url: url, icon_only: true)
+ assert_select_in html, 'a.icon-only.icon-quote[title=?]', 'Quote'
end
end
end
diff --git a/test/unit/lib/redmine/reaction_test.rb b/test/unit/lib/redmine/reaction_test.rb
new file mode 100644
index 000000000..f3228a3bd
--- /dev/null
+++ b/test/unit/lib/redmine/reaction_test.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../../../test_helper'
+
+class Redmine::ReactionTest < ActiveSupport::TestCase
+ setup do
+ @user = users(:users_002)
+ @issue = issues(:issues_007)
+ Setting.reactions_enabled = '1'
+ end
+
+ teardown do
+ Setting.clear_cache
+ end
+
+ test 'preload_reaction_details preloads ReactionDetail for all objects in the collection' do
+ User.current = users(:users_002)
+
+ issue1 = issues(:issues_001)
+ issue2 = issues(:issues_002)
+
+ assert_nil issue1.instance_variable_get(:@reaction_detail)
+ assert_nil issue2.instance_variable_get(:@reaction_detail)
+
+ Issue.preload_reaction_details([issue1, issue2])
+
+ expected_issue1_reaction_detail = Reaction::Detail.new(
+ visible_users: [users(:users_003), users(:users_002), users(:users_001)],
+ user_reaction: reactions(:reaction_002)
+ )
+
+ # ReactionDetail is already preloaded, so calling reaction_detail does not execute any query.
+ assert_no_queries do
+ assert_equal expected_issue1_reaction_detail, issue1.reaction_detail
+
+ # Even when an object has no reactions, an empty ReactionDetail is set.
+ assert_equal Reaction::Detail.new(
+ visible_users: [],
+ user_reaction: nil
+ ), issue2.reaction_detail
+ end
+ end
+
+ test 'visible_users in ReactionDetail preloaded by preload_reaction_details does not include non-visible users' do
+ current_user = User.current = User.generate!
+ visible_user = users(:users_002)
+ non_visible_user = User.generate!
+
+ project = Project.generate!
+ role = Role.generate!(users_visibility: 'members_of_visible_projects')
+
+ User.add_to_project(current_user, project, role)
+ User.add_to_project(visible_user, project, roles(:roles_001))
+
+ issue = Issue.generate!(project: project)
+
+ [current_user, visible_user, non_visible_user].each do |user|
+ issue.reactions.create!(user: user)
+ end
+
+ Issue.preload_reaction_details([issue])
+
+ # non_visible_user is not visible to current_user because they do not belong to any project.
+ assert_equal [visible_user, current_user], issue.reaction_detail.visible_users
+ end
+
+ test 'preload_reaction_details does nothing when the reaction feature is disabled' do
+ Setting.reactions_enabled = '0'
+
+ User.current = users(:users_002)
+ news1 = news(:news_001)
+
+ # Stub the Setting to avoid executing queries for retrieving settings,
+ # making it easier to confirm no queries are executed by preload_reaction_details().
+ Setting.stubs(:reactions_enabled?).returns(false)
+
+ assert_no_queries do
+ News.preload_reaction_details([news1])
+ end
+
+ assert_nil news1.instance_variable_get(:@reaction_detail)
+ end
+
+ test 'reaction_detail loads and returns ReactionDetail if it is not preloaded' do
+ message7 = messages(:messages_007)
+
+ User.current = users(:users_002)
+ assert_nil message7.instance_variable_get(:@reaction_detail)
+
+ assert_equal Reaction::Detail.new(
+ visible_users: [users(:users_002)],
+ user_reaction: reactions(:reaction_009)
+ ), message7.reaction_detail
+ end
+
+ test 'load_reaction_detail loads ReactionDetail for the object itself' do
+ comment1 = comments(:comments_001)
+
+ User.current = users(:users_001)
+ assert_nil comment1.instance_variable_get(:@reaction_detail)
+
+ comment1.load_reaction_detail
+
+ assert_equal Reaction::Detail.new(
+ visible_users: [users(:users_002)],
+ user_reaction: nil
+ ), comment1.reaction_detail
+ end
+
+ test 'visible? returns true when reactions are enabled and object is visible to user' do
+ object = issues(:issues_007)
+ user = users(:users_002)
+
+ assert Redmine::Reaction.visible?(object, user)
+ end
+
+ test 'visible? returns false when reactions are disabled' do
+ Setting.reactions_enabled = '0'
+
+ object = issues(:issues_007)
+ user = users(:users_002)
+
+ assert_not Redmine::Reaction.visible?(object, user)
+ end
+
+ test 'visible? returns false when object is not visible to user' do
+ object = issues(:issues_007)
+ user = users(:users_002)
+
+ object.expects(:visible?).with(user).returns(false)
+
+ assert_not Redmine::Reaction.visible?(object, user)
+ end
+
+ test 'editable? returns true for various reactable objects when user is logged in, object is visible, and project is active' do
+ reactable_objects = {
+ issue: issues(:issues_007),
+ message: messages(:messages_001),
+ news: news(:news_001),
+ journal: journals(:journals_001),
+ comment: comments(:comments_002)
+ }
+ user = users(:users_002)
+
+ reactable_objects.each do |type, object|
+ assert Redmine::Reaction.editable?(object, user), "Expected editable? to return true for #{type}"
+ end
+ end
+
+ test 'editable? returns false when user is not logged in' do
+ object = issues(:issues_007)
+ user = User.anonymous
+
+ assert_not Redmine::Reaction.editable?(object, user)
+ end
+
+ test 'editable? returns false when project is inactive' do
+ object = issues(:issues_007)
+ user = users(:users_002)
+ object.project.update!(status: Project::STATUS_ARCHIVED)
+
+ assert_not Redmine::Reaction.editable?(object, user)
+ end
+
+ test 'editable? returns false when project is closed' do
+ object = issues(:issues_007)
+ user = users(:users_002)
+ object.project.update!(status: Project::STATUS_CLOSED)
+
+ assert_not Redmine::Reaction.editable?(object, user)
+ end
+end
diff --git a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb
index c0bff9b1f..9d6cd6b32 100644
--- a/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb
+++ b/test/unit/lib/redmine/scm/adapters/bazaar_adapter_test.rb
@@ -27,6 +27,7 @@ class BazaarAdapterTest < ActiveSupport::TestCase
def setup
@adapter = Redmine::Scm::Adapters::BazaarAdapter.
new(File.join(REPOSITORY_PATH, "trunk"))
+ skip "SCM command is unavailable" unless @adapter.class.client_available
end
def test_scm_version
diff --git a/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb
index 2ed9dc618..3bfe24997 100644
--- a/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb
+++ b/test/unit/lib/redmine/scm/adapters/cvs_adapter_test.rb
@@ -27,6 +27,7 @@ class CvsAdapterTest < ActiveSupport::TestCase
if File.directory?(REPOSITORY_PATH)
def setup
@adapter = Redmine::Scm::Adapters::CvsAdapter.new(MODULE_NAME, REPOSITORY_PATH)
+ skip "SCM command is unavailable" unless @adapter.class.client_available
end
def test_scm_version
diff --git a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb
index bf054860a..3f0451601 100644
--- a/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb
+++ b/test/unit/lib/redmine/scm/adapters/git_adapter_test.rb
@@ -42,13 +42,6 @@ class GitAdapterTest < ActiveSupport::TestCase
WINDOWS_SKIP_STR = "TODO: This test fails in Git for Windows above 1.7.10"
def setup
- adapter_class = Redmine::Scm::Adapters::GitAdapter
- assert adapter_class
- assert adapter_class.client_command
- assert_equal true, adapter_class.client_available
- assert_equal true, adapter_class.client_version_above?([1])
- assert_equal true, adapter_class.client_version_above?([1, 0])
-
@adapter =
Redmine::Scm::Adapters::GitAdapter.
new(
@@ -59,6 +52,8 @@ class GitAdapterTest < ActiveSupport::TestCase
'ISO-8859-1'
)
assert @adapter
+ skip "SCM is unavailable" unless @adapter.class.client_available
+
@char_1 = 'Ü'
@str_felix_hex = "Felix Sch\xC3\xA4fer".b
end
diff --git a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
index b4f284103..81741a746 100644
--- a/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
+++ b/test/unit/lib/redmine/scm/adapters/mercurial_adapter_test.rb
@@ -30,12 +30,6 @@ class MercurialAdapterTest < ActiveSupport::TestCase
if File.directory?(REPOSITORY_PATH)
def setup
- adapter_class = Redmine::Scm::Adapters::MercurialAdapter
- assert adapter_class
- assert adapter_class.client_command
- assert_equal true, adapter_class.client_available
- assert_equal true, adapter_class.client_version_above?([0, 9, 5])
-
@adapter =
Redmine::Scm::Adapters::MercurialAdapter.new(
REPOSITORY_PATH,
@@ -44,6 +38,8 @@ class MercurialAdapterTest < ActiveSupport::TestCase
nil,
'ISO-8859-1'
)
+ skip "SCM command is unavailable" unless @adapter.class.client_available
+
@diff_c_support = true
@char_1 = 'Ü'
@tag_char_1 = 'tag-Ü-00'
diff --git a/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb b/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb
index fe574a4ff..edc3541d1 100644
--- a/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb
+++ b/test/unit/lib/redmine/scm/adapters/subversion_adapter_test.rb
@@ -23,6 +23,7 @@ class SubversionAdapterTest < ActiveSupport::TestCase
if repository_configured?('subversion')
def setup
@adapter = Redmine::Scm::Adapters::SubversionAdapter.new(self.class.subversion_repository_url)
+ skip "SCM command is unavailable" unless @adapter.class.client_available
end
def test_client_version
diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb
index 5214a1e00..bb0c5d450 100644
--- a/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb
+++ b/test/unit/lib/redmine/wiki_formatting/common_mark/formatter_test.rb
@@ -26,71 +26,71 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
@formatter = Redmine::WikiFormatting::CommonMark::Formatter
end
- def format(text)
+ def to_html(text)
@formatter.new(text).to_html
end
def test_should_render_hard_breaks
html ="<p>foo<br>\nbar</p>"
- assert_equal html, format("foo\\\nbar")
- assert_equal html, format("foo \nbar")
+ assert_equal html, to_html("foo\\\nbar")
+ assert_equal html, to_html("foo \nbar")
end
def test_should_render_soft_breaks
- assert_equal "<p>foo<br>\nbar</p>", format("foo\nbar")
+ assert_equal "<p>foo<br>\nbar</p>", to_html("foo\nbar")
end
def test_syntax_error_in_image_reference_should_not_raise_exception
- assert format("!>[](foo.png)")
+ assert to_html("!>[](foo.png)")
end
def test_empty_image_should_not_raise_exception
- assert format("![]()")
+ assert to_html("![]()")
end
def test_inline_style
- assert_equal "<p><strong>foo</strong></p>", format("**foo**")
+ assert_equal "<p><strong>foo</strong></p>", to_html("**foo**")
end
def test_not_set_intra_emphasis
- assert_equal "<p>foo_bar_baz</p>", format("foo_bar_baz")
+ assert_equal "<p>foo_bar_baz</p>", to_html("foo_bar_baz")
end
def test_wiki_links_should_be_preserved
text = 'This is a wiki link: [[Foo]]'
- assert_include '[[Foo]]', format(text)
+ assert_include '[[Foo]]', to_html(text)
end
def test_redmine_links_with_double_quotes_should_be_preserved
text = 'This is a redmine link: version:"1.0"'
- assert_include 'version:"1.0"', format(text)
+ assert_include 'version:"1.0"', to_html(text)
end
def test_links_by_id_should_be_preserved
text = "[project#3]"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
end
def test_links_to_users_should_be_preserved
text = "[@login]"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
text = "[user:login]"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
text = "user:user@example.org"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
text = "[user:user@example.org]"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
text = "@user@example.org"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
text = "[@user@example.org]"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
end
def test_files_with_at_should_not_end_up_as_mailto_links
text = "printscreen@2x.png"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
text = "[printscreen@2x.png]"
- assert_equal "<p>#{text}</p>", format(text)
+ assert_equal "<p>#{text}</p>", to_html(text)
end
def test_should_support_syntax_highlight
@@ -100,7 +100,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
end
~~~
STR
- assert_select_in format(text), 'pre code.ruby.syntaxhl' do
+ assert_select_in to_html(text), 'pre code.ruby.syntaxhl' do
assert_select 'span.k', :text => 'def'
assert_select "[data-language='ruby']"
end
@@ -114,7 +114,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
~~~
STR
- assert_select_in format(text), 'pre' do
+ assert_select_in to_html(text), 'pre' do
assert_select 'code[class=?]', "c++ syntaxhl"
assert_select 'span.kt', :text => 'int'
assert_select "[data-language=?]", "c++"
@@ -123,12 +123,12 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
def test_external_links_should_have_external_css_class
text = 'This is a [link](http://example.net/)'
- assert_equal '<p>This is a <a href="http://example.net/" class="external">link</a></p>', format(text)
+ assert_equal '<p>This is a <a href="http://example.net/" class="external">link</a></p>', to_html(text)
end
def test_locals_links_should_not_have_external_css_class
text = 'This is a [link](/issues)'
- assert_equal '<p>This is a <a href="/issues">link</a></p>', format(text)
+ assert_equal '<p>This is a <a href="/issues">link</a></p>', to_html(text)
end
def test_markdown_should_not_require_surrounded_empty_line
@@ -137,7 +137,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
* One
* Two
STR
- assert_equal "<p>This is a list:</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n</ul>", format(text)
+ assert_equal "<p>This is a list:</p>\n<ul>\n<li>One</li>\n<li>Two</li>\n</ul>", to_html(text)
end
def test_footnotes
@@ -156,46 +156,46 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
</ol>
EXPECTED
- assert_equal expected.gsub(%r{[\r\n\t]}, ''), format(text).gsub(%r{[\r\n\t]}, '').rstrip
+ assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '').rstrip
end
STR_WITH_PRE = [
# 0
<<~STR.chomp,
# Title
-
+
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
STR
# 1
<<~STR.chomp,
## Heading 2
-
+
~~~ruby
def foo
end
~~~
-
+
Morbi facilisis accumsan orci non pharetra.
-
+
~~~ ruby
def foo
end
~~~
-
+
```
Pre Content:
-
+
## Inside pre
-
+
<tag> inside pre block
-
+
Morbi facilisis accumsan orci non pharetra.
```
STR
# 2
<<~STR.chomp,
### Heading 3
-
+
Nulla nunc nisi, egestas in ornare vel, posuere ac libero.
STR
]
@@ -226,18 +226,18 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
text = STR_WITH_PRE.join("\n\n")
replacement = "New text"
- assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
+ assert_equal [STR_WITH_PRE[0..1], "New text"].join("\n\n"),
@formatter.new(text).update_section(3, replacement)
end
def test_should_emphasize_text
text = 'This _text_ should be emphasized'
- assert_equal '<p>This <em>text</em> should be emphasized</p>', format(text)
+ assert_equal '<p>This <em>text</em> should be emphasized</p>', to_html(text)
end
def test_should_strike_through_text
text = 'This ~~text~~ should be striked through'
- assert_equal '<p>This <del>text</del> should be striked through</p>', format(text)
+ assert_equal '<p>This <del>text</del> should be striked through</p>', to_html(text)
end
def test_should_autolink_urls_and_emails
@@ -249,13 +249,13 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
["www.example.org", '<p><a href="http://www.example.org" class="external">www.example.org</a></p>'],
["user@example.org", '<p><a href="mailto:user@example.org" class="email">user@example.org</a></p>']
].each do |text, html|
- assert_equal html, format(text)
+ assert_equal html, to_html(text)
end
end
def test_should_support_html_tables
text = '<table style="background: red"><tr><td>Cell</td></tr></table>'
- assert_equal '<table><tr><td>Cell</td></tr></table>', format(text)
+ assert_equal '<table><tr><td>Cell</td></tr></table>', to_html(text)
end
def test_should_remove_unsafe_uris
@@ -263,7 +263,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
['<img src="data:foobar">', '<img>'],
['<a href="javascript:bla">click me</a>', '<p><a>click me</a></p>'],
].each do |text, html|
- assert_equal html, format(text)
+ assert_equal html, to_html(text)
end
end
@@ -274,7 +274,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
%[sit<br/>amet <style>.foo { color: #fff; }</style> <script>alert("hello world");</script>]
]
].each do |expected, input|
- assert_equal expected, format(input)
+ assert_equal expected, to_html(input)
end
end
@@ -287,7 +287,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
expected = <<~EXPECTED
<p>Task list:</p>
- <ul class="task-list">
+ <ul class="contains-task-list">
<li class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled> Task 1
</li>
@@ -296,7 +296,50 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
</ul>
EXPECTED
- assert_equal expected.gsub(%r{[\r\n\t]}, ''), format(text).gsub(%r{[\r\n\t]}, '').rstrip
+ assert_equal expected.gsub(%r{[\r\n\t]}, ''), to_html(text).gsub(%r{[\r\n\t]}, '').rstrip
+ end
+
+ def test_should_render_alert_blocks
+ text = <<~MD
+ > [!note]
+ > This is a note.
+
+ > [!tip]
+ > This is a tip.
+
+ > [!warning]
+ > This is a warning.
+
+ > [!caution]
+ > This is a caution.
+
+ > [!important]
+ > This is a important.
+ MD
+
+ html = to_html(text)
+ %w[note tip warning caution important].each do |alert|
+ icon = Redmine::WikiFormatting::CommonMark::ALERT_TYPE_TO_ICON_NAME[alert]
+ # rubocop:disable Layout/LineLength
+ expected = %r{<div class="markdown-alert markdown-alert-#{alert}">\n<p class="markdown-alert-title"><svg class="s18 icon-svg" aria-hidden="true"><use href="/assets/icons-\w+.svg\#icon--#{icon}"></use></svg><span class="icon-label">#{alert.capitalize}</span></p>\n<p>This is a #{alert}.</p>\n</div>}
+ # rubocop:enable Layout/LineLength
+ assert_match expected, html
+ end
+ end
+
+ def test_should_not_render_unknown_alert_type
+ text = <<~MD
+ > [!unknown]
+ > This should not become an alert.
+ MD
+
+ html = to_html(text)
+
+ assert_include "<blockquote>", html
+ assert_include "[!unknown]", html
+ assert_include "This should not become an alert.", html
+
+ assert_not_include 'markdown-alert', html
end
private
diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb
index 4c0282f2d..b2d19eab9 100644
--- a/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb
+++ b/test/unit/lib/redmine/wiki_formatting/common_mark/sanitization_filter_test.rb
@@ -47,10 +47,14 @@ if Object.const_defined?(:Commonmarker)
end
def test_should_support_footnotes
- input = %(<a href="#fn-1" id="fnref-1">foo</a>)
- assert_equal input, filter(input)
- input = %(<ol><li id="fn-1">footnote</li></ol>)
- assert_equal input, filter(input)
+ [
+ %(<a href="#fn-1" id="fnref-1">foo</a>),
+ %(<a href="#fn-1" id="fnref-1-2">foo</a>),
+ %(<ol><li id="fn-1">footnote</li></ol>),
+ ].each do |input|
+ assert_equal input, filter(input)
+ assert_equal input, filter(input)
+ end
end
def test_should_remove_invalid_ids
@@ -71,6 +75,32 @@ if Object.const_defined?(:Commonmarker)
assert_equal %(<code>foo</code>), filter(input)
end
+ def test_should_allow_valid_alert_div_and_p_classes
+ html = <<~HTML
+ <div class="markdown-alert markdown-alert-tip">
+ <p class="markdown-alert-title">Tip</p>
+ <p>Useful tip.</p>
+ </div>
+ HTML
+
+ sanitized = filter(html)
+
+ assert_include 'class="markdown-alert markdown-alert-tip"', sanitized
+ assert_include 'class="markdown-alert-title"', sanitized
+ end
+
+ def test_should_remove_invalid_div_class
+ html = '<div class="bad-class">Text</div>'
+ sanitized = filter(html)
+ assert_not_includes 'bad-class', sanitized
+ end
+
+ def test_should_remove_invalid_p_class
+ html = '<p class="bad-class">Text</p>'
+ sanitized = filter(html)
+ assert_not_include 'bad-class', sanitized
+ end
+
def test_should_allow_links_with_safe_url_schemes
%w(http https ftp ssh foo).each do |scheme|
input = %(<a href="#{scheme}://example.org/">foo</a>)
diff --git a/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb b/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb
index 11dddb5f8..f8793cf9f 100644
--- a/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb
+++ b/test/unit/lib/redmine/wiki_formatting/html_sanitizer_test.rb
@@ -35,4 +35,24 @@ class Redmine::WikiFormatting::HtmlSanitizerTest < ActiveSupport::TestCase
input = %(<a href="javascript:alert('hello');">foo</a>)
assert_equal "<a>foo</a>", @sanitizer.call(input)
end
+
+ def test_should_be_strict_with_task_list_items
+ to_test = {
+ %(<input type="checkbox" class="">) => "",
+ %(<input type="checkbox" class="task-list-item-checkbox other">) => "",
+ %(<input type="checkbox" class="task-list-item-checkbox" id="item1">) => %(<input type="checkbox" class="task-list-item-checkbox">),
+ %(<input type="text" class="">) => "",
+ %(<input />) => "",
+ %(<ul class="other"></ul) => "<ul></ul>",
+ %(<ul class="contains-task-list"></ul) => "<ul class=\"contains-task-list\"></ul>",
+ %(<ul class="contains-task-list" id="list1"></ul) => "<ul class=\"contains-task-list\"></ul>",
+ %(<li class="other"></li>) => "",
+ %(<li id="other"></li>) => "",
+ %(<li class="task-list-item"></li>) => "",
+ %(<li class="task-list-item">Item 1</li>) => "Item 1",
+ }
+ to_test.each do |input, result|
+ assert_equal result, @sanitizer.call(input)
+ end
+ end
end
diff --git a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb
index 32280cfdf..678d4c6b2 100644
--- a/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb
+++ b/test/unit/lib/redmine/wiki_formatting/textile_formatter_test.rb
@@ -466,19 +466,19 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
replacement = "New text"
assert_equal(
- [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
+ [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].join("\n\n"),
@formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement)
)
assert_equal(
- [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"),
+ [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].join("\n\n"),
@formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement)
)
assert_equal(
- [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"),
+ [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].join("\n\n"),
@formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement)
)
assert_equal(
- [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"),
+ [STR_WITHOUT_PRE[0..3], replacement].join("\n\n"),
@formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
)
assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
@@ -488,7 +488,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
def test_update_section_with_hash_should_update_the_requested_section
replacement = "New text"
assert_equal(
- [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
+ [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].join("\n\n"),
@formatter.new(TEXT_WITHOUT_PRE).
update_section(2, replacement, ActiveSupport::Digest.hexdigest(STR_WITHOUT_PRE[1]))
)
@@ -552,7 +552,7 @@ class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
text = STR_WITH_PRE.join("\n\n")
replacement = "New text"
assert_equal(
- [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
+ [STR_WITH_PRE[0..1], "New text"].join("\n\n"),
@formatter.new(text).update_section(3, replacement)
)
end
diff --git a/test/unit/member_test.rb b/test/unit/member_test.rb
index f92841b76..df9088027 100644
--- a/test/unit/member_test.rb
+++ b/test/unit/member_test.rb
@@ -79,7 +79,7 @@ class MemberTest < ActiveSupport::TestCase
[1, group_a_member.member_roles.find_by(role_id: 1).id],
[1, group_b_member.member_roles.find_by(role_id: 1).id],
[2, group_b_member.member_roles.find_by(role_id: 2).id],
- ], test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]}
+ ].sort, test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]}.sort
# Verify that a new non-inherited role is added and inherited roles are maintained
test_user_member.set_editable_role_ids([3]) # Add Reporter role to test_user
@@ -88,7 +88,7 @@ class MemberTest < ActiveSupport::TestCase
[1, group_b_member.member_roles.find_by(role_id: 1).id],
[2, group_b_member.member_roles.find_by(role_id: 2).id],
[3, nil]
- ], test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]}
+ ].sort, test_user_member.member_roles.map{|r| [r.role_id, r.inherited_from]}.sort
end
def test_validate
@@ -108,7 +108,7 @@ class MemberTest < ActiveSupport::TestCase
assert !member.save
assert_include I18n.translate('activerecord.errors.messages.empty'), member.errors[:role]
assert_equal 'Rôle doit être renseigné(e)',
- [member.errors.full_messages].flatten.join
+ [member.errors.full_messages].join
end
def test_validate_member_role
diff --git a/test/unit/principal_test.rb b/test/unit/principal_test.rb
index 9eef5bbf3..61f775014 100644
--- a/test/unit/principal_test.rb
+++ b/test/unit/principal_test.rb
@@ -166,4 +166,8 @@ class PrincipalTest < ActiveSupport::TestCase
r = Principal.like('vi_ci')
assert_include user, r
end
+
+ def test_initials_should_return_nil
+ assert_nil Group.first.initials
+ end
end
diff --git a/test/unit/project_admin_query_test.rb b/test/unit/project_admin_query_test.rb
index 421b1f58d..8e58e2efb 100644
--- a/test/unit/project_admin_query_test.rb
+++ b/test/unit/project_admin_query_test.rb
@@ -95,6 +95,7 @@ class ProjectAdminQueryTest < ActiveSupport::TestCase
end
def test_project_statuses_values_should_return_all_statuses
+ set_language_if_valid 'en'
q = ProjectAdminQuery.new
assert_equal [
["active", "1"],
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
index ff2cef903..155a74b64 100644
--- a/test/unit/query_test.rb
+++ b/test/unit/query_test.rb
@@ -623,7 +623,7 @@ class QueryTest < ActiveSupport::TestCase
query.add_filter('due_date', '><t+', ['15'])
issues = find_issues_with_query(query)
assert !issues.empty?
- issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
+ issues.each {|issue| assert(issue.due_date.between?(Date.today, (Date.today + 15)))}
end
def test_operator_less_than_ago
@@ -641,7 +641,7 @@ class QueryTest < ActiveSupport::TestCase
query.add_filter('due_date', '><t-', ['3'])
issues = find_issues_with_query(query)
assert !issues.empty?
- issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
+ issues.each {|issue| assert(issue.due_date.between?((Date.today - 3), Date.today))}
end
def test_operator_more_than_ago
@@ -2326,7 +2326,7 @@ class QueryTest < ActiveSupport::TestCase
values =
issues.filter_map do |i|
begin
- Kernel.Float(i.custom_value_for(c.custom_field).to_s)
+ Kernel.Float(i.custom_value_for(c.custom_field).to_s, exception: false)
rescue
nil
end
diff --git a/test/unit/reaction_test.rb b/test/unit/reaction_test.rb
new file mode 100644
index 000000000..9b3da0738
--- /dev/null
+++ b/test/unit/reaction_test.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006- Jean-Philippe Lang
+#
+# 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.
+
+require_relative '../test_helper'
+
+class ReactionTest < ActiveSupport::TestCase
+ test 'validates :inclusion of reactable_type' do
+ %w(Issue Journal News Comment Message).each do |type|
+ reaction = Reaction.new(reactable_type: type, user: User.new)
+ assert reaction.valid?
+ end
+
+ assert_not Reaction.new(reactable_type: 'InvalidType', user: User.new).valid?
+ end
+
+ test 'scope: by' do
+ user2_reactions = issues(:issues_001).reactions.by(users(:users_002))
+
+ assert_equal [reactions(:reaction_002)], user2_reactions
+ end
+
+ test "should prevent duplicate reactions with unique constraint under concurrent creation" do
+ user = users(:users_001)
+ issue = issues(:issues_004)
+
+ threads = []
+ results = []
+
+ # Ensure both threads start at the same time
+ barrier = Concurrent::CyclicBarrier.new(2)
+
+ # Create two threads to simulate concurrent creation
+ 2.times do
+ threads << Thread.new do
+ barrier.wait # Wait for both threads to be ready
+ begin
+ reaction = Reaction.create(
+ reactable: issue,
+ user: user
+ )
+ results << reaction.persisted?
+ rescue ActiveRecord::RecordNotUnique
+ results << false
+ end
+ end
+ end
+
+ # Wait for both threads to finish
+ threads.each(&:join)
+
+ # Ensure only one reaction was created
+ assert_equal 1, Reaction.where(reactable: issue, user: user).count
+ assert_includes results, true
+ assert_equal 1, results.count(true)
+ end
+
+ test 'build_detail_map_for generates a detail map for reactable objects' do
+ result = Reaction.build_detail_map_for([issues(:issues_001), issues(:issues_006)], users(:users_003))
+
+ expected = {
+ 1 => Reaction::Detail.new(
+ visible_users: [users(:users_003), users(:users_002), users(:users_001)],
+ user_reaction: reactions(:reaction_003)
+ ),
+ 6 => Reaction::Detail.new(
+ visible_users: [users(:users_002)],
+ user_reaction: nil
+ )
+ }
+ assert_equal expected, result
+
+ # When an object have no reactions, the result should be empty.
+ result = Reaction.build_detail_map_for([journals(:journals_002)], users(:users_002))
+
+ assert_empty result
+ end
+
+ test 'build_detail_map_for filters users based on visibility' do
+ current_user = User.generate!
+ visible_user = users(:users_002)
+ non_visible_user = User.generate!
+
+ project = Project.generate!
+ role = Role.generate!(users_visibility: 'members_of_visible_projects')
+
+ User.add_to_project(current_user, project, role)
+ User.add_to_project(visible_user, project, roles(:roles_001))
+
+ issue = Issue.generate!(project: project)
+
+ [current_user, visible_user, non_visible_user].each do |user|
+ issue.reactions.create!(user: user)
+ end
+
+ result = Reaction.build_detail_map_for([issue], current_user)
+
+ assert_equal(
+ [current_user, visible_user].sort_by(&:id),
+ result[issue.id].visible_users.sort_by(&:id)
+ )
+ end
+end
diff --git a/test/unit/repository_bazaar_test.rb b/test/unit/repository_bazaar_test.rb
index 23f3ce48f..5fec37973 100644
--- a/test/unit/repository_bazaar_test.rb
+++ b/test/unit/repository_bazaar_test.rb
@@ -50,6 +50,7 @@ class RepositoryBazaarTest < ActiveSupport::TestCase
:log_encoding => 'UTF-8'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
def test_blank_path_to_repository_error_message
diff --git a/test/unit/repository_cvs_test.rb b/test/unit/repository_cvs_test.rb
index af995eac0..84d0ed80b 100644
--- a/test/unit/repository_cvs_test.rb
+++ b/test/unit/repository_cvs_test.rb
@@ -36,6 +36,7 @@ class RepositoryCvsTest < ActiveSupport::TestCase
:url => MODULE_NAME,
:log_encoding => 'UTF-8')
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
def test_blank_module_error_message
diff --git a/test/unit/repository_git_test.rb b/test/unit/repository_git_test.rb
index ec1ca5157..857be9442 100644
--- a/test/unit/repository_git_test.rb
+++ b/test/unit/repository_git_test.rb
@@ -41,6 +41,7 @@ class RepositoryGitTest < ActiveSupport::TestCase
:path_encoding => 'ISO-8859-1'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
def test_nondefault_repo_with_blank_identifier_destruction
diff --git a/test/unit/repository_mercurial_test.rb b/test/unit/repository_mercurial_test.rb
index 521bd06af..991d19a6d 100644
--- a/test/unit/repository_mercurial_test.rb
+++ b/test/unit/repository_mercurial_test.rb
@@ -35,6 +35,7 @@ class RepositoryMercurialTest < ActiveSupport::TestCase
:path_encoding => 'ISO-8859-1'
)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
def test_blank_path_to_repository_error_message
diff --git a/test/unit/repository_subversion_test.rb b/test/unit/repository_subversion_test.rb
index b4590ce31..dfdf520e7 100644
--- a/test/unit/repository_subversion_test.rb
+++ b/test/unit/repository_subversion_test.rb
@@ -30,6 +30,7 @@ class RepositorySubversionTest < ActiveSupport::TestCase
@repository = Repository::Subversion.create(:project => @project,
:url => self.class.subversion_repository_url)
assert @repository
+ skip "SCM command is unavailable" unless @repository.class.scm_available
end
def test_invalid_url
diff --git a/test/unit/repository_test.rb b/test/unit/repository_test.rb
index 53b5e0ee7..84c22a73f 100644
--- a/test/unit/repository_test.rb
+++ b/test/unit/repository_test.rb
@@ -455,7 +455,7 @@ class RepositoryTest < ActiveSupport::TestCase
def test_stats_by_author_reflect_changesets_and_changes
repository = Repository.find(10)
- expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
+ expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
assert_equal expected, repository.stats_by_author
set = Changeset.create!(
@@ -467,7 +467,7 @@ class RepositoryTest < ActiveSupport::TestCase
)
Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file1')
Change.create!(:changeset => set, :action => 'A', :path => '/path/to/file2')
- expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>5}}
+ expected = {"Dave Lopper"=>{:commits_count=>12, :changes_count=>5}}
assert_equal expected, repository.stats_by_author
end
@@ -476,7 +476,7 @@ class RepositoryTest < ActiveSupport::TestCase
# to ensure things are dynamically linked to Users
User.find_by_login("dlopper").update_attribute(:firstname, "Dave's")
repository = Repository.find(10)
- expected = {"Dave's Lopper"=>{:commits_count=>10, :changes_count=>3}}
+ expected = {"Dave's Lopper"=>{:commits_count=>11, :changes_count=>3}}
assert_equal expected, repository.stats_by_author
end
@@ -502,7 +502,7 @@ class RepositoryTest < ActiveSupport::TestCase
# with committer="dlopper <dlopper@somefoo.net>"
repository = Repository.find(10)
- expected = {"Dave Lopper"=>{:commits_count=>10, :changes_count=>3}}
+ expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
assert_equal expected, repository.stats_by_author
set = Changeset.create!(
@@ -513,7 +513,7 @@ class RepositoryTest < ActiveSupport::TestCase
:comments => 'Another commit by foo.'
)
- expected = {"Dave Lopper"=>{:commits_count=>11, :changes_count=>3}}
+ expected = {"Dave Lopper"=>{:commits_count=>12, :changes_count=>3}}
assert_equal expected, repository.stats_by_author
end
diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb
index 21103919f..1d0d39d7e 100644
--- a/test/unit/role_test.rb
+++ b/test/unit/role_test.rb
@@ -175,6 +175,32 @@ class RoleTest < ActiveSupport::TestCase
assert_equal false, role.permissions_tracker_ids?(:view_issues, 1)
end
+ def test_allowed_to_with_symbol
+ role = Role.create!(:name => 'Test', :permissions => [:view_issues])
+ assert_equal true, role.allowed_to?(:view_issues)
+ assert_equal false, role.allowed_to?(:add_issues)
+ end
+
+ def test_allowed_to_with_symbol_and_scope
+ role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues])
+ assert_equal true, role.allowed_to?(:view_issues, [:view_issues, :add_issues])
+ assert_equal false, role.allowed_to?(:add_issues, [:view_issues, :add_issues])
+ assert_equal false, role.allowed_to?(:delete_issues, [:view_issues, :add_issues])
+ end
+
+ def test_allowed_to_with_hash
+ role = Role.create!(:name => 'Test', :permissions => [:view_issues])
+ assert_equal true, role.allowed_to?(:controller => 'issues', :action => 'show')
+ assert_equal false, role.allowed_to?(:controller => 'issues', :action => 'create')
+ end
+
+ def test_allowed_to_with_hash_and_scope
+ role = Role.create!(:name => 'Test', :permissions => [:view_issues, :delete_issues])
+ assert_equal true, role.allowed_to?({:controller => 'issues', :action => 'show'}, [:view_issues, :add_issues])
+ assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'create'}, [:view_issues, :add_issues])
+ assert_equal false, role.allowed_to?({:controller => 'issues', :action => 'destroy'}, [:view_issues, :add_issues])
+ end
+
def test_has_permission_without_permissions
role = Role.create!(:name => 'Test')
assert_equal false, role.has_permission?(:delete_issues)
diff --git a/test/unit/setting_test.rb b/test/unit/setting_test.rb
index 4ae07cebb..cbfabbb02 100644
--- a/test/unit/setting_test.rb
+++ b/test/unit/setting_test.rb
@@ -147,4 +147,8 @@ class SettingTest < ActiveSupport::TestCase
def test_default_text_formatting_for_new_installations_is_common_mark
assert_equal 'common_mark', Setting.text_formatting
end
+
+ def test_default_wiki_tablesort_enabled_for_new_installations_is_disabled
+ assert_equal "0", Setting.wiki_tablesort_enabled
+ end
end
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index ede12e1ce..967771c87 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -589,6 +589,27 @@ class UserTest < ActiveSupport::TestCase
end
end
+ def test_initials_format
+ assert_equal 'JS', @jsmith.initials(:firstname_lastinitial)
+ assert_equal 'SJ', @jsmith.initials(:lastname_comma_firstname)
+ assert_equal 'SJ', @jsmith.initials(:lastname_firstname)
+ assert_equal 'JS', @jsmith.initials(:firstinitial_lastname)
+ assert_equal 'JL', User.new(:firstname => 'Jean-Philippe', :lastname => 'Lang').initials(:firstinitial_lastname)
+ assert_equal 'JS', @jsmith.initials(:undefined_format)
+ end
+
+ def test_initials_should_use_setting_as_default_format
+ with_settings :user_format => :firstname_lastname do
+ assert_equal 'JS', @jsmith.reload.initials
+ end
+ with_settings :user_format => :username do
+ assert_equal 'JS', @jsmith.reload.initials
+ end
+ with_settings :user_format => :lastname do
+ assert_equal 'SM', @jsmith.reload.initials
+ end
+ end
+
def test_lastname_should_accept_255_characters
u = User.first
u.lastname = 'a' * 255
@@ -1376,4 +1397,77 @@ class UserTest < ActiveSupport::TestCase
User.prune(7)
end
end
+
+ def test_should_recognize_authorized_by_oauth
+ u = User.find 2
+ assert_not u.authorized_by_oauth?
+ u.oauth_scope = [:add_issues, :view_issues]
+ assert u.authorized_by_oauth?
+ end
+
+ def test_admin_should_be_limited_by_oauth_scope
+ u = User.find_by_admin(true)
+ assert u.admin?
+
+ u.oauth_scope = [:add_issues, :view_issues]
+ assert_not u.admin?
+
+ u.oauth_scope = [:add_issues, :view_issues, :admin]
+ assert u.admin?
+
+ u = User.find_by_admin(false)
+ assert_not u.admin?
+ u.oauth_scope = [:add_issues, :view_issues, :admin]
+ assert_not u.admin?
+ end
+
+ def test_oauth_scope_should_limit_global_user_permissions
+ admin = User.find 1
+ user = User.find 2
+ [admin, user].each do |u|
+ assert u.allowed_to?(:add_issues, nil, global: true)
+ assert u.allowed_to?(:view_issues, nil, global: true)
+ u.oauth_scope = [:view_issues]
+ assert_not u.allowed_to?(:add_issues, nil, global: true)
+ assert u.allowed_to?(:view_issues, nil, global: true)
+ end
+ end
+
+ def test_oauth_scope_should_limit_project_user_permissions
+ admin = User.find 1
+ project = Project.find 5
+ assert admin.allowed_to?(:add_issues, project)
+ assert admin.allowed_to?(:view_issues, project)
+ admin.oauth_scope = [:view_issues]
+ assert_not admin.allowed_to?(:add_issues, project)
+ assert admin.allowed_to?(:view_issues, project)
+
+ admin.oauth_scope = [:view_issues, :admin]
+ assert admin.allowed_to?(:add_issues, project)
+ assert admin.allowed_to?(:view_issues, project)
+
+ user = User.find 2
+ project = Project.find 1
+ assert user.allowed_to?(:add_issues, project)
+ assert user.allowed_to?(:view_issues, project)
+ user.oauth_scope = [:view_issues]
+ assert_not user.allowed_to?(:add_issues, project)
+ assert user.allowed_to?(:view_issues, project)
+
+ user.oauth_scope = [:view_issues, :admin]
+ assert_not user.allowed_to?(:add_issues, project)
+ assert user.allowed_to?(:view_issues, project)
+ end
+
+ def test_destroy_should_delete_associated_reactions
+ users(:users_004).reactions.create!(
+ [
+ {reactable: issues(:issues_001)},
+ {reactable: issues(:issues_002)}
+ ]
+ )
+ assert_difference 'Reaction.count', -2 do
+ users(:users_004).destroy
+ end
+ end
end
diff --git a/vendor/javascript/turndown.js b/vendor/javascript/turndown.js
new file mode 100644
index 000000000..337ffd5af
--- /dev/null
+++ b/vendor/javascript/turndown.js
@@ -0,0 +1,110 @@
+// turndown@7.2.0 downloaded from https://ga.jspm.io/npm:turndown@7.2.0/lib/turndown.browser.es.js
+
+function extend(e){for(var n=1;n<arguments.length;n++){var t=arguments[n];for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])}return e}function repeat(e,n){return Array(n+1).join(e)}function trimLeadingNewlines(e){return e.replace(/^\n*/,"")}function trimTrailingNewlines(e){var n=e.length;while(n>0&&e[n-1]==="\n")n--;return e.substring(0,n)}var e=["ADDRESS","ARTICLE","ASIDE","AUDIO","BLOCKQUOTE","BODY","CANVAS","CENTER","DD","DIR","DIV","DL","DT","FIELDSET","FIGCAPTION","FIGURE","FOOTER","FORM","FRAMESET","H1","H2","H3","H4","H5","H6","HEADER","HGROUP","HR","HTML","ISINDEX","LI","MAIN","MENU","NAV","NOFRAMES","NOSCRIPT","OL","OUTPUT","P","PRE","SECTION","TABLE","TBODY","TD","TFOOT","TH","THEAD","TR","UL"];function isBlock(n){return is(n,e)}var n=["AREA","BASE","BR","COL","COMMAND","EMBED","HR","IMG","INPUT","KEYGEN","LINK","META","PARAM","SOURCE","TRACK","WBR"];function isVoid(e){return is(e,n)}function hasVoid(e){return has(e,n)}var t=["A","TABLE","THEAD","TBODY","TFOOT","TH","TD","IFRAME","SCRIPT","AUDIO","VIDEO"];function isMeaningfulWhenBlank(e){return is(e,t)}function hasMeaningfulWhenBlank(e){return has(e,t)}function is(e,n){return n.indexOf(e.nodeName)>=0}function has(e,n){return e.getElementsByTagName&&n.some((function(n){return e.getElementsByTagName(n).length}))}var r={};r.paragraph={filter:"p",replacement:function(e){return"\n\n"+e+"\n\n"}};r.lineBreak={filter:"br",replacement:function(e,n,t){return t.br+"\n"}};r.heading={filter:["h1","h2","h3","h4","h5","h6"],replacement:function(e,n,t){var r=Number(n.nodeName.charAt(1));if(t.headingStyle==="setext"&&r<3){var i=repeat(r===1?"=":"-",e.length);return"\n\n"+e+"\n"+i+"\n\n"}return"\n\n"+repeat("#",r)+" "+e+"\n\n"}};r.blockquote={filter:"blockquote",replacement:function(e){e=e.replace(/^\n+|\n+$/g,"");e=e.replace(/^/gm,"> ");return"\n\n"+e+"\n\n"}};r.list={filter:["ul","ol"],replacement:function(e,n){var t=n.parentNode;return t.nodeName==="LI"&&t.lastElementChild===n?"\n"+e:"\n\n"+e+"\n\n"}};r.listItem={filter:"li",replacement:function(e,n,t){e=e.replace(/^\n+/,"").replace(/\n+$/,"\n").replace(/\n/gm,"\n ");var r=t.bulletListMarker+" ";var i=n.parentNode;if(i.nodeName==="OL"){var a=i.getAttribute("start");var o=Array.prototype.indexOf.call(i.children,n);r=(a?Number(a)+o:o+1)+". "}return r+e+(n.nextSibling&&!/\n$/.test(e)?"\n":"")}};r.indentedCodeBlock={filter:function(e,n){return n.codeBlockStyle==="indented"&&e.nodeName==="PRE"&&e.firstChild&&e.firstChild.nodeName==="CODE"},replacement:function(e,n,t){return"\n\n "+n.firstChild.textContent.replace(/\n/g,"\n ")+"\n\n"}};r.fencedCodeBlock={filter:function(e,n){return n.codeBlockStyle==="fenced"&&e.nodeName==="PRE"&&e.firstChild&&e.firstChild.nodeName==="CODE"},replacement:function(e,n,t){var r=n.firstChild.getAttribute("class")||"";var i=(r.match(/language-(\S+)/)||[null,""])[1];var a=n.firstChild.textContent;var o=t.fence.charAt(0);var l=3;var u=new RegExp("^"+o+"{3,}","gm");var s;while(s=u.exec(a))s[0].length>=l&&(l=s[0].length+1);var c=repeat(o,l);return"\n\n"+c+i+"\n"+a.replace(/\n$/,"")+"\n"+c+"\n\n"}};r.horizontalRule={filter:"hr",replacement:function(e,n,t){return"\n\n"+t.hr+"\n\n"}};r.inlineLink={filter:function(e,n){return n.linkStyle==="inlined"&&e.nodeName==="A"&&e.getAttribute("href")},replacement:function(e,n){var t=n.getAttribute("href");t&&(t=t.replace(/([()])/g,"\\$1"));var r=cleanAttribute(n.getAttribute("title"));r&&(r=' "'+r.replace(/"/g,'\\"')+'"');return"["+e+"]("+t+r+")"}};r.referenceLink={filter:function(e,n){return n.linkStyle==="referenced"&&e.nodeName==="A"&&e.getAttribute("href")},replacement:function(e,n,t){var r=n.getAttribute("href");var i=cleanAttribute(n.getAttribute("title"));i&&(i=' "'+i+'"');var a;var o;switch(t.linkReferenceStyle){case"collapsed":a="["+e+"][]";o="["+e+"]: "+r+i;break;case"shortcut":a="["+e+"]";o="["+e+"]: "+r+i;break;default:var l=this.references.length+1;a="["+e+"]["+l+"]";o="["+l+"]: "+r+i}this.references.push(o);return a},references:[],append:function(e){var n="";if(this.references.length){n="\n\n"+this.references.join("\n")+"\n\n";this.references=[]}return n}};r.emphasis={filter:["em","i"],replacement:function(e,n,t){return e.trim()?t.emDelimiter+e+t.emDelimiter:""}};r.strong={filter:["strong","b"],replacement:function(e,n,t){return e.trim()?t.strongDelimiter+e+t.strongDelimiter:""}};r.code={filter:function(e){var n=e.previousSibling||e.nextSibling;var t=e.parentNode.nodeName==="PRE"&&!n;return e.nodeName==="CODE"&&!t},replacement:function(e){if(!e)return"";e=e.replace(/\r?\n|\r/g," ");var n=/^`|^ .*?[^ ].* $|`$/.test(e)?" ":"";var t="`";var r=e.match(/`+/gm)||[];while(r.indexOf(t)!==-1)t+="`";return t+n+e+n+t}};r.image={filter:"img",replacement:function(e,n){var t=cleanAttribute(n.getAttribute("alt"));var r=n.getAttribute("src")||"";var i=cleanAttribute(n.getAttribute("title"));var a=i?' "'+i+'"':"";return r?"!["+t+"]("+r+a+")":""}};function cleanAttribute(e){return e?e.replace(/(\n+\s*)+/g,"\n"):""}function Rules(e){this.options=e;this._keep=[];this._remove=[];this.blankRule={replacement:e.blankReplacement};this.keepReplacement=e.keepReplacement;this.defaultRule={replacement:e.defaultReplacement};this.array=[];for(var n in e.rules)this.array.push(e.rules[n])}Rules.prototype={add:function(e,n){this.array.unshift(n)},keep:function(e){this._keep.unshift({filter:e,replacement:this.keepReplacement})},remove:function(e){this._remove.unshift({filter:e,replacement:function(){return""}})},forNode:function(e){return e.isBlank?this.blankRule:(n=findRule(this.array,e,this.options))||(n=findRule(this._keep,e,this.options))||(n=findRule(this._remove,e,this.options))?n:this.defaultRule;var n},forEach:function(e){for(var n=0;n<this.array.length;n++)e(this.array[n],n)}};function findRule(e,n,t){for(var r=0;r<e.length;r++){var i=e[r];if(filterValue(i,n,t))return i}}function filterValue(e,n,t){var r=e.filter;if(typeof r==="string"){if(r===n.nodeName.toLowerCase())return true}else if(Array.isArray(r)){if(r.indexOf(n.nodeName.toLowerCase())>-1)return true}else{if(typeof r!=="function")throw new TypeError("`filter` needs to be a string, array, or function");if(r.call(e,n,t))return true}}
+/**
+ * collapseWhitespace(options) removes extraneous whitespace from an the given element.
+ *
+ * @param {Object} options
+ */function collapseWhitespace(e){var n=e.element;var t=e.isBlock;var r=e.isVoid;var i=e.isPre||function(e){return e.nodeName==="PRE"};if(n.firstChild&&!i(n)){var a=null;var o=false;var l=null;var u=next(l,n,i);while(u!==n){if(u.nodeType===3||u.nodeType===4){var s=u.data.replace(/[ \r\n\t]+/g," ");a&&!/ $/.test(a.data)||o||s[0]!==" "||(s=s.substr(1));if(!s){u=remove(u);continue}u.data=s;a=u}else{if(u.nodeType!==1){u=remove(u);continue}if(t(u)||u.nodeName==="BR"){a&&(a.data=a.data.replace(/ $/,""));a=null;o=false}else if(r(u)||i(u)){a=null;o=true}else a&&(o=false)}var c=next(l,u,i);l=u;u=c}if(a){a.data=a.data.replace(/ $/,"");a.data||remove(a)}}}
+/**
+ * remove(node) removes the given node from the DOM and returns the
+ * next node in the sequence.
+ *
+ * @param {Node} node
+ * @return {Node} node
+ */function remove(e){var n=e.nextSibling||e.parentNode;e.parentNode.removeChild(e);return n}
+/**
+ * next(prev, current, isPre) returns the next node in the sequence, given the
+ * current and previous nodes.
+ *
+ * @param {Node} prev
+ * @param {Node} current
+ * @param {Function} isPre
+ * @return {Node}
+ */function next(e,n,t){return e&&e.parentNode===n||t(n)?n.nextSibling||n.parentNode:n.firstChild||n.nextSibling||n.parentNode}var i=typeof window!=="undefined"?window:{};function canParseHTMLNatively(){var e=i.DOMParser;var n=false;try{(new e).parseFromString("","text/html")&&(n=true)}catch(e){}return n}function createHTMLParser(){var Parser=function(){};shouldUseActiveX()?Parser.prototype.parseFromString=function(e){var n=new window.ActiveXObject("htmlfile");n.designMode="on";n.open();n.write(e);n.close();return n}:Parser.prototype.parseFromString=function(e){var n=document.implementation.createHTMLDocument("");n.open();n.write(e);n.close();return n};return Parser}function shouldUseActiveX(){var e=false;try{document.implementation.createHTMLDocument("").open()}catch(n){i.ActiveXObject&&(e=true)}return e}var a=canParseHTMLNatively()?i.DOMParser:createHTMLParser();function RootNode(e,n){var t;if(typeof e==="string"){var r=htmlParser().parseFromString('<x-turndown id="turndown-root">'+e+"</x-turndown>","text/html");t=r.getElementById("turndown-root")}else t=e.cloneNode(true);collapseWhitespace({element:t,isBlock:isBlock,isVoid:isVoid,isPre:n.preformattedCode?isPreOrCode:null});return t}var o;function htmlParser(){o=o||new a;return o}function isPreOrCode(e){return e.nodeName==="PRE"||e.nodeName==="CODE"}function Node(e,n){e.isBlock=isBlock(e);e.isCode=e.nodeName==="CODE"||e.parentNode.isCode;e.isBlank=isBlank(e);e.flankingWhitespace=flankingWhitespace(e,n);return e}function isBlank(e){return!isVoid(e)&&!isMeaningfulWhenBlank(e)&&/^\s*$/i.test(e.textContent)&&!hasVoid(e)&&!hasMeaningfulWhenBlank(e)}function flankingWhitespace(e,n){if(e.isBlock||n.preformattedCode&&e.isCode)return{leading:"",trailing:""};var t=edgeWhitespace(e.textContent);t.leadingAscii&&isFlankedByWhitespace("left",e,n)&&(t.leading=t.leadingNonAscii);t.trailingAscii&&isFlankedByWhitespace("right",e,n)&&(t.trailing=t.trailingNonAscii);return{leading:t.leading,trailing:t.trailing}}function edgeWhitespace(e){var n=e.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);return{leading:n[1],leadingAscii:n[2],leadingNonAscii:n[3],trailing:n[4],trailingNonAscii:n[5],trailingAscii:n[6]}}function isFlankedByWhitespace(e,n,t){var r;var i;var a;if(e==="left"){r=n.previousSibling;i=/ $/}else{r=n.nextSibling;i=/^ /}r&&(r.nodeType===3?a=i.test(r.nodeValue):t.preformattedCode&&r.nodeName==="CODE"?a=false:r.nodeType!==1||isBlock(r)||(a=i.test(r.textContent)));return a}var l=Array.prototype.reduce;var u=[[/\\/g,"\\\\"],[/\*/g,"\\*"],[/^-/g,"\\-"],[/^\+ /g,"\\+ "],[/^(=+)/g,"\\$1"],[/^(#{1,6}) /g,"\\$1 "],[/`/g,"\\`"],[/^~~~/g,"\\~~~"],[/\[/g,"\\["],[/\]/g,"\\]"],[/^>/g,"\\>"],[/_/g,"\\_"],[/^(\d+)\. /g,"$1\\. "]];function TurndownService(e){if(!(this instanceof TurndownService))return new TurndownService(e);var n={rules:r,headingStyle:"setext",hr:"* * *",bulletListMarker:"*",codeBlockStyle:"indented",fence:"```",emDelimiter:"_",strongDelimiter:"**",linkStyle:"inlined",linkReferenceStyle:"full",br:" ",preformattedCode:false,blankReplacement:function(e,n){return n.isBlock?"\n\n":""},keepReplacement:function(e,n){return n.isBlock?"\n\n"+n.outerHTML+"\n\n":n.outerHTML},defaultReplacement:function(e,n){return n.isBlock?"\n\n"+e+"\n\n":e}};this.options=extend({},n,e);this.rules=new Rules(this.options)}TurndownService.prototype={
+/**
+ * The entry point for converting a string or DOM node to Markdown
+ * @public
+ * @param {String|HTMLElement} input The string or DOM node to convert
+ * @returns A Markdown representation of the input
+ * @type String
+ */
+turndown:function(e){if(!canConvert(e))throw new TypeError(e+" is not a string, or an element/document/fragment node.");if(e==="")return"";var n=process.call(this,new RootNode(e,this.options));return postProcess.call(this,n)},
+/**
+ * Add one or more plugins
+ * @public
+ * @param {Function|Array} plugin The plugin or array of plugins to add
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+use:function(e){if(Array.isArray(e))for(var n=0;n<e.length;n++)this.use(e[n]);else{if(typeof e!=="function")throw new TypeError("plugin must be a Function or an Array of Functions");e(this)}return this},
+/**
+ * Adds a rule
+ * @public
+ * @param {String} key The unique key of the rule
+ * @param {Object} rule The rule
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+addRule:function(e,n){this.rules.add(e,n);return this},
+/**
+ * Keep a node (as HTML) that matches the filter
+ * @public
+ * @param {String|Array|Function} filter The unique key of the rule
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+keep:function(e){this.rules.keep(e);return this},
+/**
+ * Remove a node that matches the filter
+ * @public
+ * @param {String|Array|Function} filter The unique key of the rule
+ * @returns The Turndown instance for chaining
+ * @type Object
+ */
+remove:function(e){this.rules.remove(e);return this},
+/**
+ * Escapes Markdown syntax
+ * @public
+ * @param {String} string The string to escape
+ * @returns A string with Markdown syntax escaped
+ * @type String
+ */
+escape:function(e){return u.reduce((function(e,n){return e.replace(n[0],n[1])}),e)}};
+/**
+ * Reduces a DOM node down to its Markdown string equivalent
+ * @private
+ * @param {HTMLElement} parentNode The node to convert
+ * @returns A Markdown representation of the node
+ * @type String
+ */function process(e){var n=this;return l.call(e.childNodes,(function(e,t){t=new Node(t,n.options);var r="";t.nodeType===3?r=t.isCode?t.nodeValue:n.escape(t.nodeValue):t.nodeType===1&&(r=replacementForNode.call(n,t));return join(e,r)}),"")}
+/**
+ * Appends strings as each rule requires and trims the output
+ * @private
+ * @param {String} output The conversion output
+ * @returns A trimmed version of the ouput
+ * @type String
+ */function postProcess(e){var n=this;this.rules.forEach((function(t){typeof t.append==="function"&&(e=join(e,t.append(n.options)))}));return e.replace(/^[\t\r\n]+/,"").replace(/[\t\r\n\s]+$/,"")}
+/**
+ * Converts an element node to its Markdown equivalent
+ * @private
+ * @param {HTMLElement} node The node to convert
+ * @returns A Markdown representation of the node
+ * @type String
+ */function replacementForNode(e){var n=this.rules.forNode(e);var t=process.call(this,e);var r=e.flankingWhitespace;(r.leading||r.trailing)&&(t=t.trim());return r.leading+n.replacement(t,e,this.options)+r.trailing}
+/**
+ * Joins replacement to the current output with appropriate number of new lines
+ * @private
+ * @param {String} output The current conversion output
+ * @param {String} replacement The string to append to the output
+ * @returns Joined output
+ * @type String
+ */function join(e,n){var t=trimTrailingNewlines(e);var r=trimLeadingNewlines(n);var i=Math.max(e.length-t.length,n.length-r.length);var a="\n\n".substring(0,i);return t+a+r}
+/**
+ * Determines whether an input can be converted
+ * @private
+ * @param {String|HTMLElement} input Describe this parameter
+ * @returns Describe what it returns
+ * @type String|Object|Array|Boolean|Number
+ */function canConvert(e){return e!=null&&(typeof e==="string"||e.nodeType&&(e.nodeType===1||e.nodeType===9||e.nodeType===11))}export{TurndownService as default};
+